我有用户给我报告一个内存不足的问题,经过了调查,找到了依然是使用已经被标记过时的 HttpWebRequest 进行文件推送,推送过程中,由于 System.Net.RequestStream 将会完全将推送的文件全部读取到内存,导致了在 x86 应用下,推送超过 500MB 的文件,基本上都会抛出 OutOfMemoryException 异常
这是一个 .NET Core 和 .NET Framework 行为的差异。在 .NET Framework 下,调用 WebRequest.Create 方法创建一个 HttpWebRequest 对象,使用 HttpWebRequest 对象调用 GetRequestStream 方法即可获取请求的 Stream 用于写入数据,写入的数据可以是一个文件的信息
在 .NET Framework 下,将会在 GetRequestStream 方法时,尝试和服务器建立连接。对 RequestStream 写入内容,将会发送给到服务器。然而在 .NET Core 里面,这个逻辑和网络优化是冲突的,而且 HttpWebRequest 这个 API 设计本身就存在缺陷。为了让 dotnet 底层的网络通讯方式统一,在 dotnet core 3.1 及更高版本,让 HttpWebRequest 底层走的和 HttpClient 相同的逻辑。当然,我没有考古 dotnet core 3.1 以前的故事
在 dotnet 6 下,调用 GetRequestStream 方法时,将不会立刻和服务器建立连接,这是和 dotnet framework 最大的不同。在 dotnet 6 下,调用 GetRequestStream 方法将立刻返回一个 System.Net.RequestStream 对象,大概代码如下
对 System.Net.RequestStream 对象进行写入时,由于 dotnet 6 下的 GetRequestStream 不会和服务器建立连接,因此写入的数据也不会立刻发送给服务器。这也就是大家将会发现在 dotnet 6 下调用 GetRequestStream 方法将会返回特别快速的原因
既然 RequestStream 不会立刻发送出去,为了不丢失数据,就只能缓存到内存。大家看看 RequestStream 的实现是多么简单,以下代码就是从 dotnet 官方仓库拷贝的,删除了部分不重要的逻辑。可以看到在 RequestStream 的实现里面,其实就是封装一个 MemoryStream 而已,而且只支持写入,写入的内容就放入到 MemoryStream 里面
也如上面代码的注释,在 .NET 6 使用此方法 POST 一段大一点的数据,将会非常的浪费内存。这就是上文说的,对于 x86 应用来说,如果发送一个超过 500MB 的文件,基本上都会抛出内存不足。使用 MemoryStream 时,申请的内存都是两倍两倍申请的,超过 500MB 的数据,将会在 MemoryStream 申请 1GB 的内存空间,对于 x86 的应用来说,基本上能用的内存就是只有 2GB 空间,就为了上传一个文件,申请一段 1GB 的连续空间,对大部分应用来说,即使现在剩余的空间还有超过 1GB 但是剩余的空间却不是连续的,存在一定内存碎片
大家可以看到在 RequestStream 里面,连读取的方法都标记不可用,那在什么使用用到呢。可以看到 RequestStream 多实现了 GetBuffer 方法,这个方法将可以获取所有的数据
在调用 GetResponse 时,才会真的使用 RequestStream 的数据。在 dotnet 6 的调用 GetResponse 方法实现如下
底层调用的是 SendRequest 方法,咱再来看看这个方法是如何使用 RequestStream 数据
可以看到在 HttpWebRequest 底层是通过 HttpClient 来发送网络请求,在如上面代码注释,将 RequestStream 的数据取出作为 ByteArrayContent 进行发送。这是一个很浪费的行为,因为如果能直接使用 HttpClient 进行网络请求,那直接使用 Stream 即可,可以减少一次内存的拷贝和内存占用
也如上面代码,可以看到,完全可以使用 HttpClient 代替 HttpWebRequest 的调用。而且也如上面代码,可以看到 HttpWebRequest 是将请求存放在 _requestStream
字段,天然就不支持复用,从性能和 API 设计,都不如 HttpClient 好用
本文测试代码放在github 和 gitee 欢迎访问
可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源
获取代码之后,进入 BujeardalljelKaifeljaynaba 文件夹
那此内存大量占用问题可以如何解决呢?十分简单,换成 HttpClient 即可
原本 HttpWebRequest 底层就是调用 HttpClient 实现发送网络请求,由因为 HttpWebRequest 的 API 限制,导致了只能将文件的数据先全部读取到内存,再进行发送。如果换成 HttpClient 的话,扔一个 StreamContent 进去即可
上传大文件的时候,还有另外一个坑,那就是上传超时的问题。在 dotnet 6 改了行为,原本的 HttpWebRequest 是分为两个阶段,一个是建立连接的超时判断,另一个是获取响应阶段,在建立连接和获取响应中间的上传数据是不会有超时影响的。但是在 dotnet 6 采用了 HttpClient 作为底层,默认的超时时间是包含整个网络请求活动,也就是建立连接到上传数据完成这个时间不能超时。这个坑将会影响到原本在 .NET Framework 能跑的好好的逻辑,升级到 dotnet 6 将会在上传文件时抛出超时异常。解决方法请看 dotnet 6 使用 HttpClient 的超时机制
原文链接: http://blog.lindexi.com/post/dotnet-6-%E4%BD%BF%E7%94%A8-HttpWebRequest-%E8%BF%9B%E8%A1%8C-POST-%E6%96%87%E4%BB%B6%E5%B0%86%E5%8D%A0%E7%94%A8%E5%A4%A7%E9%87%8F%E5%86%85%E5%AD%98
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 林德熙 (包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我 联系。