dotnet 6 使用 HttpWebRequest 進行 POST 文件將占用大量記憶體

来源:https://www.cnblogs.com/lindexi/archive/2022/06/21/16395565.html
-Advertisement-
Play Games

一:背景 1. 一個有趣的話題 最近在看 硬體異常 相關知識,發現一個有意思的空引用異常問題,拿出來和大家分享一下,為了方便講述,先上一段有問題的代碼。 namespace ConsoleApp2 { internal class Program { static Person person = n ...


我有用戶給我報告一個記憶體不足的問題,經過了調查,找到了依然是使用已經被標記過時的 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 的超時機制

博客園博客只做備份,博客發佈就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

知識共用許可協議
本作品採用知識共用署名-非商業性使用-相同方式共用 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發佈,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請與我[聯繫](mailto:[email protected])。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Spring5——JdbcTemplate筆記 概念 template,翻譯過來是模板的意思,顧名思義,JdbcTemplate就是一個JDBC的模板,它對JDBC進行了封裝,可以很方便地實現對資料庫的CRUD(增、刪、改、查)的操作。 JdbcTemplate準備工作 引入相關的依賴 druid- ...
  • 給大家推薦八個非常實用的Python案例,希望大家看過後能夠有所收穫! 1、合併兩個字典 Python3.5之後,合併字典變得容易起來,我們可以通過**符號解壓字典,並將多個字典傳入{}中,實現合併。 def Merge(dict1,dict2): res = {**dict1,**dict2} r ...
  • 1、Collections sort(List list) 自然升序排序 reverse(List<?> list) 集合反轉 binarySearch(List<? extends Comparable<? super T>> list, T key) 二分查找(要求集合有序) addAll(Co ...
  • 前言 利用selenium在做自動化測試的時候,經常會用到數據來做批量測試,常用的方式有讀取txt文件,xml文件,csv文件以及excel文 件幾種。 使用 excel 來做數據管理時,需要利用 xlrd、xlwt 開源包來讀寫 excel。 1、安裝xlrd、xlwt pip install x ...
  • 大佬的理解-> Java多線程(三)--synchronized關鍵字詳情 大佬的理解-> Java多線程(三)--synchronized關鍵字續 1、問題引入 買票問題 1.1 通過繼承Thread買票 繼承Thread買票案例 /* 模擬網路購票,多線程資源共用問題,繼承Thread方式; 結 ...
  • 控制結構 順序 程式從上到下逐行地執行,中間沒有任何判斷和跳轉。 順序控制舉例和註意事項 Java中定義成員變數時採用合法的前向引用。如: public class Test{ ​ int num1 = 12; ​ int num2 = num1 + 2; } 錯誤形式: public class ...
  • 前言 當我們開始學習Python時,我們會養成一些不良編碼習慣,而更可怕的是我們連自己也不知道。 我們學習變成的過程中,大概有會這樣的經歷: 寫的代碼只能完成了一次工作,但後來再執行就會報錯或者失敗,令人感到懊惱, 或者偶然發現一個內置函數可以讓你的工作更輕鬆時,瞬間豁然開朗。 我們中的大多數人仍然 ...
  • 1. 前言 距離上次發《MAUI初體驗:爽》一文已經過去2個月了,本計劃是下半年或者明年再研究MAUI的,現在計劃提前啦,因為我覺得MAUI Blazor挺有意思的:在Android、iOS、macOS、Windows之間共用UI,一處UI增加或者修改,就能得到一致的UI體驗。 看看這篇文章《Bla ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...