POST GZIP/Deflate Data to ASP.NET Web API

POST GZIP/Deflate Data to ASP.NET Web API

前一篇使用GZIP或DEFLATE壓縮,提升資料傳遞效能40倍,我們處理了查詢資料量大時,Web API 在傳遞未經壓縮的資料產生許多不必要的網路流量的問題。現在我們轉換方向,如果是用戶端要傳遞大量資料至 Web API 時,那麼我們要怎麼處理?這比較麻煩,有二個方向要討論,一是 Client 端進行發送請求時,必須先做 GZIP/Deflate 的壓縮,而 Web API 接收到 GZIP/Deflate 壓縮資料後需要解壓縮還原資料內容。

從這個方向來思考,你應該能發現另一件事,就是前一篇我們只專注在 GZIP/Deflate 的壓縮上,並無做任何解壓縮的工作,那是因為瀏覽器本身在協商過程與接收到的 Header 可以判定接收到的是 GZIP/Deflate 的內容,瀏覽器會直接幫我們進行 GZIP/Deflate 的解壓縮工作。

DecompressionHandler

一樣先思考資訊流,與前一篇方向完全相反,這裡是用戶端傳遞資料,是進來的方向,有二種擴充點可以選擇:MessageHandlerAction Filter。這二種擴充點都能做出符合需求解法,但因為進來的是資料,希望越早處理越好,以下選擇使用 MessageHandler 來處理。

/// <summary>
/// Decompression GZIP, Deflate content
/// </summary>
/// <seealso cref="System.Net.Http.DelegatingHandler" />
public class DecompressionHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Content.Headers.ContentEncoding.Any())
        {
            var encodes = request.Content.Headers.ContentEncoding.ToList();
            foreach (var encode in encodes)
            {
                if (encode.Equals("deflate", StringComparison.InvariantCultureIgnoreCase))
                {
                    request.Content = DecompressDeflateContent(request);
                }

                request.Content = DecompressGzipContent(request);
            }
        }

        return base.SendAsync(request, cancellationToken);
    }


    /// <summary>
    /// Decompresses the content of the gzip.
    /// </summary>
    /// <param name="request">The request.</param>
    /// <returns></returns>
    private HttpContent DecompressGzipContent(HttpRequestMessage request)
    {
        MemoryStream outputStream = new MemoryStream();
        var inputStream = request.Content.ReadAsStreamAsync().Result;
        using (var decompressor = new GZipStream(inputStream, CompressionMode.Decompress))
        {
            decompressor.CopyToAsync(outputStream);
        }

        outputStream.Seek(0, SeekOrigin.Begin);

        HttpContent newContent = AddHeadersToNewContent(request, outputStream);
        return newContent;
    }

    /// <summary>
    /// Decompresses the content of the deflate.
    /// </summary>
    /// <param name="request">The request.</param>
    /// <returns></returns>
    private HttpContent DecompressDeflateContent(HttpRequestMessage request)
    {
        MemoryStream outputStream = new MemoryStream();
        var inputStream = request.Content.ReadAsStreamAsync().Result;
        using (var decompressor = new DeflateStream(inputStream, CompressionMode.Decompress))
        {
            decompressor.CopyToAsync(outputStream);
        }

        outputStream.Seek(0, SeekOrigin.Begin);

        HttpContent newContent = AddHeadersToNewContent(request, outputStream);
        return newContent;
    }


    private HttpContent AddHeadersToNewContent(HttpRequestMessage request, MemoryStream outputStream)
    {
        HttpContent requestContent = request.Content;
        HttpContent newContent = new StreamContent(outputStream);

        // 複製原內容所有 Header 到 newContent
        foreach (var header in requestContent.Headers)
        {
            // 更改 content-encoding header 到預設值
            if (header.Key.ToLowerInvariant() == "content-encoding")
            {
                newContent.Headers.Add(header.Key, "identity");
                continue;
            }

            // 更改 content-length header 值為解壓縮後的長度
            if (header.Key.ToLowerInvariant() == "content-length")
            {
                newContent.Headers.Add(header.Key, outputStream.Length.ToString());
                continue;
            }

            newContent.Headers.Add(header.Key, header.Value);
        }

        return newContent;
    }
}

其中的 content-encoding 操作可能是大家可比較看不懂的,可以參考 MDN 文件,以下節錄 identity 說明。

identity:Indicates the identity function (i.e. no compression, nor modification). This token, except if explicitly specified, is always deemed acceptable.

HTTP Client with GZIP/Deflate

這等於前一篇在做的事,現在換用戶端來做,發送前設定應有 Header 與壓縮,然後進行傳送。以下整理我常用的 HttpClient 與 RestSharp 兩個套件的 GZIP/Deflate 設置方式。

HttpClient

HttpClient 類別 是 .NET Framework 內建類別,它簡單好用且全部都是非同步方法,非常合適 RESTful API 的用戶端呼叫使用。

void Main(string[] args)
{
 Task t = MainAsync(args);
 t.Wait();
}

// Define other methods and classes here
static async Task MainAsync(string[] args)
{
 var jsonData = new 
 {
  Name = "ASP.NET Web API",
  Site = "skilltree.my",
  Lecturer = "Bruce Chen"
 };
 var jsonParam = JsonConvert.SerializeObject(jsonData);
 var content = JsonToGZipStream(jsonParam);
 content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
 content.Headers.ContentEncoding.Add("gzip");

 HttpClientHandler handler = new HttpClientHandler()
 {
  AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
 };
 using (var client = new HttpClient(handler))
 {
  var response = await client.PostAsync("https://requestb.in/18i73061", content).ConfigureAwait(false);
  var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
  Console.WriteLine(result);
 }
}

static StreamContent JsonToGZipStream(string jsonParam)
{
 byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonParam);
 MemoryStream ms = new MemoryStream();
 using (GZipStream gzip = new GZipStream(ms, CompressionMode.Compress, true))
 {
  gzip.Write(jsonBytes, 0, jsonBytes.Length);
 }
 ms.Position = 0;
 StreamContent content = new StreamContent(ms);
 return content;
}

與平常的寫法差異只有加入 HttpClientHandler 的組態,指定 AutomaticDecompression,而這一行組態的重點在於要能處理前篇 ASP.NET Web API 已經把回應都使用 GZip 壓縮,當 HttpClient 取得使用 GZip 壓縮資料才能還原資料,這個還原資料的動作不需要在撰寫任何程式碼。然後 JSON 資料在傳遞前需要先使用 GZipStream 處理並指定好 Header 即可進行 POST 操作。

測試結果可以利用 https://requestb.in/ 查看:

Post GZip Data From HttpClient

RestSharp.org

RestSharp 也是一套強調簡單易用開源 HTTP Client 套件,我們也大量用於專案內。

void Main()
{
 var jsonData = new
 {
  Name = "ASP.NET Web API",
  Site = "skilltree.my",
  Lecturer = "Bruce Chen"
 };
 var jsonParam = JsonConvert.SerializeObject(jsonData);
 var content = JsonToGZipStream(jsonParam);
  
 var client = new RestClient("https://requestb.in/18i73061");
 var request = new RestRequest(Method.POST);
 request.AddHeader("cache-control", "no-cache");
 request.AddHeader("content-type", "application/json");
 request.AddHeader("content-encoding", "gzip");
 request.AddParameter("application/json", content.ToArray(), ParameterType.RequestBody);

 IRestResponse response = client.Execute(request);
 Console.WriteLine(response.Content);
}

static MemoryStream JsonToGZipStream(string jsonParam)
{
 byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonParam);
 MemoryStream ms = new MemoryStream();
 using (GZipStream gzip = new GZipStream(ms, CompressionMode.Compress, true))
 {
  gzip.Write(jsonBytes, 0, jsonBytes.Length);
 }
 ms.Position = 0;
 return ms;
}

RestSharp 不用特別設置本身即支援 GZip 的解壓縮,唯一要處理的是先將 JSON 做 GZipStream 處理,這部分前面已經看很多了,就不多解釋了。一樣來看一下 requestb.in 的結果:

Post GZip Data From RestSharp

從 Header 來看,,RestSharp 幫我們做的事情比 HttpClient 多一些。

JavaScript

目前在 JavaScript 環境,看到許多人介紹 JSONC 這一套來做 GZIP 的壓縮與解壓縮。如果你是 SPA 環境,又有大量資料要傳遞,可以試試看。

小結

這樣處理之後,整個 ASP.NET Web API 的資訊流,從進入到輸出都有完整的 GZip 配套措施。


沒有留言:

張貼留言

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