.Net Core HttpClient處理響應壓縮

前言

    在上篇文章[ASP.NET Core中的響應壓縮]中咱們談到了在ASP.NET Core服務端處理關於響應壓縮的請求,服務端的主要工做就是根據Content-Encoding頭信息判斷採用哪一種方式壓縮並返回。以前在羣裏有人問道過,如今的網絡帶寬這麼高了還有必要在服務端針對請求進行壓縮嗎?確實,現在分佈式和負載均衡技術這麼成熟,不少須要處理高併發大數據的場景均可以經過增長服務器節點來進行。可是,在資源受限的狀況下,或者是還不必爲了某一個點去增長新的服務器節點的時候,咱們仍是要採用一些程序自己的常規處理手段來進行處理。筆者我的認爲響應壓縮的使用場景是這樣的,在帶寬壓力比較緊張的狀況,且CPU資源比較充足的狀況下,使用響應壓縮總體效果仍是比較明顯的。
    有壓縮就有解壓,而解壓的工做就是在請求客戶端處理的。好比瀏覽器,這是咱們最經常使用的Http客戶端,許多瀏覽器都是默認在咱們發出請求的時候(好比咱們瀏覽網頁的時候)在Request Head中添加Content-Encoding,而後根據響應信息處理相關解壓。這些都源於瀏覽器已經內置了關於請求壓縮和解壓的機制。相似的還有許多,好比經常使用的代理抓包工具Filder也是內置這種機制的。只不過須要手動去處理,但實現方式都是同樣的。有時候咱們在本身寫程序的過程當中也須要使用這種機制,在傳統的.Net HttpWebRequest類庫中,並無這種機制,後來版本中加入了HttpClient,有自帶的機制能夠處理這種操做,.Net Core做爲後起之秀直接將HttpClient扶正,而且在此基礎上改良了HttpClientFactory,接下來咱們就來探究一下在.Net Core中使用HttpClient處理響應壓縮的機制。html

使用方式

首先咱們來看一下直接在HttpClient中如何處理響應壓縮git

//自定義HttpClientHandler實例
HttpClientHandler httpClientHandler = new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip
};
//使用傳遞自定義HttpClientHandler實例的構造函數
using (HttpClient client = new HttpClient(httpClientHandler))
{
    var response = await client.GetAsync($"http://MyDemo/Home/GetPerson?userId={userId}");
}

這個操做仍是很是簡單的,咱們操做的並非HttpClient的屬性而是HttpClientHandler中的屬性,咱們在以前的文章[.NET Core HttpClient源碼探究]中曾探討過,HttpClient的本質其實就是HttpMessageHandler,而HttpClient真正使用到的是HttpMessageHandler最重要的一個子類HttpClientHandler,全部的請求操做都是經過HttpMessageHandler進行的。咱們能夠看到AutomaticDecompression接受的是DecompressionMethods枚舉,既然是枚舉就說明包含了不止一個值,接下來咱們查看DecompressionMethods中的源碼github

[Flags]
public enum DecompressionMethods
{
    // 使用全部壓縮解壓縮算法。
    All = -1,
    // 不使用解壓
    None = 0x0,
    // 使用gzip解壓算法
    GZip = 0x1,
    // 使用deflate解壓算法
    Deflate = 0x2,
    // 使用Brotli解壓算法
    Brotli = 0x4
}

該枚舉默認都是針對經常使用輸出解壓算法,接下來咱們看一下在HttpClientFactory中如何處理響應壓縮。在以前的文章[.NET Core HttpClientFactory+Consul實現服務發現]中咱們曾探討過HttpClientFactory的大體工做方式默認PrimaryHandler傳遞的就是HttpClientHandler實例,並且在咱們註冊HttpClientFactory的時候是能夠經過ConfigurePrimaryHttpMessageHandler自定義PrimaryHandler的默認值,接下來咱們具體代碼實現算法

services.AddHttpClient("mydemo", c =>
{
    c.BaseAddress = new Uri("http://MyDemo/");
}).ConfigurePrimaryHttpMessageHandler(provider=> new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip
});

其實在註冊HttpClientFactory的時候還可使用自定義的HttpClient,具體的使用方式是這樣的瀏覽器

services.AddHttpClient("mydemo", c =>
{
    c.BaseAddress = new Uri("http://MyDemo/");
}).ConfigureHttpClient(provider => new HttpClient(new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip
}));

HttpClient確實幫咱們作了好多事情,只須要簡單的配置一下就開啓了針對響應壓縮的處理。這更勾起了咱們對HttpClient的探討,接下來咱們就經過源碼的方式查看它是如何發起可響應壓縮請求,並解壓響應結果的。服務器

源碼探究

經過上面的使用方式咱們得知,不管使用哪一種形式,最終都是針對HttpClientHandler作配置操做,接下來咱們查看HttpClientHandler類[點擊查看源碼👈]中AutomaticDecompression屬性的代碼網絡

public DecompressionMethods AutomaticDecompression
{
    get => _underlyingHandler.AutomaticDecompression;
    set => _underlyingHandler.AutomaticDecompression = value;
}

它自己的值操做來自_underlyingHandler這個對象,也就是說讀取和設置都是在操做_underlyingHandler.AutomaticDecompression,咱們查找到_underlyingHandler對象的聲明位置併發

private readonly SocketsHttpHandler _underlyingHandler;

這裏說明一下,HttpClient的實質工做類是HttpClientHandler,而HttpClientHandler真正發起請求是依靠的SocketsHttpHandler這個類,也就是說SocketsHttpHandler是最原始發起請求的類。HttpClientHandler本質仍是經過SocketsHttpHandler發起的Http請求,接下來咱們就查看SocketsHttpHandler類[點擊查看源碼👈]是如何處理AutomaticDecompression這個屬性的負載均衡

public DecompressionMethods AutomaticDecompression
{
    get => _settings._automaticDecompression;
    set
    {
        CheckDisposedOrStarted();
        _settings._automaticDecompression = value;
    }
}

這裏的_settings再也不是具體的功能類,而是用於初始化或者保存SocketsHttpHandler的部分屬性值的配置類async

private readonly HttpConnectionSettings _settings = new HttpConnectionSettings();

這裏咱們不在分析SocketsHttpHandler出處理響應壓縮以外的其餘代碼,因此具體就再也不看這些了,直接查找_settings._automaticDecompression屬性引用的地方,最終找到了這段代碼

if (settings._automaticDecompression != DecompressionMethods.None)
{
    handler = new DecompressionHandler(settings._automaticDecompression, handler);
}

這裏就比較清晰了,真正處理請求響應壓縮相關的都是在DecompressionHandler中。正如咱們以前所說的,HttpClient真正的工做方式就是一些實現自HttpMessageHandler的子類在工做,它把不一樣功能的實現模塊都封裝成了具體的Handler中。當你須要使用哪一個模塊的功能,直接使用對應的Handler操做類去發送處理請求便可。這種設計思路在ASP.NET Core中體現的也是淋漓盡致,ASP.NET Core採用的是構建不一樣終結點去處理和輸出請求。經過這些咱們能夠得知DecompressionHandler纔是今天的主題,接下來咱們就來查看DecompressionHandler類的源碼[點擊查看源碼👈]就不粘貼所有源碼了,咱們先來看最核心的SendAsync方法,這個方法是發送請求的執行方法

internal override async ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
{
    //判斷是不是GZIP壓縮請求,若是是則添加請求頭Accept-Encoding頭爲gzip
    if (GZipEnabled && !request.Headers.AcceptEncoding.Contains(s_gzipHeaderValue))
    {
        request.Headers.AcceptEncoding.Add(s_gzipHeaderValue);
    }
    //判斷是不是Deflate壓縮請求,若是是則添加請求頭Accept-Encoding頭爲deflate
    if (DeflateEnabled && !request.Headers.AcceptEncoding.Contains(s_deflateHeaderValue))
    {
        request.Headers.AcceptEncoding.Add(s_deflateHeaderValue);
    }
    //判斷是不是Brotli壓縮請求,若是是則添加請求頭Accept-Encoding頭爲brotli
    if (BrotliEnabled && !request.Headers.AcceptEncoding.Contains(s_brotliHeaderValue))
    {
        request.Headers.AcceptEncoding.Add(s_brotliHeaderValue);
    }
    //發送請求
    HttpResponseMessage response = await _innerHandler.SendAsync(request, async, cancellationToken).ConfigureAwait(false);

    Debug.Assert(response.Content != null);
    //獲取返回的Content-Encoding輸出頭信息
    ICollection<string> contentEncodings = response.Content.Headers.ContentEncoding;
    if (contentEncodings.Count > 0)
    {
        string? last = null;
        //獲取最後一個值
        foreach (string encoding in contentEncodings)
        {
            last = encoding;
        }
        //根據響應頭判斷服務端採用的是否爲gzip壓縮
        if (GZipEnabled && last == Gzip)
        {
            //使用gzip解壓算法解壓返回內容,並重新賦值到response.Content
            response.Content = new GZipDecompressedContent(response.Content);
        }
        //根據響應頭判斷服務端採用的是否爲deflate壓縮
        else if (DeflateEnabled && last == Deflate)
        {
            //使用deflate解壓算法解壓返回內容,並重新賦值到response.Content
            response.Content = new DeflateDecompressedContent(response.Content);
        }
        //根據響應頭判斷服務端採用的是否爲brotli壓縮
        else if (BrotliEnabled && last == Brotli)
        {
            //使用brotli解壓算法解壓返回內容,並重新賦值到response.Content
            response.Content = new BrotliDecompressedContent(response.Content);
        }
    }
    return response;
}

經過上面的邏輯咱們能夠看到GZipEnabled、DeflateEnabled、BrotliEnabled三個bool類型的變量,中三個變量決定了採用哪一種請求壓縮方式,主要實現方式是

internal bool GZipEnabled => (_decompressionMethods & DecompressionMethods.GZip) != 0;
internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0;
internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0;

主要就是根據咱們配置的DecompressionMethods枚舉值判斷想獲取哪一種方式的壓縮結果,解壓的實現邏輯都封裝在GZipDecompressedContent、DeflateDecompressedContent、BrotliDecompressedContent中,咱們看一下他們的具體的代碼

private sealed class GZipDecompressedContent : DecompressedContent
{
        public GZipDecompressedContent(HttpContent originalContent)
            : base(originalContent)
        { }
        //使用GZipStream類對返回的流進行解壓
        protected override Stream GetDecompressedStream(Stream originalStream) =>
            new GZipStream(originalStream, CompressionMode.Decompress);
    }

    private sealed class DeflateDecompressedContent : DecompressedContent
    {
        public DeflateDecompressedContent(HttpContent originalContent)
            : base(originalContent)
        { }
        //使用DeflateStream類對返回的流進行解壓
        protected override Stream GetDecompressedStream(Stream originalStream) =>
            new DeflateStream(originalStream, CompressionMode.Decompress);
    }

    private sealed class BrotliDecompressedContent : DecompressedContent
    {
        public BrotliDecompressedContent(HttpContent originalContent) :
            base(originalContent)
        { }
        //使用BrotliStream類對返回的流進行解壓
        protected override Stream GetDecompressedStream(Stream originalStream) =>
            new BrotliStream(originalStream, CompressionMode.Decompress);
    }
}

    其主要的工做方式就是使用對應壓縮算法的解壓方法獲得原始信息。簡單總結一下,HttpClient關於壓縮相關的處理機制是,首先根據你配置的DecompressionMethods判斷你想使用那種壓縮算法。而後匹配到對應的壓縮算法後添加Accept-Encoding請求頭爲你指望的壓縮算法。最後根據響應結果獲取Content-Encoding輸出頭信息,判斷服務端採用的是哪一種壓縮算法,並採用對應的解壓方法解壓獲取原始數據。

總結

    經過本次探討HttpClient關於響應壓縮的處理咱們能夠了解到,HttpClient不管從設計上仍是實現方式上都有很是高的靈活性和擴展性,這也是爲何到了.Net Core上官方只推薦使用HttpClient一種Http請求方式。因爲使用比較簡單,實現方式比較清晰,這裏就不過多拗述。主要是是想告訴你們HttpClient默承認以直接處理響應壓縮,而不是和以前咱們使用HttpWebRequest的時候還須要手動編碼的方式去實現。

👇歡迎掃碼關注個人公衆號👇
相關文章
相關標籤/搜索