當有大量資料 Client/Server 之間往返時,可以考慮使用壓縮/解壓縮來降低網路流量的往返,不過,這伴隨而來副作用就是伺服器的資源損耗,使用時務必深思;壓縮/解壓縮是要彼此搭配,一方壓,另一方解,演算法也要能對應的比較常見的就是 GZip/Deflate 了,等下為了減少篇幅,我會只會呈現 Deflate 的實作,其餘的代碼就到 github 看
本文連結
- #開發環境
- #事前準備
- #情境 - Client 向 Server 上傳大量資源
- #情境 - Server 回傳大量資料給 Client
- #情境 - Client 向 Server 上傳大量資源 Part 2
- #專案位置
開發環境
- Windows 10 Enterprise
- VS 2019 Enterprise
- DotNetZip 1.13.3
- .NET Framework 4.7.2
事前準備
Server - Web API 專案
安裝套件
Install-Package DotNetZip
Install-Package Faker.Net
Install-Package Swagger-Net
Client - 單元測試專案
這裡會用 OWIN 把 Web API 建立起來,測試案例就可以打進去,由於這是 Lab,案例都是為了演示寫的
安裝套件
Install-Package Microsoft.AspNet.WebApi.OwinSelfHost
Install-Package Microsoft.Owin.Diagnostics
Install-Package Microsoft.Owin.Host.SystemWeb
Startup.cs
配置 Server,這裡會用到 Web API 專案的 WebApiConfig.Register(configuration)
public class Startup { public void Configuration(IAppBuilder app) { var configuration = new HttpConfiguration(); WebApiConfig.Register(configuration); //app.UseErrorPage(); //app.UseWelcomePage("/Welcome"); app.UseWebApi(configuration); } }
MsTestHook.cs
WebApp.Start 套用 Startup 把站台建立起來
[TestClass] public class MsTestHook { private const string HOST_ADDRESS = "http://localhost:9527"; private static IDisposable s_webApp; public static HttpClient Client; [AssemblyCleanup] public static void AssemblyCleanup() { s_webApp.Dispose(); } [AssemblyInitialize] public static void AssemblyInitialize(TestContext context) { s_webApp = WebApp.Start<Startup>(HOST_ADDRESS); Console.WriteLine("Web API started!"); Client = new HttpClient(); Client.BaseAddress = new Uri(HOST_ADDRESS); } }
壓縮/解壓縮
public class Deflate { public static byte[] Compress(byte[] sourceBytes) { if (sourceBytes == null) { return null; } using (var output = new MemoryStream()) { using (var compressor = new DeflateStream(output, CompressionMode.Compress, CompressionLevel.BestSpeed)) { compressor.Write(sourceBytes, 0, sourceBytes.Length); } return output.ToArray(); } } public static byte[] Decompress(byte[] sourceBytes) { if (sourceBytes == null) { return null; } using (var output = new MemoryStream()) { using (var compressor = new DeflateStream(output, CompressionMode.Decompress, CompressionLevel.BestSpeed)) { compressor.Write(sourceBytes, 0, sourceBytes.Length); } return output.ToArray(); } } }
完整範例
https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.Compress/Lab.Compress/Deflate.cs
https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.Compress/Lab.Compress/GZip.cs
情境 - Client 向 Server 上傳大量資源
[TestMethod] public void Client_DeflateCompress_Server_DeflateDecompress() { Console.WriteLine("用戶端用Deflate壓縮資料→伺服器端解壓縮後回傳結果→驗證解壓縮結果和Client壓縮前是否相同"); var url = "api/test/DeflateDecompression"; var builder = CreateData(); var contentBytes = Encoding.UTF8.GetBytes(builder); var zipContent = Deflate.Compress(contentBytes); var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = new ByteArrayContent(zipContent) }; var response = MsTestHook.Client.SendAsync(request).Result; var result = response.Content.ReadAsStringAsync().Result; Assert.AreEqual(response.StatusCode, HttpStatusCode.OK); Assert.AreEqual(builder, result); }
Server 收到後解壓縮,我要把解壓縮的邏輯抽離 Action,進入 Action 之前得先解壓縮,這裡我複寫 ActionFilter 的 OnActionExecuting
[DeflateDecompression] public IHttpActionResult DeflateDecompression() { var content = this.Request.Content.ReadAsByteArrayAsync().Result; var result = Encoding.UTF8.GetString(content); return new ResponseMessageResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(result, Encoding.UTF8) }); }
public class DeflateDecompressionAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { var content = actionContext.Request.Content; var zipContentBytes = content == null ? null : content.ReadAsByteArrayAsync().Result; var unzipContentBytes = zipContentBytes == null ? new byte[0] : Deflate.Decompress(zipContentBytes); actionContext.Request.Content = new ByteArrayContent(unzipContentBytes); base.OnActionExecuting(actionContext); } }
看看執行效果,Client 壓縮前是86582,壓縮後27263
Server 收到之後解壓縮
資料傳遞的 byte 長度好像不太一樣,不過解壓縮的內容是完全一樣的
情境 - Server 回傳大量資料給 Client
Server 收到後先產生大量資料,然後壓縮資料
[DeflateCompression] [HttpGet] public IHttpActionResult DeflateCompression(string id) { var content = CreateData(id); return new ResponseMessageResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(content, Encoding.UTF8) }); }
Action 執行完畢後再進行壓縮,這裡我複寫 ActionFilter 的 OnActionExecuted
public class DeflateCompressionAttribute : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext actionContext) { var content = actionContext.Response.Content; var sourceBytes = content == null ? null : content.ReadAsByteArrayAsync().Result; var zipContent = sourceBytes == null ? new byte[0] : Deflate.Compress(sourceBytes); actionContext.Response.Content = new ByteArrayContent(zipContent); actionContext.Response.Content.Headers.Remove("Content-Type"); actionContext.Response.Content.Headers.Add("Content-encoding", "deflate"); actionContext.Response.Content.Headers.Add("Content-Type", "application/json;charset=utf-8"); base.OnActionExecuted(actionContext); } }
Client 端收到後解壓縮
[TestMethod] public void Server_DeflateCompress_Client_DeflateDecompress_should_be_yao() { Console.WriteLine("用戶端用訪問伺服器→伺服器用Deflate壓縮資料→Client解壓縮,驗證解壓縮結果是否包含關鍵字"); var url = "api/test/DeflateCompression/yao"; var response = MsTestHook.Client.GetAsync(url).Result; var content = response.Content.ReadAsByteArrayAsync().Result; var decompress = Deflate.Decompress(content); var result = Encoding.UTF8.GetString(decompress); Assert.AreEqual(response.StatusCode, HttpStatusCode.OK); Assert.AreEqual(true, result.Contains("yao")); }
PS.由於資料是動態產生的,所以我只驗證我傳入的關鍵字,有沒有被解出來
用瀏覽器也能順利的解壓縮得到資料
情境 - Client 向 Server 上傳大量資源 Part 2
為了要壓縮各種不同的 Content, 我實作了 CompressContent 提供給 Client 用,他會把壓縮方式放到 Header 的 ContentEncoding 裡面,Server 會再根據它進行判斷
public class CompressContent : HttpContent { private readonly CompressMethod _compressionMethod; private readonly HttpContent _originalContent; public CompressContent(HttpContent content, CompressMethod compressionMethod) { if (content == null) { throw new ArgumentNullException(nameof(content)); } this._originalContent = content; this._compressionMethod = compressionMethod; foreach (var header in this._originalContent.Headers) { this.Headers.TryAddWithoutValidation(header.Key, header.Value); } this.Headers.ContentEncoding.Add(this._compressionMethod.ToString().ToLowerInvariant()); } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { if (this._compressionMethod == CompressMethod.GZip) { using (var gzipStream = new GZipStream(stream, CompressionMode.Compress, CompressionLevel.BestSpeed, true)) { await this._originalContent.CopyToAsync(gzipStream); } } else if (this._compressionMethod == CompressMethod.Deflate) { using (var deflateStream = new DeflateStream(stream, CompressionMode.Compress, CompressionLevel.BestSpeed, true)) { await this._originalContent.CopyToAsync(deflateStream); } } } protected override bool TryComputeLength(out long length) { length = -1; return false; } }
使用方式
[TestMethod] public void Client_DeflateCompressHttpContent_Server_DeflateHandlerDecompress() { var url = "api/test/Decompression"; var data = CreateData(); var content = new CompressContent(new StringContent(data, Encoding.UTF8, "text/plain"), CompressMethod.Deflate); var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content }; var response = MsTestHook.Client.SendAsync(request).Result; var result = response.Content.ReadAsStringAsync().Result; Assert.AreEqual(response.StatusCode, HttpStatusCode.OK); Assert.AreEqual(data, result); }
Server 端改用 Handler 來實作解壓縮,收到 Requre Header 的 ContentEncoding 有 gzip/deflate 關鍵字,就會進行解壓縮
public class DecompressHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request.Method == HttpMethod.Post) { var sourceContent = request.Content; var encodings = sourceContent.Headers.ContentEncoding; var isGzip = encodings.Contains("gzip"); var isDeflate = !isGzip && encodings.Contains("deflate"); if (isGzip || isDeflate) { var compressStream = await sourceContent.ReadAsStreamAsync(); var decompressStream = new MemoryStream(); if (isGzip) { using (var gzipStream = new GZipStream(compressStream, CompressionMode.Decompress, CompressionLevel.BestSpeed, true)) { await gzipStream.CopyToAsync(decompressStream); } } else if (isDeflate) { using (var gzipStream = new DeflateStream(compressStream, CompressionMode.Decompress, CompressionLevel.BestSpeed, true)) { await gzipStream.CopyToAsync(decompressStream); } } decompressStream.Seek(0, SeekOrigin.Begin); var targetContent = new StreamContent(decompressStream); foreach (var header in sourceContent.Headers) { targetContent.Headers.Add(header.Key, header.Value); } request.Content = targetContent; } } return await base.SendAsync(request, cancellationToken); } }
使用方式,註冊全域就可以了
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services config.MessageHandlers.Add(new DecompressHandler()); // Web API routes config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( "DefaultApi", "api/{controller}/{action}/{id}", new {id = RouteParameter.Optional} ); } }
專案位置
https://github.com/yaochangyu/sample.dotblog/tree/master/WebAPI/Lab.Compress
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET