我有用户给我报告一个内存不足的问题,经过了调查,找到了依然是使用已经被标记过时的 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. 记录--微信调用jssdk全流程详解

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 微信调用jssdk全流程详解 系统框架使用的是前后端分离,前端使用vant,后端是springboot 一.网页授权的时序图 二.公众号配 ...

  2. 记录--vue.config.js 的完整配置(超详细)!

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间,对部门的个别项目进行Vue3.0+ts框架的迁移,刚开始研究的时候也是踩坑特别多,尤其我们的项目还有些特殊的webpack配置, ...

  3. kingbaseES sql 优化技巧汇总

    1.整体思路 针对业务系统的出现的慢sql 我们的优化步骤大概分为以下几步 1.识别高负载语句 2.收集性能相关的数据 3.确定性能问题产生的原因 4.实施优化手段 下面我们针对这几个步骤展开进行讲解 ...

  4. java实战字符串1:给定两个字符串 s 和 t,判断他们的编辑距离是否为 1。

    题目描述给定两个字符串 s 和 t,判断他们的间距是否为 1.(满足以下三个条件) 往 s 中插入一个字符得到 t从 s 中删除一个字符得到 t在 s 中替换一个字符得到 t 例1 输入: ab ac ...

  5. Java面试题【4】

    28)Java 栈和堆的区别 1 栈:为编译器自动分配和释放,如函数参数.局部变量.临时变量等等 2 堆:为成员分配和释放,由程序员自己申请.自己释放.否则发生内存泄露.典型为使用new申请的堆内容. ...

  6. Scala打印输出

    1 package com.atguigu.chapter02 2 object TestCharType { 3 def main(args: Array[String]): Unit = { 4 ...

  7. Make It Equal 题解

    Problem Link 简要题意 翻译很清楚. 思路 提供一种简单直接的思路. 可以发现最多会操作 \(n\) 次. 那么就可以每次直接枚举切的高度 \(h\),检查更改是否超过 \(k\),之后暴 ...

  8. HTTP编码和解码与基本认证

    咱们在计算机屏幕上看到的是实体化的文字,而在计算机存储介质中存放的实际是二进制的比特流.那么在这二者之间的转换规则就须要一个统一的标准,不然把计算机上文档就乱码了:因而为了实现转换标准,各类字符集标准 ...

  9. #线性筛,质数#LOJ 6165 一道水题

    题目 \((lcm_{i=1}^ni)\bmod 10^8+7,n\leq 10^8\) 分析 考虑对于某个质数\(p\),在\(n\)范围内做出的最大贡献为\(p^k(p^k\leq n)\), 线 ...

  10. SQL HAVING 子句详解:在 GROUP BY 中更灵活的条件筛选

    SQL HAVING子句 HAVING子句被添加到SQL中,因为WHERE关键字不能与聚合函数一起使用. HAVING语法 SELECT column_name(s) FROM table_name ...