dotnet 6 使用 HttpWebRequest 进行 POST 文件将占用大量内存
我有用户给我报告一个内存不足的问题,经过了调查,找到了依然是使用已经被标记过时的 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 对象,大概代码如下
public override Stream GetRequestStream()
{
return InternalGetRequestStream().Result;
}
private Task<Stream> InternalGetRequestStream()
{
_requestStream = new RequestStream();
return Task.FromResult((Stream)_requestStream);
}
对 System.Net.RequestStream 对象进行写入时,由于 dotnet 6 下的 GetRequestStream 不会和服务器建立连接,因此写入的数据也不会立刻发送给服务器。这也就是大家将会发现在 dotnet 6 下调用 GetRequestStream 方法将会返回特别快速的原因
既然 RequestStream 不会立刻发送出去,为了不丢失数据,就只能缓存到内存。大家看看 RequestStream 的实现是多么简单,以下代码就是从 dotnet 官方仓库拷贝的,删除了部分不重要的逻辑。可以看到在 RequestStream 的实现里面,其实就是封装一个 MemoryStream 而已,而且只支持写入,写入的内容就放入到 MemoryStream 里面
namespace System.Net
{
// Cache the request stream into a MemoryStream. This is the
// default behavior of Desktop HttpWebRequest.AllowWriteStreamBuffering (true).
// Unfortunately, this property is not exposed in .NET Core, so it can't be changed
// This will result in inefficient memory usage when sending (POST'ing) large
// amounts of data to the server such as from a file stream.
internal sealed class RequestStream : Stream
{
private readonly MemoryStream _buffer = new MemoryStream();
public RequestStream()
{
}
public override void Flush()
{
// Nothing to do.
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
// Nothing to do.
return cancellationToken.IsCancellationRequested ?
Task.FromCanceled(cancellationToken) :
Task.CompletedTask;
}
public override long Length
{
get
{
throw new NotSupportedException();
}
}
public override long Position
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
ValidateBufferArguments(buffer, offset, count);
_buffer.Write(buffer, offset, count);
}
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ValidateBufferArguments(buffer, offset, count);
return _buffer.WriteAsync(buffer, offset, count, cancellationToken);
}
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? asyncCallback, object? asyncState)
{
ValidateBufferArguments(buffer, offset, count);
return _buffer.BeginWrite(buffer, offset, count, asyncCallback, asyncState);
}
public override void EndWrite(IAsyncResult asyncResult)
{
_buffer.EndWrite(asyncResult);
}
public ArraySegment<byte> GetBuffer()
{
ArraySegment<byte> bytes;
bool success = _buffer.TryGetBuffer(out bytes);
Debug.Assert(success); // Buffer should always be visible since default MemoryStream constructor was used.
return bytes;
}
}
}
也如上面代码的注释,在 .NET 6 使用此方法 POST 一段大一点的数据,将会非常的浪费内存。这就是上文说的,对于 x86 应用来说,如果发送一个超过 500MB 的文件,基本上都会抛出内存不足。使用 MemoryStream 时,申请的内存都是两倍两倍申请的,超过 500MB 的数据,将会在 MemoryStream 申请 1GB 的内存空间,对于 x86 的应用来说,基本上能用的内存就是只有 2GB 空间,就为了上传一个文件,申请一段 1GB 的连续空间,对大部分应用来说,即使现在剩余的空间还有超过 1GB 但是剩余的空间却不是连续的,存在一定内存碎片
大家可以看到在 RequestStream 里面,连读取的方法都标记不可用,那在什么使用用到呢。可以看到 RequestStream 多实现了 GetBuffer 方法,这个方法将可以获取所有的数据
在调用 GetResponse 时,才会真的使用 RequestStream 的数据。在 dotnet 6 的调用 GetResponse 方法实现如下
public override WebResponse GetResponse()
{
try
{
_sendRequestCts = new CancellationTokenSource();
return SendRequest(async: false).GetAwaiter().GetResult();
}
catch (Exception ex)
{
throw WebException.CreateCompatibleException(ex);
}
}
底层调用的是 SendRequest 方法,咱再来看看这个方法是如何使用 RequestStream 数据
private async Task<WebResponse> SendRequest(bool async)
{
var request = new HttpRequestMessage(new HttpMethod(_originVerb), _requestUri);
bool disposeRequired = false;
HttpClient? client = null;
try
{
client = GetCachedOrCreateHttpClient(async, out disposeRequired);
if (_requestStream != null)
{
// 在这里使用到 RequestStream 数据
ArraySegment<byte> bytes = _requestStream.GetBuffer();
request.Content = new ByteArrayContent(bytes.Array!, bytes.Offset, bytes.Count);
}
// Copy the HttpWebRequest request headers from the WebHeaderCollection into HttpRequestMessage.Headers and
// HttpRequestMessage.Content.Headers.
foreach (string headerName in _webHeaderCollection)
{
// The System.Net.Http APIs require HttpRequestMessage headers to be properly divided between the request headers
// collection and the request content headers collection for all well-known header names. And custom headers
// are only allowed in the request headers collection and not in the request content headers collection.
// 拷贝 Head 逻辑
}
request.Headers.TransferEncodingChunked = SendChunked;
_sendRequestTask = async ?
client.SendAsync(request, _allowReadStreamBuffering ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, _sendRequestCts!.Token) :
Task.FromResult(client.Send(request, _allowReadStreamBuffering ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, _sendRequestCts!.Token));
HttpResponseMessage responseMessage = await _sendRequestTask.ConfigureAwait(false);
HttpWebResponse response = new HttpWebResponse(responseMessage, _requestUri, _cookieContainer);
return response;
}
finally
{
if (disposeRequired)
{
client?.Dispose();
}
}
}
可以看到在 HttpWebRequest 底层是通过 HttpClient 来发送网络请求,在如上面代码注释,将 RequestStream 的数据取出作为 ByteArrayContent 进行发送。这是一个很浪费的行为,因为如果能直接使用 HttpClient 进行网络请求,那直接使用 Stream 即可,可以减少一次内存的拷贝和内存占用
也如上面代码,可以看到,完全可以使用 HttpClient 代替 HttpWebRequest 的调用。而且也如上面代码,可以看到 HttpWebRequest 是将请求存放在 _requestStream 字段,天然就不支持复用,从性能和 API 设计,都不如 HttpClient 好用
可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 7a8217d8c6f6915360f1e25b06f3166c955b8e0e
以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
获取代码之后,进入 BujeardalljelKaifeljaynaba 文件夹
那此内存大量占用问题可以如何解决呢?十分简单,换成 HttpClient 即可
原本 HttpWebRequest 底层就是调用 HttpClient 实现发送网络请求,由因为 HttpWebRequest 的 API 限制,导致了只能将文件的数据先全部读取到内存,再进行发送。如果换成 HttpClient 的话,扔一个 StreamContent 进去即可
上传大文件的时候,还有另外一个坑,那就是上传超时的问题。在 dotnet 6 改了行为,原本的 HttpWebRequest 是分为两个阶段,一个是建立连接的超时判断,另一个是获取响应阶段,在建立连接和获取响应中间的上传数据是不会有超时影响的。但是在 dotnet 6 采用了 HttpClient 作为底层,默认的超时时间是包含整个网络请求活动,也就是建立连接到上传数据完成这个时间不能超时。这个坑将会影响到原本在 .NET Framework 能跑的好好的逻辑,升级到 dotnet 6 将会在上传文件时抛出超时异常。解决方法请看 dotnet 6 使用 HttpClient 的超时机制
dotnet 6 使用 HttpWebRequest 进行 POST 文件将占用大量内存的更多相关文章
- 记一次使用tika解析文件文本导致的内存溢出问题
背景 笔者曾供职于某信息安全公司,接到过一个需求,提取文档中的文本以供后续分析.tika是apache开源的解析文档内容的组件,应用十分广泛.tika几乎支持你能想到的所有文档格式,docx , pp ...
- 关于HttpWebRequest上传文件
我们web 操作离不开 http请求响应 HttpWebRequest上传文件也是一样的道理 下面码一些代码: private void UploadFile(string strRequestUri ...
- HTTPWebrequest上传文件--Upload files with HTTPWebrequest (multipart/form-data)
使用HTTPWebrequest上传文件遇到问题,可以参考Upload files with HTTPWebrequest (multipart/form-data)来解决 https://stack ...
- asp dotnet core 支持客户端上传文件
本文告诉大家如何在 asp dotnet core 支持客户端上传文件 新建一个 asp dotnet core 程序,创建一个新的类,用于给客户端上传文件的信息 public class Kanaj ...
- c#+handle.exe实现升级程序在运行时自动解除文件被占用的问题
我公司最近升级程序经常报出更新失败问题,究其原因,原来是更新时,他们可能又打开了正在被更新的文件,导致更新文件时,文件被其它进程占用,无法正常更新而报错,为了解决这个问题,我花了一周时间查询多方资料及 ...
- 解决 SqlServer执行脚本,文件过大,内存溢出问题
原文:解决 SqlServer执行脚本,文件过大,内存溢出问题 执行.sql脚本文件,如果文件较大时,执行会出现内存溢出问题,可用命令替代 cmd 中输入 osql -S 127.0.0.1,8433 ...
- GDI+中发生一般性错误之文件被占用
有多种原因可能导致这个异常出现,比如创建文件的权限不足.文件被占用等. 这里提供一个使用Stream读取图片避免文件被占用的方法. public Image GetImageFromStream(st ...
- PE文件从文件加载到内存,再从内存读取,然后存盘到文件
// mem.cpp : 定义控制台应用程序的入口点. //PE文件从文件加载到内存,再从内存读取,然后存盘到文件 #include "stdafx.h" #include < ...
- hadoop 小文件 挂载 小文件对NameNode的内存消耗 HDFS小文件解决方案 客户端 自身机制 HDFS把块默认复制3次至3个不同节点。
hadoop不支持传统文件系统的挂载,使得流式数据装进hadoop变得复杂. hadoo中,文件只是目录项存在:在文件关闭前,其长度一直显示为0:如果在一段时间内将数据写到文件却没有将其关闭,则若网络 ...
- 【VS开发】内存映射文件进程间共享内存
内存映射文件进程间共享内存 内存映射文件的另一个功能是在进程间共享数据,它提供了不同进程共享内存的一个有效且简单的方法.后面的许多例子都要用到共享内存.共享内存主要是通过映射机制实现的.Windows ...
随机推荐
- WebView库功能完善
目录介绍 01.loadUrl到底做了什么 02.触发加载网页的行为 03.webView重定向怎么办 04.js交互的一点知识分享 05.拦截缓存如何优雅处理 06.关于一些问题和优化 07.关于一 ...
- springboot 配置 OpenFeign 时报错:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; Connection refused: no further information
报错内容如下: 2022-11-18 01:55:18.998 ERROR 22220 --- [nio-8086-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServl ...
- 记录--uniapp开发安卓APP视频通话模块初实践
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 视频通话SDK用的即构的,uniapp插件市场地址 推送用的极光的,uniapp插件市场地址 即构音视频SDK uniapp插件市场的貌似 ...
- 可变形卷积系列(三) Deformable Kernels,创意满满的可变形卷积核 | ICLR 2020
论文提出可变形卷积核(DK)来自适应有效感受域,每次进行卷积操作时都从原卷积中采样出新卷积,是一种新颖的可变形卷积的形式,从实验来看,是之前方法的一种有力的补充. 来源:晓飞的算法工程笔记 公众号 ...
- KingbaseES V8R6 逻辑恢复到新的 schema
前言 本文介绍一下KingbaseES V8R6版本中逻辑恢复时,将原有的对象恢复到新的schema. sys_restore命令中如果只加入了-g(原schema) -G(新schema)参数 那么 ...
- 探秘Kubernetes:在本地环境中玩转容器技术
在云计算时代,Kubernetes 已成为云原生技术的真正基石.它是应用程序容器的编排动力源,可跨多个集群自动部署.扩展和运行容器.Kubernetes 不仅仅是一个流行词,它还是一种模式转变,是现代 ...
- 【已解决】git push send-pack: unexpected disconnect while reading sideband packet
解决办法:修改缓存大小 打开项目所在路径下的git目录 找到config文件,用记事本打开编辑. 添加如下内容并保存即可 [http] postBuffer = 1048576000
- 才储性格测试(INTJ 专家型——追求能力与独立)
INTJ 专家型--追求能力与独立 一.你的荣格理论图形 二.基本描述 才储分析:您的性格类型倾向为" INTJ "(内向 直觉 思维 判断 倾向度: I60 N56 T74 J5 ...
- 花式栈溢出 CTFshowpwn88
花式栈溢出 在这之前确实对这方面了解很少,一般这种花式栈溢出不仅仅要求你能发现漏洞,最主要的是你要有随机应变的能力 这个题是一个64位的题目看一下保护 canary 和 nx保护都开了,我们用ida打 ...
- #树状数组,哈希#洛谷 6687 论如何玩转 Excel 表格
题目 分析 首先一列的数不会发生变化,只是交换列, 并且交换列的时候奇数列变成偶数列取反, 偶数列变成奇数列取反,考虑直接将偶数列全部取反, 那只需要交换列就可以了,奇数列交换到偶数列会取反, 奇数列 ...