private readonly Stream _inner; public FileBufferingReadStream(Stream inner) { //接收原始的Request.Body _inner = inner; } public override int Read(Span<byte> buffer) { ThrowIfDisposed(); //如果读取完成过则直接在buffer中获取信息直接返回 if (_buffer.Position < _buffer.Length || _completelyBuffered) { return _buffer.Read(buffer); } //未读取完成才会走到这里 //_inner正是接收的原始的RequestBody //读取的RequestBody放入buffer中 var read = _inner.Read(buffer); //超过设定的长度则会抛出异常 if (_bufferLimit.HasValue && _bufferLimit - read < _buffer.Length) { throw new IOException("Buffer limit exceeded."); } //如果设定存储在内存中并且Body长度大于设定的可存储在内存中的长度,则存储到磁盘中 if (_inMemory && _memoryThreshold - read < _buffer.Length) { _inMemory = false; //缓存原始的Body流 var oldBuffer = _buffer; //创建缓存文件 _buffer = CreateTempFile(); //超过内存存储限制,但是还未写入过临时文件 if (_rentedBuffer == null) { oldBuffer.Position = 0; var rentedBuffer = _bytePool.Rent(Math.Min((int)oldBuffer.Length, _maxRentedBufferSize)); try { //将Body流读取到缓存文件流中 var copyRead = oldBuffer.Read(rentedBuffer); //判断是否读取到结尾 while (copyRead > 0) { //将oldBuffer写入到缓存文件流_buffer当中 _buffer.Write(rentedBuffer.AsSpan(0, copyRead)); copyRead = oldBuffer.Read(rentedBuffer); } } finally { //读取完成之后归还临时缓冲区到ArrayPool中 _bytePool.Return(rentedBuffer); } } else { _buffer.Write(_rentedBuffer.AsSpan(0, (int)oldBuffer.Length)); _bytePool.Return(_rentedBuffer); _rentedBuffer = null; } } //如果读取RequestBody未到结尾,则一直写入到缓存区 if (read > 0) { _buffer.Write(buffer.Slice(0, read)); } else { //如果已经读取RequestBody完毕,也就是写入到缓存完毕则更新_completelyBuffered //标记为以全部读取RequestBody完成,后续在读取RequestBody则直接在_buffer中读取 _completelyBuffered = true; } //返回读取的byte个数用于外部StreamReader判断读取是否完成 return read; }
代码比较多看着也比较复杂,其实核心思路还是比较清晰的,我们来大致的总结一下
首先判断是否完全的读取过原始的RequestBody,如果完全完整的读取过RequestBody则直接在缓冲区中获取返回
如果RequestBody长度大于设定的内存存储限定,则将缓冲写入磁盘临时文件中
如果是首次读取或为完全完整的读取完成RequestBody,那么将RequestBody的内容写入到缓冲区,知道读取完成
其中CreateTempFile这是创建临时文件的操作流,目的是为了将RequestBody的信息写入到临时文件中。可以指定临时文件的地址,若如果不指定则使用系统默认目录,它的实现如下[]
private Stream CreateTempFile() { //判断是否制定过缓存目录,没有的话则使用系统临时文件目录 if (_tempFileDirectory == null) { Debug.Assert(_tempFileDirectoryAccessor != null); _tempFileDirectory = _tempFileDirectoryAccessor(); Debug.Assert(_tempFileDirectory != null); } //临时文件的完整路径 _tempFileName = Path.Combine(_tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid().ToString() + ".tmp"); //返回临时文件的操作流 return new FileStream(_tempFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete, 1024 * 16, FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.SequentialScan); }
我们上面分析了FileBufferingReadStream的Read方法这个方法是同步读取的方法可供StreamReader的ReadToEnd方法使用,当然它还存在一个异步读取方法ReadAsync供StreamReader的ReadToEndAsync方法使用。这两个方法的实现逻辑是完全一致的,只是读取和写入操作都是异步的操作,这里咱们就不介绍那个方法了,有兴趣的同学可以自行了解一下ReadAsync方法的实现[]
当开启EnableBuffering的时候,无论首次读取是设置了AllowSynchronousIO为true的ReadToEnd同步读取方式,还是直接使用ReadToEndAsync的异步读取方式,那么再次使用ReadToEnd同步方式去读取Request.Body也便无需去设置AllowSynchronousIO为true。因为默认的Request.Body已经由HttpRequestStream实例替换为FileBufferingReadStream实例,而FileBufferingReadStream重写了Read和ReadAsync方法,并不存在不允许同步读取的限制。
总结