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
  • 用例演示 - 創建實體 本節將演示一些示例用例並討論可選場景。 創建實體 從實體/聚合根類創建對象是實體生命周期的第一步。聚合/聚合根規則和最佳實踐部分 建議為Entity類創建一個主構造函數,以保證創建一個有效的實體。因此,無論何時我們需要創建實體的實例,我們都應該使用那個構造函數 參見下麵的問題 ...
  • 領域邏輯 & 應用邏輯 如前所述,領域驅動設計中的業務邏輯分為兩部分(層):領域邏輯和應用邏輯: 領域邏輯由系統的核心領域規則組成,應用邏輯實現應用特定的用例 雖然定義很明確,但實現起來可能並不容易。您可能無法決定哪些代碼應該位於應用程式層,哪些代碼應該位於領域層。本節試圖解釋其中的差異 多個應用程 ...
  • 表弟大學快畢業了,學了一個學期Python居然還不會寫學生管理系統,真的給我丟臉啊,教他又不肯學,還讓我直接給他寫,我真想兩巴掌上去,最終還是寫了給他,誰讓他是我表弟呢,關鍵時候還是得幫他一把! 寫完了放在那也是放著,所以今天分享給大家吧! 話不多說,咱們直接開始吧! 代碼解析 一、登錄頁面 1、定 ...
  • Zookeeper3.7源碼剖析 能力目標 掌握Zookeeper中Session的管理機制 能基於Client進行Debug測試Session創建/刷新操作 能搭建Zookeeper集群源碼配置 掌握集群環境下Leader選舉啟動過程 能說出Zookeeper選舉過程中的概念 能說出Zookeep ...
  • 前言 今天給大家分享一下我自己寫的筆記,純純的都是乾貨,關於字好像也能看。這是我學python整理出來的一些資料,希望對大家 有用。想要更多的資料那就的給一個關註了… python學習交流Q群:903971231### #導入Counter from collections import Count ...
  • Hi,大家好,我是Mic 一個工作5年的粉絲找到我。 他說: “Mic老師,你要是能回答出這個問題,我就佩服你” 我當場就懵了,現在打賭都這麼隨意了嗎? 我問他問題是什麼,他說“Kafka如何避免重覆消費的問題!” 下麵看看普通人和高手的回答! 普通人: Kafka怎麼避免重覆消費就是我們可以通過 ...
  • 前言 Steam是由美國電子游戲商Valve於2003年9月12日推出的數字發行平臺,被認為是電腦游戲界最大的數位發行平臺之一,Steam平臺是全球最大的綜合性數字發行平臺之一。玩家可以在該平臺購買、下載、討論、上傳和分享游戲和軟體。 而每周的steam會開啟了一輪特惠,可以讓游戲打折,而玩家就會 ...
  • 本篇內容將在上一篇已有的內容基礎上,進一步的聊一下項目中使用JPA的一些高階複雜場景的實踐指導,覆蓋了主要核心的JPA使用場景,可以讓你在需求開發的時候對JPA的使用更加的游刃有餘。 ...
  • 1.路徑處理 1.找模塊:sys.path import sys print(sys.path) - 1.理解 - 1.是python去查找包或模塊 - 2.項目開始根目錄,python內置的目錄 - 3.雖然說python的安裝目錄下也可以存放我們寫的模塊,但是不建議(太多了,不大好找) - 4. ...
  • Go 語言入門練手項目系列 01 基於命令行的圖書的增刪查改 02 文件管理 持續更新中... > 本文來自博客園,作者:Arway,轉載請註明原文鏈接:https://www.cnblogs.com/cenjw/p/gobeginner-proj-bookstore-cli.html 介紹 這是一 ...