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
  • JWT(JSON Web Token)是一種用於在網路應用之間傳遞信息的開放標準(RFC 7519)。它使用 JSON 對象在安全可靠的方式下傳遞信息,通常用於身份驗證和信息交換。 在Web API中,JWT通常用於對用戶進行身份驗證和授權。當用戶登錄成功後,伺服器會生成一個Token並返回給客戶端 ...
  • 老周在幾個世紀前曾寫過樹莓派相關的 iOT 水文,之所以沒寫 Nano Framework 相關的內容,是因為那時候這貨還不成熟,可玩性不高。不過,這貨現在已經相對完善,老周都把它用在項目上了——第一個是自製的智能插座,這個某寶上50多塊可以買到,搜“esp32 插座”就能找到。一種是 86 型盒子 ...
  • 引言 上一篇我們創建了一個Sample.Api項目和Sample.Repository,並且帶大家熟悉了一下Moq的概念,這一章我們來實戰一下在xUnit項目使用依賴註入。 Xunit.DependencyInjection Xunit.DependencyInjection 是一個用於 xUnit ...
  • 在 Avalonia 中,樣式是定義控制項外觀的一種方式,而控制項主題則是一組樣式和資源,用於定義應用程式的整體外觀和感覺。本文將深入探討這些概念,並提供示例代碼以幫助您更好地理解它們。 樣式是什麼? 樣式是一組屬性,用於定義控制項的外觀。它們可以包括背景色、邊框、字體樣式等。在 Avalonia 中,樣 ...
  • 在處理大型Excel工作簿時,有時候我們需要在工作表中凍結窗格,這樣可以在滾動查看數據的同時保持某些行或列固定不動。凍結窗格可以幫助我們更容易地導航和理解複雜的數據集。相反,當你不需要凍結窗格時,你可能需要解凍它們以獲得完整的視野。 下麵將介紹如何使用免費.NET庫通過C#實現凍結Excel視窗以鎖 ...
  • .NET 部署 IIS 的簡單步驟一: 下載 dotnet-hosting-x.y.z-win.exe ,下載地址:.NET Downloads (Linux, macOS, and Windows) (microsoft.com) .NET 部署 IIS 的簡單步驟二: 選擇對應的版本,點擊進入詳 ...
  • 拓展閱讀 資料庫設計工具-08-概覽 資料庫設計工具-08-powerdesigner 資料庫設計工具-09-mysql workbench 資料庫設計工具-10-dbdesign 資料庫設計工具-11-dbeaver 資料庫設計工具-12-pgmodeler 資料庫設計工具-13-erdplus ...
  • 初識STL STL,(Standard Template Library),即"標準模板庫",由惠普實驗室開發,STL中提供了非常多對信息學奧賽很有用的東西。 vector vetor是STL中的一個容器,可以看作一個不定長的數組,其基本形式為: vector<數據類型> 名字; 如: vector ...
  • 前言 最近自己做了個 Falsk 小項目,在部署上伺服器的時候,發現雖然不乏相關教程,但大多都是將自己項目代碼複製出來,不講核心邏輯,不太簡潔,於是將自己部署的經驗寫成內容分享出來。 uWSGI 簡介 uWSGI: 一種實現了多種協議(包括 uwsgi、http)並能提供伺服器搭建功能的 Pytho ...
  • 1 文本Embedding 將整個文本轉化為實數向量的技術。 Embedding優點是可將離散的詞語或句子轉化為連續的向量,就可用數學方法來處理詞語或句子,捕捉到文本的語義信息,文本和文本的關係信息。 ◉ 優質的Embedding通常會讓語義相似的文本在空間中彼此接近 ◉ 優質的Embedding相 ...