在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言相信大家在使用ASP.NET Core进行开发的时候,肯定会涉及到读取Request.Body的场景,毕竟我们大部分的POST请求都是将数据存放到Http的Body当中。因为笔者日常开发所使用的主要也是ASP.NET Core所以笔者也遇到这这种场景,关于本篇文章所套路的内容,来自于在开发过程中我遇到的关于Request.Body的读取问题。在之前的使用的时候,基本上都是借助搜索引擎搜索的答案,并没有太关注这个,发现自己理解的和正确的使用之间存在很大的误区。故有感而发,便写下此文,以作记录。学无止境,愿与君共勉。 常用读取方式当我们要读取Request Body的时候,相信大家第一直觉和笔者是一样的,这有啥难的,直接几行代码写完,这里我们模拟在Filter中读取Request Body,在Action或Middleware或其他地方读取类似,有Request的地方就有Body,如下所示 public override void OnActionExecuting(ActionExecutingContext context) { //在ASP.NET Core中Request Body是Stream的形式 StreamReader stream = new StreamReader(context.HttpContext.Request.Body); string body = stream.ReadToEnd(); _logger.LogDebug("body content:" + body); base.OnActionExecuting(context); } 写完之后,也没多想,毕竟这么常规的操作,信心满满,运行起来调试一把,发现直接报一个这个错 同步读取首先我们来看设置 services.Configure<KestrelServerOptions>(options => { options.AllowSynchronousIO = true; }); 这种方式和在配置文件中配置Kestrel选项配置是一样的只是方式不同,设置完之后即可,运行不在报错。还有一种方式,可以不用在 public override void OnActionExecuting(ActionExecutingContext context) { var syncIOFeature = context.HttpContext.Features.Get<IHttpBodyControlFeature>(); if (syncIOFeature != null) { syncIOFeature.AllowSynchronousIO = true; } StreamReader stream = new StreamReader(context.HttpContext.Request.Body); string body = stream.ReadToEnd(); _logger.LogDebug("body content:" + body); base.OnActionExecuting(context); } 这种方式同样有效,通过这种方式操作,不需要每次读取Body的时候都去设置,只要在准备读取Body之前设置一次即可。这两种方式都是去设置
由此可以知道,这种方式虽然能解决问题,但是性能并不是不好,微软也不建议这么操作,当程序流量比较大的时候,很容易导致程序不稳定甚至崩溃。 异步读取通过上面我们了解到微软并不希望我们通过设置 public override void OnActionExecuting(ActionExecutingContext context) { StreamReader stream = new StreamReader(context.HttpContext.Request.Body); string body = stream.ReadToEndAsync().GetAwaiter().GetResult(); _logger.LogDebug("body content:" + body); base.OnActionExecuting(context); } 就这么简单,不需要额外设置其他的东西,仅仅通过 public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { StreamReader stream = new StreamReader(context.HttpContext.Request.Body); string body = await stream.ReadToEndAsync(); _logger.LogDebug("body content:" + body); await next(); } 这两种方式的操作优点是不需要额外设置别的,只是通过异步方法读取即可,也是我们比较推荐的做法。比较神奇的是我们只是将 重复读取上面我们演示了使用同步方式和异步方式读取RequestBody,但是这样真的就可以了吗?其实并不行,这种方式每次请求只能读取一次正确的Body结果,如果继续对RequestBody这个Stream进行读取,将读取不到任何内容,首先来举个例子 public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { StreamReader stream = new StreamReader(context.HttpContext.Request.Body); string body = await stream.ReadToEndAsync(); _logger.LogDebug("body content:" + body); StreamReader stream2 = new StreamReader(context.HttpContext.Request.Body); string body2 = await stream2.ReadToEndAsync(); _logger.LogDebug("body2 content:" + body2); await next(); } 上面的例子中body里有正确的RequestBody的结果,但是body2中是空字符串。这个情况是比较糟糕的,为啥这么说呢?如果你是在Middleware中读取的RequestBody,而这个中间件的执行是在模型绑定之前,那么将会导致模型绑定失败,因为模型绑定有的时候也需要读取RequestBody获取http请求内容。至于为什么会这样相信大家也有了一定的了解,因为我们在读取完Stream之后,此时的Stream指针位置已经在Stream的结尾处,即Position此时不为0,而Stream读取正是依赖Position来标记外部读取Stream到啥位置,所以我们再次读取的时候会从结尾开始读,也就读取不到任何信息了。所以我们要想重复读取RequestBody那么就要再次读取之前重置RequestBody的Position为0,如下所示 public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { StreamReader stream = new StreamReader(context.HttpContext.Request.Body); string body = await stream.ReadToEndAsync(); _logger.LogDebug("body content:" + body); //或者使用重置Position的方式 context.HttpContext.Request.Body.Position = 0; //如果你确定上次读取完之后已经重置了Position那么这一句可以省略 context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin); StreamReader stream2 = new StreamReader(context.HttpContext.Request.Body); string body2 = await stream2.ReadToEndAsync(); //用完了我们尽量也重置一下,自己的坑自己填 context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin); _logger.LogDebug("body2 content:" + body2); await next(); } 写完之后,开开心心的运行起来看一下效果,发现报了一个错 public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { //操作Request.Body之前加上EnableBuffering即可 context.HttpContext.Request.EnableBuffering(); StreamReader stream = new StreamReader(context.HttpContext.Request.Body); string body = await stream.ReadToEndAsync(); _logger.LogDebug("body content:" + body); context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin); StreamReader stream2 = new StreamReader(context.HttpContext.Request.Body); //注意这里!!!我已经使用了同步读取的方式 string body2 = stream2.ReadToEnd(); context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin); _logger.LogDebug("body2 content:" + body2); await next(); } 通过添加 源码探究上面我们看到了通过 StreamReader和Stream的关系我们看到了都是通过操作 public override string ReadToEnd() { ThrowIfDisposed(); CheckAsyncTaskInProgress(); // 调用ReadBuffer,然后从charBuffer中提取数据。 StringBuilder sb = new StringBuilder(_charLen - _charPos); do { //循环拼接读取内容 sb.Append(_charBuffer, _charPos, _charLen - _charPos); _charPos = _charLen; //读取buffer,这是核心操作 ReadBuffer(); } while (_charLen > 0); //返回读取内容 return sb.ToString(); } 通过这段源码我们了解到了这么个信息,一个是 if (_checkPreamble) { //通过这里我们可以知道本质就是使用要读取的Stream里的Read方法 int len = _stream.Read(_byteBuffer, _bytePos, _byteBuffer.Length - _bytePos); if (len == 0) { if (_byteLen > 0) { _charLen += _decoder.GetChars(_byteBuffer, 0, _byteLen, _charBuffer, _charLen); _bytePos = _byteLen = 0; } return _charLen; } _byteLen += len; } else { //通过这里我们可以知道本质就是使用要读取的Stream里的Read方法 _byteLen = _stream.Read(_byteBuffer, 0, _byteBuffer.Length); if (_byteLen == 0) { return _charLen; } } 通过上面的代码我们可以了解到 public override Task<string> ReadToEndAsync() { if (GetType() != typeof(StreamReader)) { return base.ReadToEndAsync(); } ThrowIfDisposed(); CheckAsyncTaskInProgress(); //本质是ReadToEndAsyncInternal方法 Task<string> task = ReadToEndAsyncInternal(); _asyncReadTask = task; return task; } private async Task<string> ReadToEndAsyncInternal() { //也是循环拼接读取的内容 StringBuilder sb = new StringBuilder(_charLen - _charPos); do { int tmpCharPos = _charPos; sb.Append(_charBuffer, tmpCharPos, _charLen - tmpCharPos); _charPos = _charLen; //核心操作是ReadBufferAsync方法 await ReadBufferAsync(CancellationToken.None).ConfigureAwait(false); } while (_charLen > 0); return sb.ToString(); } 通过这个我们可以看到核心操作是 byte[] tmpByteBuffer = _byteBuffer; //Stream赋值给tmpStream Stream tmpStream = _stream; if (_checkPreamble) { int tmpBytePos = _bytePos; //本质是调用Stream的ReadAsync方法 int len = await tmpStream.ReadAsync(new Memory<byte>(tmpByteBuffer, tmpBytePos, tmpByteBuffer.Length - tmpBytePos), cancellationToken).ConfigureAwait(false); if (len == 0) { if (_byteLen > 0) { _charLen += _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, _charLen); _bytePos = 0; _byteLen = 0; } return _charLen; } _byteLen += len; } else { //本质是调用Stream的ReadAsync方法 _byteLen = await tmpStream.ReadAsync(new Memory<byte>(tmpByteBuffer), cancellationToken).ConfigureAwait(false); if (_byteLen == 0) { return _charLen; } } 通过上面代码我可以了解到StreamReader的本质就是读取Stream的包装,核心方法还是来自Stream本身。我们之所以大致介绍了StreamReader类,就是为了给大家呈现出StreamReader和Stream的关系,否则怕大家误解这波操作是StreamReader的里的实现,而不是Request.Body的问题,其实并不是这样的所有的一切都是指向Stream的 HttpRequest的Body上面我们说到了Request的Body本质就是Stream,Stream本身是抽象类,所以Request.Body是Stream的实现类。默认情况下Request.Body的是HttpRequestStream的实例[点击查看源码👈],我们这里说了是默认,因为它是可以改变的,我们一会再说。我们从上面StreamReader的结论中得到ReadToEnd本质还是调用的Stream的Read方法,即这里的HttpRequestStream的Read方法,我们来看一下具体实现[点击查看源码👈] public override int Read(byte[] buffer, int offset, int count) { //知道同步读取Body为啥报错了吧 if (!_bodyControl.AllowSynchronousIO) { throw new InvalidOperationException(CoreStrings.SynchronousReadsDisallowed); } //本质是调用ReadAsync return ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); } 通过这段代码我们就可以知道了为啥在不设置AllowSynchronousIO为true的情下读取Body会抛出异常了吧,这个是程序级别的控制,而且我们还了解到Read的本质还是在调用ReadAsync异步方法 public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default) { return ReadAsyncWrapper(destination, cancellationToken); } ReadAsync本身并无特殊限制,所以直接操作ReadAsync不会存在类似Read的异常。 通过这个我们得出了结论Request.Body即HttpRequestStream的同步读取Read会抛出异常,而异步读取ReadAsync并不会抛出异常只和HttpRequestStream的Read方法本身存在判断AllowSynchronousIO的值有关系。 AllowSynchronousIO本质来源通过HttpRequestStream的Read方法我们可以知道AllowSynchronousIO控制了同步读取的方式。而且我们还了解到了AllowSynchronousIO有几种不同方式的去配置,接下来我们来大致看下几种方式的本质是哪一种。通过HttpRequestStream我们知道Read方法中的AllowSynchronousIO的属性是来自 private readonly HttpRequestPipeReader _pipeReader; private readonly IHttpBodyControlFeature _bodyControl; public HttpRequestStream(IHttpBodyControlFeature bodyControl, HttpRequestPipeReader pipeReader) { _bodyControl = bodyControl; _pipeReader = pipeReader; } 那么它和 private readonly HttpRequestStream _request; public BodyControl(IHttpBodyControlFeature bodyControl, IHttpResponseControl responseControl) { _request = new HttpRequestStream(bodyControl, _requestReader); } 而初始化BodyControl的地方在HttpProtocol中,我们找到初始化BodyControl的InitializeBodyControl方法[点击查看源码👈] public void InitializeBodyControl(MessageBody messageBody) { if (_bodyControl == null) { //这里传递的是bodyControl传递的是this _bodyControl = new BodyControl(bodyControl: this, this); } (RequestBody, ResponseBody, RequestBodyPipeReader, ResponseBodyPipeWriter) = _bodyControl.Start(messageBody); _requestStreamInternal = RequestBody; _responseStreamInternal = ResponseBody; } 这里我们可以看的到初始化IHttpBodyControlFeature既然传递的是this,也就是HttpProtocol当前实例。也就是说HttpProtocol是实现了IHttpBodyControlFeature接口,HttpProtocol本身是partial的,我们在其中一个分布类HttpProtocol.FeatureCollection中看到了实现关系 internal partial class HttpProtocol : IHttpRequestFeature, IHttpRequestBodyDetectionFeature, IHttpResponseFeature, IHttpResponseBodyFeature, IRequestBodyPipeFeature, IHttpUpgradeFeature, IHttpConnectionFeature, IHttpRequestLifetimeFeature, IHttpRequestIdentifierFeature, IHttpRequestTrailersFeature, IHttpBodyControlFeature, IHttpMaxRequestBodySizeFeature, IEndpointFeature, IRouteValuesFeature { bool IHttpBodyControlFeature.AllowSynchronousIO { get => AllowSynchronousIO; set => AllowSynchronousIO = value; } } 通过这个可以看出HttpProtocol确实实现了IHttpBodyControlFeature接口,接下来我们找到初始化AllowSynchronousIO的地方,找到了 private HttpConnectionContext _context; //ServiceContext初始化来自HttpConnectionContext public ServiceContext ServiceContext => _context.ServiceContext; protected KestrelServerOptions ServerOptions { get; set; } = default!; public void Initialize(HttpConnectionContext context) { _context = context; //来自ServiceContext ServerOptions = ServiceContext.ServerOptions; Reset(); HttpResponseControl = this; } 通过这个我们知道ServerOptions来自于ServiceContext的ServerOptions属性,我们找到给ServiceContext赋值的地方,在KestrelServerImpl的CreateServiceContext方法里[点击查看源码👈]精简一下逻辑,抽出来核心内容大致实现如下 public KestrelServerImpl( IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, ILoggerFactory loggerFactory) //注入进来的IOptions<KestrelServerOptions>调用了CreateServiceContext : this(transportFactories, null, CreateServiceContext(options, loggerFactory)) { } private static ServiceContext CreateServiceContext(IOptions<KestrelServerOptions> options, ILoggerFactory loggerFactory) { //值来自于IOptions<KestrelServerOptions> var serverOptions = options.Value ?? new KestrelServerOptions(); return new ServiceContext { Log = trace, HttpParser = new HttpParser<Http1ParsingHandler>(trace.IsEnabled(LogLevel.Information)), Scheduler = PipeScheduler.ThreadPool, SystemClock = heartbeatManager, DateHeaderValueManager = dateHeaderValueManager, ConnectionManager = connectionManager, Heartbeat = heartbeat, //赋值操作 ServerOptions = serverOptions, }; } 通过上面的代码我们可以看到如果配置了KestrelServerOptions那么ServiceContext的ServerOptions属性就来自于KestrelServerOptions,即我们通过 如果配置了KestrelServerOptions即services.Configure EnableBuffering神奇的背后我们在上面的示例中看到了,如果不添加EnableBuffering的话直接设置RequestBody的Position会报NotSupportedException这么一个错误,而且加了它之后我居然可以直接使用同步的方式去读取RequestBody,首先我们来看一下为啥会报错,我们从上面的错误了解到错误来自于HttpRequestStream这个类[点击查看源码👈],上面我们也说了这个类继承了Stream抽象类,通过源码我们可以看到如下相关代码 //不能使用Seek操作 public override bool CanSeek => false; //允许读 public override bool CanRead => true; //不允许写 public override bool CanWrite => false; //不能获取长度 public override long Length => throw new NotSupportedException(); //不能读写Position public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } //不能使用Seek方法 public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } 相信通过这些我们可以清楚的看到针对HttpRequestStream的设置或者写相关的操作是不被允许的,这也是为啥我们上面直接通过Seek设置Position的时候为啥会报错,还有一些其他操作的限制,总之默认是不希望我们对HttpRequestStream做过多的操作,特别是设置或者写相关的操作。但是我们使用EnableBuffering的时候却没有这些问题,究竟是为什么?接下来我们要揭开它的什么面纱了。首先我们从 /// <summary> /// 确保Request.Body可以被多次读取 /// </summary> /// <param name="request"></param> public static void EnableBuffering(this HttpRequest request) { BufferingHelper.EnableRewind(request); } 上面的方法是最简单的形式,还有一个EnableBuffering的扩展方法是参数最全的扩展方法,这个方法可以控制读取的大小和控制是否存储到磁盘的限定大小 /// <summary> /// 确保Request.Body可以被多次读取 /// </summary> /// <param name="request"></param> /// <param name="bufferThreshold">内存中用于缓冲流的最大大小(字节)。较大的请求主体被写入磁盘。</param> /// <param name="bufferLimit">请求正文的最大大小(字节)。尝试读取超过此限制将导致异常</param> public static void EnableBuffering(this HttpRequest request, int bufferThreshold, long bufferLimit) { BufferingHelper.EnableRewind(request, bufferThreshold, bufferLimit); } 无论那种形式,最终都是在调用 //默认内存中可缓存的大小为30K,超过这个大小将会被存储到磁盘 internal const int DefaultBufferThreshold = 1024 * 30; /// <summary> /// 这个方法也是HttpRequest扩展方法 /// </summary> /// <returns></returns> public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) { if (request == null) { throw new ArgumentNullException(nameof(request)); } //先获取Request Body var body = request.Body; //默认情况Body是HttpRequestStream这个类CanSeek是false所以肯定会执行到if逻辑里面 if (!body.CanSeek) { //实例化了FileBufferingReadStream这个类,看来这是关键所在 var fileStream = new FileBufferingReadStream(body, bufferThreshold,bufferLimit,AspNetCoreTempDirectory.TempDirectoryFactory); //赋值给Body,也就是说开启了EnableBuffering之后Request.Body类型将会是FileBufferingReadStream request.Body = fileStream; //这里要把fileStream注册给Response便于释放 request.HttpContext.Response.RegisterForDispose(fileStream); } return request; } 从上面这段源码实现中我们可以大致得到两个结论
通过上面的分析我们也清楚的看到了,核心操作在于 public class FileBufferingReadStream : Stream { } 毋庸置疑确实是继承自Steam类,我们上面也看到了使用了Request.EnableBuffering之后就可以设置和重复读取RequestBody,说明进行了一些重写操作,具体我们来看一下 /// <summary> /// 允许读 /// </summary> public override bool CanRead { get { return true; } } /// <summary> /// 允许Seek /// </summary> public override bool CanSeek { get { return true; } } /// <summary> /// 不允许写 /// </summary> public override bool CanWrite { get { return false; } } /// <summary> /// 可以获取长度 /// </summary> public override long Length { get { return _buffer.Length; } } /// <summary> /// 可以读写Position /// </summary> public override long Position { get { return _buffer.Position; } set { ThrowIfDisposed(); _buffer.Position = value; } } public override long Seek(long offset, SeekOrigin origin) { //如果Body已释放则异常 ThrowIfDisposed(); //特殊情况抛出异常 //_completelyBuffered代表是否完全缓存一定是在原始的HttpRequestStream读取完成后才置为true //出现没读取完成但是原始位置信息和当前位置信息不一致则直接抛出异常 if (!_completelyBuffered && origin == SeekOrigin.End) { throw new NotSupportedException("The content has not been fully buffered yet."); } else if (!_completelyBuffered && origin == SeekOrigin.Current && offset + Position > Length) { throw new NotSupportedException("The content has not been fully buffered yet."); } else if (!_completelyBuffered && origin == SeekOrigin.Begin && offset > Length) { throw new NotSupportedException("The content has not been fully buffered yet."); } //充值buffer的Seek return _buffer.Seek(offset, origin); } 因为重写了一些关键设置,所以我们可以设置一些流相关的操作。从Seek方法中我们看到了两个比较重要的参数 private readonly ArrayPool<byte> _bytePool; private const int _maxRentedBufferSize = 1024 * 1024; //1MB private Stream _buffer; public FileBufferingReadStream(int memoryThreshold) { //即使我们设置memoryThreshold那么它最大也不能超过1MB否则也会存储在磁盘上 if (memoryThreshold <= _maxRentedBufferSize) { _rentedBuffer = bytePool.Rent(memoryThreshold); _buffer = new MemoryStream(_rentedBuffer); _buffer.SetLength(0); } else { //超过1M将缓存到磁盘所以仅仅初始化 _buffer = new MemoryStream(); } } 这些都是一些初始化的操作,核心操作当然还是在FileBufferingReadStream的Read方法里,因为真正读取的地方就在这,我们找到Read方法位置[点击查看源码👈] 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; } 代码比较多看着也比较复杂,其实核心思路还是比较清晰的,我们来大致的总结一下
其中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方法使用。这两个方法的实现逻辑是完全一致的,只是读取和写入操作都是异步的操作,这里咱们就不介绍那个方法了,有兴趣的同学可以自行了解一下 当开启EnableBuffering的时候,无论首次读取是设置了AllowSynchronousIO为true的ReadToEnd同步读取方式,还是直接使用ReadToEndAsync的异步读取方式,那么再次使用ReadToEnd同步方式去读取Request.Body也便无需去设置AllowSynchronousIO为true。因为默认的Request.Body已经由HttpRequestStream实例替换为FileBufferingReadStream实例,而FileBufferingReadStream重写了Read和ReadAsync方法,并不存在不允许同步读取的限制。 总结本篇文章篇幅比较多,如果你想深入的研究相关逻辑,希望本文能给你带来一些阅读源码的指导。为了防止大家深入文章当中而忘记了具体的流程逻辑,在这里我们就大致的总结一下关于正确读取RequestBody的全部结论
到此这篇关于ASP.NET Core读取Request.Body的正确方法的文章就介绍到这了,更多相关ASP.NET Core读取Request.Body内容请搜索极客世界以前的文章或继续浏览下面的相关文章希望大家以后多多支持极客世界! |
请发表评论