【问题标题】:Is it safe to use FileStream.seek in this way?以这种方式使用 FileStream.seek 是否安全?
【发布时间】:2015-03-05 02:58:49
【问题描述】:

假设我有一个由一系列对象组成的文件格式,其中每个对象都有以下格式的标题:

public struct FileObjectHeader {
    //The type of the object (not important for this question, but it exists)
    public byte TypeID;
    //The length of the object's data, which DOES NOT include the size of the header.
    public UInt16 Length;
}

后跟指定长度的数据。

我首先通过为每个对象和对象的标题创建一个位置列表来读取这些数据:

struct FileObjectIndex {
    public FileObjectHeader Header;
    public long Location;
}
public List<FileObject> ReadObjects(Stream s) {
    List<FileObjectReference> objectRefs = new List<FileObjectReference>();

    try {
        while (true) {
            FileObjectHeader header = ReadObjectHeader(s); 
            //The above advances the stream by the size of the header as well.
            FileObjectReference reference = new FileObjectReference() { Header = header, Position = stream.Position };
            objectRefs.add(reference);
            //Advance the stream to the next object's header.
            s.Seek(header.Length, SeekOrigin.Current);
        }
    } catch (EndOfStreamException) {
        //Do nothing as this is an expected case
    }

    //Now we'd read all of the objects that we've previously located.
    //This code isn't too important for the question but I'm including it for reference.
    List<FileObject> objects = new List<FileObject>();
    foreach (var reference in objectRefs) {
        s.seek(reference.Location, SeekOrigin.Begin);

        objects.add(ReadObject(reference.Header, s));
    }

    return objects;
}

一些注意事项:

  • ReadObjectHeaderReadObject 方法如果未能读取所有需要的数据(IE,如果它们到达流的末尾)抛出 EndOfStreamException。
  • 我在这里使用 Seek 是因为对象可以引用其他对象,并且将有逻辑确保父对象在其子对象之前加载(文件格式不能保证父对象位于子对象之前)。我没有在上面的示例代码中包含它,因为它会使示例变得复杂,但不会改进它。
  • 在大多数情况下,这可能是只读的FileStream,但我也不能确定。但是,对于这种情况,我主要担心 FileStreams。

我的问题是这样的:

由于我使用的是FileStream.seek,使用搜索是否会导致超出流的末尾并无限期地扩展文件?根据文档:

您可以搜索到流长度之外的任何位置。当您搜索超出文件长度时,文件大小会增加。在 Windows NT 和更高版本中,添加到文件末尾的数据设置为零。在 Windows 98 或更早版本中,添加到文件末尾的数据不会设置为零,这意味着之前删除的数据对流可见。

按照规定的方式,它似乎可以在没有我扩展的情况下扩展文件,从而导致文件不断增长,因为它从标题中读取 3 个字节。实际上,这似乎不会发生,但我想确认它不会发生。

【问题讨论】:

  • 我可能遗漏了一些东西,但是如果您害怕寻找超过原始长度并且只进行读取,为什么不在开始时捕获最大寻找位置,然后进行检查以确保您没有'不过去吗?我认为关于 seek 过去的文档是,如果您要回到位置 0,然后保存流或在需要原始大小的地方使用它——它会比您最初阅读的更大。
  • 我不相信 Seek 实际上会改变文件本身的任何内容...如果您写入文件,它就会增长。请注意,无论如何您都应该打开FileAccess.Read 的文件,因此没有任何操作会更改文件...查看参考源以获取确切的详细信息。

标签: c# .net filestream seek


【解决方案1】:

FileStream.Read() 的文档却说:

返回值
类型:System.Int32
读入缓冲区的总字节数。如果该字节数当前不可用,则该字节数可能小于请求的字节数,或者如果到达流的末尾则为零

因此,我强烈怀疑(但您应该自己验证这一点)这种超越终点的搜索仅适用于您事后写入文件的情况。这是有道理的 - 如果您知道自己需要空间,您可以保留空间,而无需在其中实际写入任何内容(这会很慢)。

但是,在阅读时,我的猜测是您应该得到0 作为回报,并且不会读取任何数据。此外,没有文件扩展。

【讨论】:

    【解决方案2】:

    为了简单地回答您的问题,以下代码不会使您的文件增长。然而,它会抛出新的 EndOfStreamException()。只有在文件末尾之外的位置写入才会使您的文件增长。当文件增长时,当前文件结尾和写入开始之间的数据将用零填充(除非您启用了稀疏标志,在这种情况下它将被标记为未分配)。

    using (var fileStream = new FileStream("f", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
    {
        var buffer = new byte[10];
        fileStream.Seek(10, SeekOrigin.Begin);
        var bytesRead = fileStream.Read(buffer, 0, 10);
        if (bytesRead == 0) {
            throw new EndOfStreamException();
        }
    }
    

    由于您正在读取/写入二进制结构化数据,我建议您做三件事:

    1. 您的二进制结构化数据应在磁盘块中包含整数个元素。在大多数系统上,这是 4096 MSDN。这样做将允许 CLR 将数据直接从 FileSystem 缓存读取到您的缓冲区中。
    2. 使用MemoryMappedFile 和不安全的指针来访问您的数据(如果您的应用仅在 Windows 上运行)。您也可以使用ViewAccessor,但您可能会发现这比自己进行缓存要慢,因为互操作会产生额外的副本。如果你走不安全的路线,这里的代码可以快速填充你的结构:

      internal static class Native
      {
          [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
          private static unsafe extern void CopyMemory(void *dest, void *src, int count);
      
          private static unsafe byte[] Serialize(TestStruct[] index)
          {
              var buffer = new byte[Marshal.SizeOf(typeof(TestStruct)) * index.Length];
              fixed (void* d = &index[0])
              {
                  fixed (void* s = &buffer[0])
                  {
                      CopyMemory(d, s, buffer.Length);
                  }
              }
      
              return buffer;
          }
      }
      

    【讨论】:

    • 这是非常有用的信息。不过,我认为您应该纠正一件事:if (bytesRead) 在 C# 中无效,因为它“无法将类型 int 隐式转换为 bool”,因此可能应该是 if (bytesRead != 0)if (bytesRead != 10)。我还更正了第二个代码块的格式 - 列表格式弄乱了它,它需要第二组缩进。
    猜你喜欢
    • 2021-02-21
    • 1970-01-01
    • 2021-03-05
    • 2013-12-01
    • 1970-01-01
    • 2012-11-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多