我有用户给我报告一个内存不足的问题,经过了调查,找到了依然是使用已经被标记过时的 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 好用

本文测试代码放在githubgitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 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 文件将占用大量内存的更多相关文章

  1. 记一次使用tika解析文件文本导致的内存溢出问题

    背景 笔者曾供职于某信息安全公司,接到过一个需求,提取文档中的文本以供后续分析.tika是apache开源的解析文档内容的组件,应用十分广泛.tika几乎支持你能想到的所有文档格式,docx , pp ...

  2. 关于HttpWebRequest上传文件

    我们web 操作离不开 http请求响应 HttpWebRequest上传文件也是一样的道理 下面码一些代码: private void UploadFile(string strRequestUri ...

  3. HTTPWebrequest上传文件--Upload files with HTTPWebrequest (multipart/form-data)

    使用HTTPWebrequest上传文件遇到问题,可以参考Upload files with HTTPWebrequest (multipart/form-data)来解决 https://stack ...

  4. asp dotnet core 支持客户端上传文件

    本文告诉大家如何在 asp dotnet core 支持客户端上传文件 新建一个 asp dotnet core 程序,创建一个新的类,用于给客户端上传文件的信息 public class Kanaj ...

  5. c#+handle.exe实现升级程序在运行时自动解除文件被占用的问题

    我公司最近升级程序经常报出更新失败问题,究其原因,原来是更新时,他们可能又打开了正在被更新的文件,导致更新文件时,文件被其它进程占用,无法正常更新而报错,为了解决这个问题,我花了一周时间查询多方资料及 ...

  6. 解决 SqlServer执行脚本,文件过大,内存溢出问题

    原文:解决 SqlServer执行脚本,文件过大,内存溢出问题 执行.sql脚本文件,如果文件较大时,执行会出现内存溢出问题,可用命令替代 cmd 中输入 osql -S 127.0.0.1,8433 ...

  7. GDI+中发生一般性错误之文件被占用

    有多种原因可能导致这个异常出现,比如创建文件的权限不足.文件被占用等. 这里提供一个使用Stream读取图片避免文件被占用的方法. public Image GetImageFromStream(st ...

  8. PE文件从文件加载到内存,再从内存读取,然后存盘到文件

    // mem.cpp : 定义控制台应用程序的入口点. //PE文件从文件加载到内存,再从内存读取,然后存盘到文件 #include "stdafx.h" #include < ...

  9. hadoop 小文件 挂载 小文件对NameNode的内存消耗 HDFS小文件解决方案 客户端 自身机制 HDFS把块默认复制3次至3个不同节点。

    hadoop不支持传统文件系统的挂载,使得流式数据装进hadoop变得复杂. hadoo中,文件只是目录项存在:在文件关闭前,其长度一直显示为0:如果在一段时间内将数据写到文件却没有将其关闭,则若网络 ...

  10. 【VS开发】内存映射文件进程间共享内存

    内存映射文件进程间共享内存 内存映射文件的另一个功能是在进程间共享数据,它提供了不同进程共享内存的一个有效且简单的方法.后面的许多例子都要用到共享内存.共享内存主要是通过映射机制实现的.Windows ...

随机推荐

  1. TP6框架--EasyAdmin学习笔记:定义路由

    这是我写的学习EasyAdmin的第二章,这一章我给大家分享下如何定义一条路由 正常的tp6定义路由方法如下: /route/admins/app.php 文件内容 //路由变量自定义 Route:: ...

  2. 作用域&变量提升&闭包题目及内容解答

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 1. 代码输出结果 (function(){ var x = y = 1; })(); var z; console.log(y); // ...

  3. VUE3.0---Pinia学习记录

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 Pinia优势 Pinia是一个全新的Vue状态管理库,是Vuex的代替者,尤雨溪强势推荐 Vue2 和 Vue3 都能支持 抛弃传统的 ...

  4. 《HelloGitHub》第 96 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...

  5. 【问题解决1】fatal error: X11/XXXX.h: No such file or directory

    问题现象 编译鸿蒙代码时,报如下类似的错误: 错误1: 错误2: 解决方法 step 1:安装依赖文件 sudo apt-get install apt-file sudo apt-file upda ...

  6. archlinux 时间,时钟设置与详解,时区对应的时间不正确

    参照 https://wiki.archlinux.org/title/System_time 1.使用命令查看时间 timedatectl 显示类似 Local time: Wed 2024-01- ...

  7. #离线#洛谷 5358 [SDOI2019]快速查询

    题目传送门 分析 由于询问次数很多,只能离线去做,考虑全局的操作都可以打标记. 对于单点的操作,实际上的数为 \(x'=x*Mul+Add\) 由于这两个标记单独撤销比较困难, 不妨开一个数组直接维护 ...

  8. 响应式系统reactive system初探

    目录 初识响应式系统 什么是响应式系统 响应式系统的四大特点 及时响应性(Responsive) 恢复性(Resilient) 有弹性(Elastic) 消息驱动(Message Driven) 总结 ...

  9. OpenHarmony系统能力SystemCapability使用指南

    一.概述 1.系统能力与 API SysCap,全称SystemCapability,即系统能力,指操作系统中每一个相对独立的特性,如蓝牙,WIFI,NFC,摄像头等,都是系统能力之一.每个系统能力对 ...

  10. 从模型到部署,教你如何用Python构建机器学习API服务

    本文分享自华为云社区<Python构建机器学习API服务从模型到部署的完整指南>,作者: 柠檬味拥抱. 在当今数据驱动的世界中,机器学习模型在解决各种问题中扮演着重要角色.然而,将这些模型 ...