京东6.18大促主会场领京享红包更优惠

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 9674|回复: 0

ASP.NET Core读取Request.Body的正确方法

[复制链接]

19

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2021-8-8 05:15:39 | 显示全部楼层 |阅读模式 来自 中国
目录
" W) h# z! h8 f, v6 u5 G* o3 ^/ a
    % W6 V8 F" @- ~8 ~" w
  • 前言
    3 \, R! z! E4 U! e: l& W) J
  • 常用读取方式
    # P6 s/ J8 R9 ]& e3 q
  • 同步读取
    0 V6 L3 m3 \2 k- O& [& ^2 p+ L8 m  q8 P
  • 异步读取
    # @& Y' m/ f& m( J+ G
  • 重复读取) x. ~" M8 m7 \0 q' }
  • 源码探究4 J) `# ~$ G0 D+ |) ~
      , C; J: m$ P. z. q
    • StreamReader和Stream的关系
      , e, c: t) o9 v! t0 C( z
    • HttpRequest的Body; q# t" J+ z' y9 Z- B& l
    • AllowSynchronousIO本质来源# A8 j4 o- ~$ n' X% s
    • EnableBuffering神奇的背后# r( k8 T$ e* V* H# }) \8 ~* I

    $ g* y3 f$ f4 L0 d8 L" W7 ~6 a' _
  • 总结! \5 n& x9 _* u0 x, Q

; X2 t. P6 i7 l) E4 R" R8 I8 y前言
: N% s4 |' t( k0 D6 G2 o4 z3 u$ F4 I6 C" V1 ]0 H7 R
相信大家在使用ASP.NET Core进行开发的时候,肯定会涉及到读取Request.Body的场景,毕竟我们大部分的POST请求都是将数据存放到Http的Body当中。因为笔者日常开发所使用的主要也是ASP.NET Core所以笔者也遇到这这种场景,关于本篇文章所套路的内容,来自于在开发过程中我遇到的关于Request.Body的读取问题。在之前的使用的时候,基本上都是借助搜索引擎搜索的答案,并没有太关注这个,发现自己理解的和正确的使用之间存在很大的误区。故有感而发,便写下此文,以作记录。学无止境,愿与君共勉。
' M/ y1 V. C. y- z1 y3 R" W. k( a& C) D  W, e6 Z3 {( r9 U2 S
常用读取方式+ v. r* j$ ?1 U4 H9 V4 Q
5 R/ {0 @" o% z1 z* t: V
当我们要读取Request Body的时候,相信大家第一直觉和笔者是一样的,这有啥难的,直接几行代码写完,这里我们模拟在Filter中读取Request Body,在Action或Middleware或其他地方读取类似,有Request的地方就有Body,如下所示
  1. 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,不过我们借助搜索引擎是我们最大的强项。  E, [0 W: U+ U! o4 s9 F" ]
' Y7 Z2 J, F% i! R
同步读取7 }8 `$ j/ D( C) Y) i1 i  w
) m) R+ Z9 k+ A3 m
首先我们来看设置AllowSynchronousIO为true的方式,看名字也知道是允许同步IO,设置方式大致有两种,待会我们会通过源码来探究一下它们直接有何不同,我们先来看一下如何设置AllowSynchronousIO的值。第一种方式是在ConfigureServices中配置,操作如下
  1. services.Configure(options =>{    options.AllowSynchronousIO = true;});
复制代码
这种方式和在配置文件中配置Kestrel选项配置是一样的只是方式不同,设置完之后即可,运行不在报错。还有一种方式,可以不用在ConfigureServices中设置,通过IHttpBodyControlFeature的方式设置,具体如下
  1. public override void OnActionExecuting(ActionExecutingContext context){    var syncIOFeature = context.HttpContext.Features.Get();    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之前设置一次即可。这两种方式都是去设置AllowSynchronousIO为true,但是我们需要思考一点,微软为何设置AllowSynchronousIO默认为false,说明微软并不希望我们去同步读取Body。通过查找资料得出了这么一个结论
2 Y9 A0 r. H( O8 a
Kestrel:默认情况下禁用 AllowSynchronousIO(同步IO),线程不足会导致应用崩溃,而同步I/O API(例如HttpRequest.Body.Read)是导致线程不足的常见原因。$ B& \, R+ J6 A& r( C- ~, i
由此可以知道,这种方式虽然能解决问题,但是性能并不是不好,微软也不建议这么操作,当程序流量比较大的时候,很容易导致程序不稳定甚至崩溃。. J# I9 R% a' r# k. K- }! r
4 O& c% ]. P! c2 Q5 B1 H! W" Q8 L( [
异步读取- M& k9 d/ C* P2 |0 M  ]

9 ?2 G7 w3 M" L( C$ C) T9 @通过上面我们了解到微软并不希望我们通过设置AllowSynchronousIO的方式去操作,因为会影响性能。那我们可以使用异步的方式去读取,这里所说的异步方式其实就是使用Stream自带的异步方法去读取,如下所示
  1. 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类型的方法,因此我们可以直接使用异步操作
  1. 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();}
复制代码
这两种方式的操作优点是不需要额外设置别的,只是通过异步方法读取即可,也是我们比较推荐的做法。比较神奇的是我们只是将StreamReader的ReadToEnd替换成ReadToEndAsync方法就皆大欢喜了,有没有感觉到比较神奇。当我们感到神奇的时候,是因为我们对它还不够了解,接下来我们就通过源码的方式,一步一步的揭开它神秘的面纱。+ n  X& |2 d$ G1 Q4 @( M6 [6 [. F

+ B5 |+ [; ?; E, U; d( V重复读取% E. \7 ?- q; \! r  R
) G. R/ x$ z9 ~
上面我们演示了使用同步方式和异步方式读取RequestBody,但是这样真的就可以了吗?其实并不行,这种方式每次请求只能读取一次正确的Body结果,如果继续对RequestBody这个Stream进行读取,将读取不到任何内容,首先来举个例子
  1. 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,如下所示
  1. 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
  1. 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,是不是很神奇,待会的时候我们会从源码的角度分析这个问题。
, ?( T0 }2 t* r' I3 n$ H1 i; a' b" H0 @" I, X% {8 ~  l
源码探究. F$ X  ~6 f, }0 x' G

5 ]2 i- i; q: d5 u* B上面我们看到了通过StreamReader的ReadToEnd同步读取Request.Body需要设置AllowSynchronousIO为true才能操作,但是使用StreamReader的ReadToEndAsync方法却可以直接操作。. x+ L+ I6 z1 f. [6 P

0 Z( g: L) e& B0 BStreamReader和Stream的关系
0 C7 z. D$ d% ^5 S! Q- d% Y$ s' D( W; a

7 [- P9 \4 g4 a1 g& k我们看到了都是通过操作StreamReader的方法即可,那关我Request.Body啥事,别急咱们先看一看这里的操作,首先来大致看下ReadToEnd的实现了解一下StreamReader到底和Stream有啥关联,找到ReadToEnd方法[点击查看源码2 W( c2 v- \+ v3 O- j
来源:http://www.jb51.net/article/213540.htm
  G  h- I8 T8 K% n& i$ w! t免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

帖子地址: 

梦想之都-俊月星空 优酷自频道欢迎您 http://i.youku.com/zhaojun917
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐上一条 /6 下一条

QQ|手机版|小黑屋|梦想之都-俊月星空 ( 粤ICP备18056059号 )|网站地图

GMT+8, 2025-8-29 23:39 , Processed in 0.036647 second(s), 23 queries .

Powered by Mxzdjyxk! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表