【问题标题】:Creating .Net Stream from chunks of data从数据块创建 .Net Stream
【发布时间】:2016-08-03 15:11:36
【问题描述】:

我有一些大型数据文件,我可以使用专门为此设计的 API 以大块(比如 32kb)的形式检索它们。 API 的一种用法如下:

LargeFileAPI lfa = new LargeFileAPI("file1.bin");
bool moredata = true;
List<byte[]> theWholeFile = new List<byte[]>();
while ( moredata  ) 
{
  byte[] arrayRead = new byte[32768];
  moredata = lfa.Read(arrayRead);
  theWholeFile.Add(arrayRead);
}

上面的问题是从它读取占用的内存与大文件的大小一样多(比如说 100Mb)。由于我想将此作为返回结果传递给 WCF 服务,因此我更愿意使用 Stream 作为服务的输出。

如何从中创建一个 Stream 对象并将其作为返回参数传递给 WCF 服务而不占用内存中的完整文件大小?

我正在考虑创建一个继承自

的类 LargeFileStream
System.IO.Stream

并覆盖 Read 方法。但我似乎无法弄清楚如何解决 Stream.Read 需要一个偏移量参数和要读取的字节数这一事实,因为我提到的 API 需要为每次读取读取固定数量的字节。此外,我必须重写的所有其他方法怎么样,例如 Flush()、Position 和其他任何方法。他们应该实施什么?我之所以问,是因为我不知道 Stream.Read() 之外的其他函数,当我从客户端(WCF 服务的调用者)读取流时,WCF 会调用什么函数。

此外,我需要它是可序列化的,以便它可以成为 WCF 服务的输出参数。

谢谢 圣战

【问题讨论】:

    标签: .net wcf stream


    【解决方案1】:

    只需将您的数据存储在这样的临时文件中:

    // create temporary stream
    var stream = new FileStream(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, 4096, FileOptions.DeleteOnClose);
    
    try
    {
        // write all data to temporary stream
        while (moredata) 
        {
            byte[] arrayRead = new byte[32768];
            moredata = lfa.Read(arrayRead);
            stream.Write(arrayRead, 0, arrayRead.Length);
        }
    
        stream.Flush();
    
        stream.Position = 0; // Reset position so stream will be read from beginning
    }
    catch
    {
        stream.Close(); // close stream to delete temporary file if error occured
    }
    

    临时文件流保存从 LargeFileApi 接收的数据。由于数据实际存储在文件中,因此您不会耗尽内存。

    由于FileOptions.DeleteOnClose 选项传递给构造函数,流关闭后临时文件将被删除。因此,如果出现问题或阅读完毕,您可以关闭流。

    【讨论】:

    • 谢谢。这绝对是一条路。不过,我有以下问题,您可以帮我解决: 1- 如果出现问题(例如 Web 服务崩溃),则不会删除该文件。 2-将整个文件写入磁盘然后再次读取它不会那么有效。 3-此外,我确定网络服务有权写入临时文件吗? IIS 设置的默认/最佳实践是什么?众所周知,我们拥有的生产环境非常严格。
    • 关于效率:@Evk 非常了解如何在没有临时缓冲区的情况下将数据作为流读取。但是要实现最好的方法要困难得多,而且还有一个关于 LargeFileApi 是如何工作的问题。如果 LargeFileApi 在完全读取文件之前一直保持连接,则当文件未完成读取时连接可能会过期。我很久以前在生产中遇到过这样的问题(连接过期),并开始使用临时文件的解决方案。因此,我建议测试这两种解决方案并确定最适合您的情况
    • 关于您的担忧: 1. 您可以将 while(modedata) {...} 包装到 try...catch 块中,并在出现异常时关闭流。 2. 这取决于你想用流做什么。 3. 我 90% 确定哪个 Web 服务可以访问 临时目录,但很容易检查以确保 100%
    【解决方案2】:

    你可以编写你的流来做你想做的事,使用一个你的 api 大小的缓冲区(即 32kb)并在阅读时回收它。示例代码如下(不是说它还没有准备好生产并且需要测试,而是让你开始):

    public class LargeFileApiStream : Stream {
        private readonly LargeFileApi _api;
        private bool _hasMore;
        private bool _done;
        private byte[] _buffer;
        const int ApiBufferSize = 32768;
        public LargeFileApiStream(LargeFileApi api) {
            _api = api;    
        }
    
        public override void Flush() {
            // you can ignore that, this stream is not writable
        }
    
        public override long Seek(long offset, SeekOrigin origin) {
            throw new NotSupportedException(); // not seekable, only read from beginning to end
        }
    
        public override void SetLength(long value) {
            throw new NotSupportedException(); // not writable
        }        
    
        public override void Write(byte[] buffer, int offset, int count) {
            throw new NotSupportedException(); // not writable
        }
    
        public override int Read(byte[] buffer, int offset, int count) {
            // if we reached end of stream before - done
            if (_done)
                return 0;
    
            if (_buffer == null) {
                // we are just starting, read first block
                _buffer = new byte[ApiBufferSize];
                _hasMore = _api.Read(_buffer);
            }
    
            var nextIndex = _position % ApiBufferSize;
            int bytesRead = 0;
            for (int i = 0; i < count; i++) {
                if (_buffer.Length <= nextIndex) {
                    // ran out of current chunk - fetch next if possible                    
                    if (_hasMore) {
                        _hasMore = _api.Read(_buffer);
                    }
                    else {
                        // we are done, nothing more to read
                        _done = true;
                        break;
                    }
                    // reset next index back to 0, we are now reading next chunk
                    nextIndex = 0;
                    buffer[offset + i] = _buffer[nextIndex];
                    nextIndex++;
                    bytesRead++;
                }
                else {
                    // write byte to output buffer
                    buffer[offset + i] = _buffer[nextIndex];
                    nextIndex++;
                    bytesRead++;
                }                                                                
            }
    
            _position += bytesRead;
            return bytesRead;
        }
    
        public override bool CanRead {
            get { return true; }
        }
        public override bool CanSeek {
            get { return false; }
        }
        public override bool CanWrite {
            get { return false; }
        }
        public override long Length {
            get { throw new NotSupportedException(); }
        }
    
        private long _position;
        public override long Position
        {
            get { return _position; }
            set { throw new NotSupportedException(); } // not seekable
        }
    }
    

    【讨论】:

    • 这正是我的想法。我只是想确保我的想法是正确的。谢谢
    • 如何确保与 LargeFileAPI 的连接正确关闭。我尝试了一些测试文件 FileStream 并意识到当 WCF 服务停止读取时,Dispose 和 Close 都不会被调用。
    • WCF 应该默认处理返回的流,除非你没有改变它。如果您使用像上面这样的自定义流 - 覆盖基 Stream 类的 Dispose 方法并在那里关闭您的 api 连接。另请参阅:stackoverflow.com/q/6483320/5311735
    • 您好,再次感谢。我已经覆盖了 Stream 的 Dispose,但它没有被调用。知道我可以做些什么来强制调用它。
    • 您是否检查过 AutoDisposeParameters 是否为真(如上面链接中所述)?您是否只是从 WCF 方法调用中返回普通 Stream(未包装在其他响应对象中)?
    【解决方案3】:

    您可以执行以下操作:

    1. 使用 netTcpBinding 创建 WCF 服务。服务可以返回一个应用了 MessageContract 属性的对象

      [消息合约] 公共类大流 {

      [MessageHeader]
      public int Section { get; set; }
      
      [MessageBodyMember]
      public Stream Data { get; set; }
      

      }

    如果您想添加其他元数据,请使用 MessageHeader 属性装饰它们。

    在客户端,Web 应用程序可以使用服务并发出两个请求。

    1. 获取文件的节数
    2. 为每个部分请求流
    3. 下载完成后将所有流合并到一个文件中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-04-14
      • 1970-01-01
      • 2022-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多