深入探究ASP.NET Core读取Request.Body的正确方式

前言

    相信大家在使用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);
}

写完之后,也没多想,毕竟这么常规的操作,信心满满,运行起来调试一把,发现直接报一个这个错System.InvalidOperationException: Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.大致的意思就是同步操作不被允许,请使用ReadAsync的方式或设置AllowSynchronousIO为true。虽然没说怎么设置AllowSynchronousIO,不过我们借助搜索引擎是我们最大的强项。

同步读取

首先我们来看设置AllowSynchronousIOtrue的方式,看名字也知道是允许同步IO,设置方式大致有两种,待会我们会通过源码来探究一下它们直接有何不同,我们先来看一下如何设置AllowSynchronousIO的值。第一种方式是在ConfigureServices中配置,操作如下

services.Configure<KestrelServerOptions>(options =>
{options.AllowSynchronousIO = true;
});

这种方式和在配置文件中配置Kestrel选项配置是一样的只是方式不同,设置完之后即可,运行不在报错。还有一种方式,可以不用在ConfigureServices中设置,通过IHttpBodyControlFeature的方式设置,具体如下

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之前设置一次即可。这两种方式都是去设置AllowSynchronousIOtrue,但是我们需要思考一点,微软为何设置AllowSynchronousIO默认为false,说明微软并不希望我们去同步读取Body。通过查找资料得出了这么一个结论

Kestrel:默认情况下禁用 AllowSynchronousIO(同步IO),线程不足会导致应用崩溃,而同步I/O API(例如HttpRequest.Body.Read)是导致线程不足的常见原因。

由此可以知道,这种方式虽然能解决问题,但是性能并不是不好,微软也不建议这么操作,当程序流量比较大的时候,很容易导致程序不稳定甚至崩溃。

异步读取

通过上面我们了解到微软并不希望我们通过设置AllowSynchronousIO的方式去操作,因为会影响性能。那我们可以使用异步的方式去读取,这里所说的异步方式其实就是使用Stream自带的异步方法去读取,如下所示

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);
}

就这么简单,不需要额外设置其他的东西,仅仅通过ReadToEndAsync的异步方法去操作。ASP.NET Core中许多操作都是异步操作,甚至是过滤器或中间件都可以直接返回Task类型的方法,因此我们可以直接使用异步操作

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();
}

这两种方式的操作优点是不需要额外设置别的,只是通过异步方法读取即可,也是我们比较推荐的做法。比较神奇的是我们只是将StreamReaderReadToEnd替换成ReadToEndAsync方法就皆大欢喜了,有没有感觉到比较神奇。当我们感到神奇的时候,是因为我们对它还不够了解,接下来我们就通过源码的方式,一步一步的揭开它神秘的面纱。

重复读取

上面我们演示了使用同步方式和异步方式读取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();
}

写完之后,开开心心的运行起来看一下效果,发现报了一个错System.NotSupportedException: Specified method is not supported.at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.Seek(Int64 offset, SeekOrigin origin)大致可以理解起来不支持这个操作,至于为啥,一会解析源码的时候咱们一起看一下。说了这么多,那到底该如何解决呢?也很简单,微软知道自己刨下了坑,自然给我们提供了解决办法,用起来也很简单就是加EnableBuffering

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();
}

通过添加Request.EnableBuffering()我们就可以重复的读取RequestBody了,看名字我们可以大概的猜出来,他是和缓存RequestBody有关,需要注意的是Request.EnableBuffering()要加在准备读取RequestBody之前才有效果,否则将无效,而且每次请求只需要添加一次即可。而且大家看到了我第二次读取Body的时候使用了同步的方式去读取的RequestBody,是不是很神奇,待会的时候我们会从源码的角度分析这个问题。

源码探究

上面我们看到了通过StreamReaderReadToEnd同步读取Request.Body需要设置AllowSynchronousIOtrue才能操作,但是使用StreamReaderReadToEndAsync方法却可以直接操作。

StreamReader和Stream的关系

我们看到了都是通过操作StreamReader的方法即可,那关我Request.Body啥事,别急咱们先看一看这里的操作,首先来大致看下ReadToEnd的实现了解一下StreamReader到底和Stream有啥关联,找到ReadToEnd方法[点击查看源码????[1]]

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();
}

通过这段源码我们了解到了这么个信息,一个是StreamReaderReadToEnd其实本质是通过循环读取ReadBuffer然后通过StringBuilder去拼接读取的内容,核心是读取ReadBuffer方法,由于代码比较多,我们找到大致呈现一下核心操作[点击查看源码????[2]]

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;}
}

通过上面的代码我们可以了解到StreamReader其实是工具类,只是封装了对Stream的原始操作,简化我们的代码ReadToEnd方法本质是读取Stream的Read方法。接下来我们看一下ReadToEndAsync方法的具体实现[点击查看源码????[3]]

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();
}

通过这个我们可以看到核心操作是ReadBufferAsync方法,代码比较多我们同样看一下核心实现[点击查看源码????[4]]

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的Request的Body就是Stream这个大家可以自己查看一下,了解到这一步我们就可以继续了。

HttpRequest的Body

上面我们说到了Request的Body本质就是Stream,Stream本身是抽象类,所以Request.Body是Stream的实现类。默认情况下Request.Body的是HttpRequestStream的实例[点击查看源码????[5]],我们这里说了是默认,因为它是可以改变的,我们一会再说。我们从上面StreamReader的结论中得到ReadToEnd本质还是调用的Stream的Read方法,即这里的HttpRequestStream的Read方法,我们来看一下具体实现[点击查看源码????[6]]

public override int Read(byte[] buffer, int offset, int count)
{//知道同步读取Body为啥报错了吧if (!_bodyControl.AllowSynchronousIO){throw new InvalidOperationException(CoreStrings.SynchronousReadsDisallowed);}//本质是调用ReadAsyncreturn 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的属性是来自IHttpBodyControlFeature也就是我们上面介绍的第二种配置方式

private readonly HttpRequestPipeReader _pipeReader;
private readonly IHttpBodyControlFeature _bodyControl;
public HttpRequestStream(IHttpBodyControlFeature bodyControl, HttpRequestPipeReader pipeReader)
{_bodyControl = bodyControl;_pipeReader = pipeReader;
}

那么它和KestrelServerOptions肯定是有关系的,因为我们只配置KestrelServerOptions的是HttpRequestStream的Read是不报异常的,而HttpRequestStream的Read只依赖了IHttpBodyControlFeature的AllowSynchronousIO属性。Kestrel中HttpRequestStream初始化的地方在BodyControl[点击查看源码????[7]]

private readonly HttpRequestStream _request;
public BodyControl(IHttpBodyControlFeature bodyControl, IHttpResponseControl responseControl)
{_request = new HttpRequestStream(bodyControl, _requestReader);
}

而初始化BodyControl的地方在HttpProtocol中,我们找到初始化BodyControl的InitializeBodyControl方法[点击查看源码????[8]]

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中看到了实现关系 [点击查看源码????[9]]

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的地方,找到了AllowSynchronousIO = ServerOptions.AllowSynchronousIO;这段代码说明来自于ServerOptions这个属性,找到初始化ServerOptions的地方[点击查看源码????[10]]

private HttpConnectionContext _context;
//ServiceContext初始化来自HttpConnectionContext 
public ServiceContext ServiceContext => _context.ServiceContext;
protected KestrelServerOptions ServerOptions { get; set; } = default!;
public void Initialize(HttpConnectionContext context)
{_context = context;//来自ServiceContextServerOptions = ServiceContext.ServerOptions;Reset();HttpResponseControl = this;
}

通过这个我们知道ServerOptions来自于ServiceContext的ServerOptions属性,我们找到给ServiceContext赋值的地方,在KestrelServerImpl的CreateServiceContext方法里[点击查看源码????[11]]精简一下逻辑,抽出来核心内容大致实现如下

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,即我们通过services.Configure<KestrelServerOptions>()配置的值,总之得到了这么一个结论

如果配置了KestrelServerOptions即services.Configure(),那么AllowSynchronousIO来自于KestrelServerOptions。即IHttpBodyControlFeature的AllowSynchronousIO属性来自于KestrelServerOptions。如果没有配置,那么直接通过修改IHttpBodyControlFeature实例的 AllowSynchronousIO属性能得到相同的效果,毕竟HttpRequestStream是直接依赖的IHttpBodyControlFeature实例。

EnableBuffering神奇的背后

我们在上面的示例中看到了,如果不添加EnableBuffering的话直接设置RequestBody的Position会报NotSupportedException这么一个错误,而且加了它之后我居然可以直接使用同步的方式去读取RequestBody,首先我们来看一下为啥会报错,我们从上面的错误了解到错误来自于HttpRequestStream这个类[点击查看源码????[12]],上面我们也说了这个类继承了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的时候却没有这些问题,究竟是为什么?接下来我们要揭开它的什么面纱了。首先我们从Request.EnableBuffering()这个方法入手,找到源码位置在HttpRequestRewindExtensions扩展类中[点击查看源码????[13]],我们从最简单的无参方法开始看到如下定义

/// <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);
}

无论那种形式,最终都是在调用BufferingHelper.EnableRewind这个方法,话不多说直接找到BufferingHelper这个类,找到类的位置[点击查看源码????[14]]代码不多而且比较简洁,咱们就把EnableRewind的实现粘贴出来

//默认内存中可缓存的大小为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 Bodyvar 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类型将会是FileBufferingReadStreamrequest.Body = fileStream;//这里要把fileStream注册给Response便于释放request.HttpContext.Response.RegisterForDispose(fileStream);}return request;
}

从上面这段源码实现中我们可以大致得到两个结论

•BufferingHelper的EnableRewind方法也是HttpRequest的扩展方法,可以直接通过Request.EnableRewind的形式调用,效果等同于调用Request.EnableBuffering因为EnableBuffering也是调用的EnableRewind•启用了EnableBuffering这个操作之后实际上会使用FileBufferingReadStream替换掉默认的HttpRequestStream,所以后续处理RequestBody的操作将会是FileBufferingReadStream实例

通过上面的分析我们也清楚的看到了,核心操作在于FileBufferingReadStream这个类,而且从名字也能看出来它肯定是也继承了Stream抽象类,那还等啥直接找到FileBufferingReadStream的实现[点击查看源码????[15]],首先来看他类的定义

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的Seekreturn _buffer.Seek(offset, origin);
}

因为重写了一些关键设置,所以我们可以设置一些流相关的操作。从Seek方法中我们看到了两个比较重要的参数_completelyBuffered_buffer,_completelyBuffered用来判断原始的HttpRequestStream是否读取完成,因为FileBufferingReadStream归根结底还是先读取了HttpRequestStream的内容。_buffer正是承载从HttpRequestStream读取的内容,我们大致抽离一下逻辑看一下,切记这不是全部逻辑,是抽离出来的大致思想

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方法位置[点击查看源码????[16]]

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的信息写入到临时文件中。可以指定临时文件的地址,若如果不指定则使用系统默认目录,它的实现如下[点击查看源码????[17]]

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方法的实现[点击查看源码????[18]]

当开启EnableBuffering的时候,无论首次读取是设置了AllowSynchronousIO为true的ReadToEnd同步读取方式,还是直接使用ReadToEndAsync的异步读取方式,那么再次使用ReadToEnd同步方式去读取Request.Body也便无需去设置AllowSynchronousIO为true。因为默认的Request.Body已经由HttpRequestStream实例替换为FileBufferingReadStream实例,而FileBufferingReadStream重写了Read和ReadAsync方法,并不存在不允许同步读取的限制。

总结

    本篇文章篇幅比较多,如果你想深入的研究相关逻辑,希望本文能给你带来一些阅读源码的指导。为了防止大家深入文章当中而忘记了具体的流程逻辑,在这里我们就大致的总结一下关于正确读取RequestBody的全部结论

•首先关于同步读取Request.Body由于默认的RequestBody的实现是HttpRequestStream,但是HttpRequestStream在重写Read方法的时候会判断是否开启AllowSynchronousIO,如果未开启则直接抛出异常。但是HttpRequestStream的ReadAsync方法并无这种限制,所以使用异步方式的读取RequestBody并无异常。•虽然通过设置AllowSynchronousIO或使用ReadAsync的方式我们可以读取RequestBody,但是RequestBody无法重复读取,这是因为HttpRequestStream的Position和Seek都是不允许进行修改操作的,设置了会直接抛出异常。为了可以重复读取,我们引入了Request的扩展方法EnableBuffering通过这个方法我们可以重置读取位置来实现RequestBody的重复读取。•关于开启EnableBuffering方法每次请求设置一次即可,即在准备读取RequestBody之前设置。其本质其实是使用FileBufferingReadStream代替默认RequestBody的默认类型HttpRequestStream,这样我们在一次Http请求中操作Body的时候其实是操作FileBufferingReadStream,这个类重写Stream的时候Position和Seek都是可以设置的,这样我们就实现了重复读取。•FileBufferingReadStream带给我们的不仅仅是可重复读取,还增加了对RequestBody的缓存功能,使得我们在一次请求中重复读取RequestBody的时候可以在Buffer里直接获取缓存内容而Buffer本身是一个MemoryStream。当然我们也可以自己实现一套逻辑来替换Body,只要我们重写的时候让这个Stream支持重置读取位置即可。

以上就是本次笔者对关于如何更好的方式操作Request.Body的理解,关于讲解内容笔者深知自己能力有限,理解的不一定透彻,甚至理解的不一定对,还望大家多多谅解,也欢迎大家能够多多交流。

References

[1] 点击查看源码????: https://github.com/dotnet/runtime/blob/v5.0.5/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs#L393
[2] 点击查看源码????: https://github.com/dotnet/runtime/blob/v5.0.5/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs#L576
[3] 点击查看源码????: https://github.com/dotnet/runtime/blob/v5.0.5/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs#L898
[4] 点击查看源码????: https://github.com/dotnet/runtime/blob/v5.0.5/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs#L1222
[5] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs
[6] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs#L55
[7] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Servers/Kestrel/Core/src/Internal/Infrastructure/BodyControl.cs
[8] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs#L315
[9] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs
[10] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs#L82
[11] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs#L92
[12] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs
[13] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs
[14] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Http/Http/src/Internal/BufferingHelper.cs
[15] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Http/WebUtilities/src/FileBufferingReadStream.cs
[16] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Http/WebUtilities/src/FileBufferingReadStream.cs#L212
[17] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Http/WebUtilities/src/FileBufferingReadStream.cs#L198
[18] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.5/src/Http/WebUtilities/src/FileBufferingReadStream.cs#L285

????欢迎扫码关注我的公众号????

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/302666.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

追MM的各种算法

全世界有3.14 % 的人已经关注了数据与算法之美动态规划基本上就是说&#xff1a;你追一个MM的时候&#xff0c;需要对该MM身边的各闺中密友都好&#xff0c;这样你追MM这个问题就分解为对其MM朋友的问题&#xff0c;只有把这些问题都解决了&#xff0c;最终你才能追到MM。因此&…

ML.NET 示例:对象检测

ML.NET 版本API 类型状态应用程序类型数据类型场景机器学习任务算法v1.4动态API最新控制台应用程序图像文件对象检测深度学习Tiny Yolo2 ONNX 模型有关如何构建此应用程序的详细说明&#xff0c;请参阅Microsoft Docs网站上附带的教程。问题对象检测是计算机视觉中的一个经典问…

双十一来了,揭秘菜鸟物流背后的那些算法黑科技

又是一年双十一&#xff0c;又一个巨大的挑战摆在了包括菜鸟网络在内的物流面前&#xff0c;但随着人工智能、大数据的应用越来越广泛&#xff0c;无人化仓储、无人化配送越来越多&#xff0c;供应链越来越扁平化&#xff0c;各种算法应用到物流领域的每一个细节。那么&#xf…

开源推荐:Asp.Net Core入门学习手册!

前言推荐一个入门级的.NET Core开源项目&#xff0c;非常适合新手入门学习.NET Core。开源地址:https://github.com/windsting/little-aspnetcore-book。手册在线下载地址&#xff1a;https://nbarbettini.gitbooks.io/little-asp-net-core-book/content/chapters/mvc-basics/c…

如何拿到高薪数据分析师offer?从精准解读一篇招聘信息开始!

大家好&#xff0c;我是大鹏&#xff0c;目前是一名数据分析师&#xff0c;从非本专业成功转行&#xff0c;创立“数据团学社”“城市数据研习社”&#xff0c;运营数十万人社群&#xff0c;联合发起“城市数据团”。最近有很多学弟学妹咨询我入行数据分析师的情况。总体上说&a…

微软翻译api的使用介绍和注意事项

google翻译api已经收费了&#xff0c;而微软翻译api目前是免费的&#xff0c;支持几种不同的方式访问&#xff0c;如果感兴趣可以自己封装下协议处理。官方介绍&#xff1a; http://msdn.microsoft.com/en-us/library/hh454950.aspx。 这里介绍一下java下的使用。Java下使用微软…

linux备份文件到ftp上,Linux服务器下用FTP上传下载备份文件

从linux服务器批量上传/home/test文件夹里面文件到FTP(192.168.1.122)里面wwwroot目录##shell脚本如下&#xff1a;updir/home/testtodirwwwrootip192.168.1.122usertestpasswordtest123123sssfind $updir -type d -printf $todir/’%P\n’| awk ‘{if ($0 “”)next;print “…

为什么不能除以零?

全世界有3.14 % 的人已经关注了数据与算法之美如果你问苹果手机上的Siri&#xff0c;“零除以零等于多少”&#xff0c;它会显示&#xff1a;但是&#xff0c;英文版的Siri还会用语音说这一段话&#xff1a;“假如你有0块饼干&#xff0c;要分给0个朋友&#xff0c;每个人能分到…

设计模式之Builder

2019独角兽企业重金招聘Python工程师标准>>> Builder模式定义&#xff1a; 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示. Builder模式是一步一步创建一个复杂的对象,它允许用户可以只通过指定复杂对象的类型和内容就可以构建它们…

linux kvm系统,linux kvm虚拟机安装以及主机配置支持有哪些?如何远程控制linux kvm系统?...

服务器安装kvm的时候&#xff0c;如果没有安装桌面(一般不会安装&#xff0c;节省资源)&#xff0c;就需要使用命令去创建虚拟机。要支持 KVM&#xff0c;Intel CPU需要 vmx 或者 AMD CPU 需要svm 扩展。在linux系统上使用kvm虚拟化系统需要对主机开启虚拟化,并做一些基础设置,…

C# 答群友:把窗体应用改成类库输出然后去引用

类库你就新建类库文件直接生成dll去引用好了&#xff0c;但是群友非要骚操作&#xff0c;新建一个windows窗体把他搞成dll去引用&#xff0c;当时我回答了这个问题是可以的&#xff0c;但是实际上我没有这么干过&#xff0c;今天恰好有空&#xff0c;体验了一把&#xff0c;的确…

麻省理工告诉我们男女配对的真相!

全世界有3.14 % 的人已经关注了数据与算法之美该实验出自麻省理工学院著名经济学家Dan Ariely的《The Upside of Irrationality》。其结果很有趣&#xff0c;也在我们的生活中尤为常见。Part 1.实验人员找来100位正值青春年华的大学生。男女各半。然后制作了100张卡片&#xff…

快速排序的性能和名字一样优秀

前言上次分享的冒泡排序虽然比较简单、容易理解&#xff0c;但每一次冒泡的过程都需要依次比较相邻的元素&#xff0c;然后交换&#xff0c;可见性能还是有很大的优化空间&#xff0c;只要能减少比较次数&#xff0c;性能自然就上去啦&#xff1b;快速排序便是一个很不错的选择…

资料分享 | 教程与开发手册资料分享来袭

小编从大学开始&#xff0c;便开启资料收集功能。随着科技时代的不断发展&#xff0c;计算机语言发展进入新的阶段&#xff0c;再加上日常的深入研究&#xff0c;小编收集整理了丰富的计算机语言学习资料&#xff0c;内容涵盖“教程与开发手册”&#xff0c;“js资料”、“PHP编…

备受期待的Python深度学习来了

在这个酣畅淋漓的暑假结束后&#xff0c;深度学习的四大名著之一漂洋过海来见中国的程序员们啦&#xff0c;豆瓣评分9.5分的《Deep Learning with Python》&#xff0c;推出中译版——《Python深度学习》Deep Learning with PythonPython深度学习扫码购买优惠近30元&#xff01…

安装linux6.10 I386系统教程,一看就懂的Centos6.10安装教程

第一步将虚拟机安装完毕后&#xff0c;运行虚拟机进行到这个界面下(虚拟机安装及其配置&#xff0c;详见论坛内的博客分享在这就不在陈述)&#xff0c;&#xff1a;出现的是5条英文由上到下以此分别为&#xff1a;1.安装或升级2.基本的显卡驱动来安装系统(在有些操作系统无法识…

计算机是怎么知道两张图片相似的呢?

全世界有3.14 % 的人已经关注了数据与算法之美很多搜索引擎可以用一张图片&#xff0c;搜索互联网上所有与它相似的图片。你输入网片的网址&#xff0c;或者直接上传图片&#xff0c;Google就会找出与其相似的图片。下面这张图片是美国女演员Alyson Hannigan。上传后&#xff0…

WPF实现下拉框带图文和水印

WPF开发者QQ群&#xff1a; 340500857 有小伙伴需要实现ComboBox下拉框带水印&#xff0c;并且选择Item内容后水印默认从中间到顶部。Item需要展示图文&#xff0c;选择后的数据展示图文。欢迎转发、分享、点赞&#xff0c;谢谢大家~。效果如下&#xff1a;一、Xaml代码如下<…

Google第一女神李飞飞,从洗碗工蜕变成为首席科学家

全世界有3.14 % 的人已经关注了数据与算法之美最近这几天&#xff0c;5岁孩子简历事件刷爆了朋友圈。批阅完朋友圈的文章&#xff0c;超模君心好累。别人家的孩子5岁已经每周3篇英语日记&#xff0c;而5岁我却还在玩泥巴&#xff0c;终于明白什么叫输在起跑线上了。即便输在了起…

通过Dapr实现一个简单的基于.net的微服务电商系统(九)——一步一步教你如何撸Dapr之OAuth2授权...

Oauth2授权&#xff0c;熟悉微信开发的同学对这个东西应该不陌生吧。当我们的应用系统需要集成第三方授权时一般都会做oauth集成&#xff0c;今天就来看看在Dapr的语境下我们如何仅通过配置无需修改应用程序的方式让第三方服务保护我们的API应用。目录&#xff1a;一、通过Dapr…