ASP.NET Web API:使用GZIP或Deflate壓縮,提升資料傳遞效能40倍

ASP.NET Web API使用GZIP或Deflate壓縮,提升資料傳遞效能40倍

未壓縮資料量

專案有個查詢資料量不小,基本測試資料每次約1.92 MB,透過 ASP.NET Web API 傳遞至 Client 端每次就是 1.92 MB 的網路傳遞量,而且未來上線後,資料量只會越來越大,直覺感到不對勁(壞味道)。

IIS的動態壓縮靜態壓縮對 ASP.NET Web API 是無效的。而且,平常 ASP.NET Web API 的請求,資料負載量也不是很大,那種 KB 級的資料壓縮率不高,平常也就沒特別注意。不過在 MB 級的資料,有無啟用 GZIP 或 Deflate 壓縮的資料量差異,直接說結果,以我實測得到的數據差了40倍

啟用IIS的GZIP或Deflate壓縮 for JSON

這前提是你碰得到 IIS 主機能修改 applicationHost.config,把 mimeType 加入至 <dynamicTypes>,這樣 application/json 回應就會被 IIS 接手並使用 GZIP 壓縮處理。

<httpCompression directory="%TEMP%\iisexpress\IIS Temporary Compressed Files">
    <scheme name="gzip" dll="%IIS_BIN%\gzip.dll" />
    <dynamicTypes>
        <!-- 壓縮 JSON 回應 -->
        <add mimeType="application/json" enabled="true" />
    </dynamicTypes>
</httpCompression>

這是最直接簡易的處理方式。如果碰不到也沒關係,我們有土炮精神。

Compression ActionFilter

ASP.NET Web API 提供完整的擴充點,沒有的東西,那麼就拉起袖子來自己來做一個。

首先思考的問題點是,資訊流進出問題,目前問題在查詢後資料量非常大,也就是查詢取得結果我們想加工處理,很明顯,我們應該使用 ActionFilter 來處理。

/// <summary>
/// Response content compression
/// </summary>
/// <seealso cref="System.Web.Http.Filters.ActionFilterAttribute" />
public class CompressionAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        base.OnActionExecuted(actionExecutedContext);
    }
}

以生命流程來看,取得資料後的加工,應該是在 OnActionExecuted 來處理。

這裡要先回到 HTTP 協定上,在 Web 的世界,如果 Client 與 Server 雙方要進行資料壓縮傳遞,雙方的內容協商中必須包含特定的 Header,Client 端指定 Accept-Encoding,Server 端指定 Content-Encoding。這必須有 HTTP 協定基礎瞭解才能明白後面的實作。

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
    var content = actionExecutedContext.Response.Content;
    var bytes = content?.ReadAsByteArrayAsync().Result;
    if (bytes != null && bytes.Length > 0)
    {
        var acceptEncoding = actionExecutedContext.Request.Headers.AcceptEncoding.Where(x => x.Value == "gzip" || x.Value == "deflate").ToList();
        byte[] zlibbedContent;
        if (acceptEncoding.FirstOrDefault()?.Value == "deflate")
        {
            zlibbedContent = DeflateByte(bytes);
            actionExecutedContext.Response.Content = new ByteArrayContent(zlibbedContent);
            actionExecutedContext.Response.Content.Headers.Add("Content-Encoding", "deflate");
        }
        else
        {
            zlibbedContent = GZipByte(bytes);
            actionExecutedContext.Response.Content = new ByteArrayContent(zlibbedContent);
            actionExecutedContext.Response.Content.Headers.Add("Content-Encoding", "gzip");
        }
    }
    actionExecutedContext.Response.Content.Headers.Add("Content-Type", "application/json");

    base.OnActionExecuted(actionExecutedContext);
}

這裡的實作是不論 Client 是否指定 Accept-Encoding 值都會進行壓縮,預設使用 GZIP 壓縮。如果希望 Client 有指定 Accept-Encoding 才進行壓縮,請自行調整程式碼。

HTTP壓縮常見有 GZIP 與 Deflate 兩種,針對兩種方式提供不同實作方法。

以下使用常見的 DotNetZip 套件來進行實作:

#region DotNetZip
private static byte[] DeflateByte(byte[] data)
{
    using (var output = new MemoryStream())
    {
        using (var compressor = new DeflateStream(
            output, CompressionMode.Compress,
            CompressionLevel.BestSpeed))
        {
            compressor.Write(data, 0, data.Length);
        }
        return output.ToArray();
    }
}

private byte[] GZipByte(byte[] data)
{
    using (var output = new MemoryStream())
    {
        using (var compressor = new GZipStream(
            output, CompressionMode.Compress,
            CompressionLevel.BestSpeed))
        {
            compressor.Write(data, 0, data.Length);
        }
        return output.ToArray();
    }
}
#endregion

這樣就完成我們第一版的 Compression Action Filter。

測試取得的數據:

  • 原始:1.92 MB
  • GZIP:89.7 KB
  • Deflate:89.68 KB

透過第一版 Compression Action Filter 的幫忙,我們整整提升21倍的資料傳遞效率。


  • 讀者:雖然內容不錯,但作者你也喜歡用標頭殺人法來吸流量?
  • 筆者:非也非也。我是個有誠信的人。讓我們往下看。

第一版 Compression Action Filter 採用網路上大家常用的 DotNetZip 套件來實作。不過,我愛 .NET Framework,那麼剛好就記得 .NET Framework 也有支援GZipStream 類別DeflateStream 類別,就順手實作並測試。

/// <summary>
/// GZip the byte.
/// </summary>
/// <param name="data">The data.</param>
/// <returns></returns>
private byte[] GZipByte(byte[] data)
{
    using (var output = new MemoryStream())
    {
        using (var compressor = new GZipStream(output, CompressionMode.Compress))
        {
            compressor.Write(data, 0, data.Length);
            compressor.Close();
        }

        return output.ToArray();
    }
}

/// <summary>
/// Deflate the byte.
/// </summary>
/// <param name="data">The data.</param>
/// <returns></returns>
private byte[] DeflateByte(byte[] data)
{
    using (var output = new MemoryStream())
    {
        using (var compressor = new DeflateStream(output, CompressionMode.Compress))
        {
            compressor.Write(data, 0, data.Length);
            compressor.Close();
        }

        return output.ToArray();
    }
}

就程式碼而言,.NET Framework提供 GZipStreamDeflateStream 程式碼更短也更好寫。

壓縮資料量

有沒有嚇到。

  • 原始:1.92 MB
  • GZIP:50.15 KB
  • Deflate:50.14 KB

第二版 Compression Action Filter 改採用 .NET Framework 的 GZipStream 類別與 DeflateStream 類別來實作,硬是把 DotNetZip 的 89 KB 比下去。整體的資料傳遞效能整整從21倍提升至38.4倍

原始:1.92 MB → DotNetZip:89 KB → .NET Framework:50 KB。

不是外來的和尚比較會唸經,讓我們幫 .NET Framework 按個讚,好棒!


  • 筆者:這樣的數據,讓我取個整數號稱40倍,不過份吧。
  • 讀者:;-)

下集:POST GZIP/Deflate Data to ASP.NET Web API


沒有留言:

張貼留言

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。