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
  • 前言 當別人做大數據用Java、Python的時候,我使用.NET做大數據、數據挖掘,這確實是值得一說的事。 寫的並不全面,但都是實際工作中的內容。 .NET在大數據項目中,可以做什麼? 寫腳本(使用控制台程式+頂級語句) 寫工具(使用Winform) 寫介面、寫服務 使用C#寫代碼的優點是什麼? ...
  • 前言 本文寫給想學C#的朋友,目的是以儘快的速度入門 C#好學嗎? 對於這個問題,我以前的回答是:好學!但仔細想想,不是這麼回事,對於新手來說,C#沒有那麼好學。 反而學Java還要容易一些,學Java Web就行了,就是SpringBoot那一套。 但是C#方向比較多,你是學控制台程式、WebAP ...
  • 某一日晚上上線,測試同學在回歸項目黃金流程時,有一個工單項目介面報JSF序列化錯誤,馬上升級對應的client包版本,編譯部署後錯誤消失。 線上問題是解決了,但是作為程式員要瞭解問題發生的原因和本質。但這都是為什麼呢? ...
  • 本文介紹基於Python語言中TensorFlow的Keras介面,實現深度神經網路回歸的方法。 1 寫在前面 前期一篇文章Python TensorFlow深度學習回歸代碼:DNNRegressor詳細介紹了基於TensorFlow tf.estimator介面的深度學習網路;而在TensorFl ...
  • 前段時間因業務需要完成了一個工作流組件的編碼工作。藉著這個機會跟大家分享一下整個創作過程,希望大家喜歡,組件暫且命名為"easyFlowable"。 接下來的文章我將從什麼是工作流、為什麼要自研這個工作流組件、架構設計三個維度跟大家來做個整體介紹。 ...
  • 1 簡介 我們之前使用了dapr的本地托管模式,但在生產中我們一般使用Kubernetes托管,本文介紹如何在GKE(GCP Kubernetes)安裝dapr。 相關文章: dapr本地托管的服務調用體驗與Java SDK的Spring Boot整合 dapr入門與本地托管模式嘗試 2 安裝GKE ...
  • 摘要:在jvm中有很多的參數可以進行設置,這樣可以讓jvm在各種環境中都能夠高效的運行。絕大部分的參數保持預設即可。 本文分享自華為雲社區《為什麼需要對jvm進行優化,jvm運行參數之標準參數》,作者:共飲一杯無。 我們為什麼要對jvm做優化? 在本地開發環境中我們很少會遇到需要對jvm進行優化的需 ...
  • 背景 我們的業務共使用11台(阿裡雲)伺服器,使用SpringcloudAlibaba構建微服務集群,共計60個微服務,全部註冊在同一個Nacos集群 流量轉發路徑: nginx->spring-gateway->業務微服務 使用的版本如下: spring-boot.version:2.2.5.RE ...
  • 基於php+webuploader的大文件分片上傳,帶進度條,支持斷點續傳(刷新、關閉頁面、重新上傳、網路中斷等情況)。文件上傳前先檢測該文件是否已上傳,如果已上傳提示“文件已存在”,如果未上傳則直接上傳。視頻上傳時會根據設定的參數(分片大小、分片數量)進行上傳,上傳過程中會在目標文件夾中生成一個臨 ...
  • 基於php大文件分片上傳至七牛雲,使用的是七牛雲js-sdk V2版本,引入js文件,配置簡單,可以暫停,暫停後支持斷點續傳(刷新、關閉頁面、重新上傳、網路中斷等情況),可以配置分片大小和分片數量,官方文檔https://developer.qiniu.com/kodo/6889/javascrip ...