.Net Core HttpClient處理響應壓縮

語言: CN / TW / HK

前言

在上篇文章[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處理響應壓縮的機制。

使用方式

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

//自定義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中的原始碼

[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類[點選檢視原始碼:point_left:]中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類[ 點選檢視原始碼:point_left: ]是如何處理AutomaticDecompression這個屬性的

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

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

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類的原始碼[ 點選檢視原始碼:point_left: ]就不貼上全部原始碼了,我們先來看最核心的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的時候還需要手動編碼的方式去實現。

:point_down: 歡迎掃碼關注我的公眾號 :point_down: