響應壓縮技術是目前Web開發領域中比較經常使用的技術,在帶寬資源受限的狀況下,使用壓縮技術是提高帶寬負載的首選方案。咱們熟悉的Web服務器,好比IIS、Tomcat、Nginx、Apache等均可以使用壓縮技術,經常使用的壓縮類型包括Brotli、Gzip、Deflate,它們對CSS、JavaScript、HTML、XML 和 JSON等類型的效果仍是比較明顯的,可是也存在必定的限制對於圖片效果可能沒那麼好,由於圖片自己就是壓縮格式。其次,對於小於大約150-1000 字節的文件(具體取決於文件的內容和壓縮的效率,壓縮小文件的開銷可能會產生比未壓縮文件更大的壓縮文件。在ASP.NET Core中咱們可使用很是簡單的方式來使用響應壓縮。javascript
在ASP.NET Core中使用響應壓縮的方式比較簡單。首先,在ConfigureServices中添加services.AddResponseCompression注入響應壓縮相關的設置,好比使用的壓縮類型、壓縮級別、壓縮目標類型等。其次,在Configure添加app.UseResponseCompression攔截請求判斷是否須要壓縮,大體使用方式以下css
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddResponseCompression(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseResponseCompression(); } }
若是須要自定義一些配置的話還能夠手動設置壓縮相關html
public void ConfigureServices(IServiceCollection services) { services.AddResponseCompression(options => { //能夠添加多種壓縮類型,程序會根據級別自動獲取最優方式 options.Providers.Add<BrotliCompressionProvider>(); options.Providers.Add<GzipCompressionProvider>(); //添加自定義壓縮策略 options.Providers.Add<MyCompressionProvider>(); //針對指定的MimeType來使用壓縮策略 options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( new[] { "application/json" }); }); //針對不一樣的壓縮類型,設置對應的壓縮級別 services.Configure<GzipCompressionProviderOptions>(options => { //使用最快的方式進行壓縮,單不必定是壓縮效果最好的方式 options.Level = CompressionLevel.Fastest; //不進行壓縮操做 //options.Level = CompressionLevel.NoCompression; //即便須要耗費很長的時間,也要使用壓縮效果最好的方式 //options.Level = CompressionLevel.Optimal; }); }
關於響應壓縮大體的工做方式就是,當發起Http請求的時候在Request Header中添加Accept-Encoding:gzip或者其餘你想要的壓縮類型,能夠傳遞多個類型。服務端接收到請求獲取Accept-Encoding判斷是否支持該種類型的壓縮方式,若是支持則壓縮輸出內容相關而且設置Content-Encoding爲當前使用的壓縮方式一塊兒返回。客戶端獲得響應以後獲取Content-Encoding判斷服務端是否採用了壓縮技術,並根據對應的值判斷使用了哪一種壓縮類型,而後使用對應的解壓算法獲得原始數據。java
經過上面的介紹,相信你們對ResponseCompression有了必定的瞭解,接下來咱們經過查看源碼的方式瞭解一下它大體的工做原理。git
首先咱們來查看注入相關的代碼,具體代碼承載在ResponseCompressionServicesExtensions擴展類中[點擊查看源碼👈]github
public static class ResponseCompressionServicesExtensions { public static IServiceCollection AddResponseCompression(this IServiceCollection services) { services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>(); return services; } public static IServiceCollection AddResponseCompression(this IServiceCollection services, Action<ResponseCompressionOptions> configureOptions) { services.Configure(configureOptions); services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>(); return services; } }
主要就是注入ResponseCompressionProvider和ResponseCompressionOptions,首先咱們來看關於ResponseCompressionOptions[點擊查看源碼👈]算法
public class ResponseCompressionOptions { // 設置須要壓縮的類型 public IEnumerable<string> MimeTypes { get; set; } // 設置不須要壓縮的類型 public IEnumerable<string> ExcludedMimeTypes { get; set; } // 是否開啓https支持 public bool EnableForHttps { get; set; } = false; // 壓縮類型集合 public CompressionProviderCollection Providers { get; } = new CompressionProviderCollection(); }
關於這個類就不作過多介紹了,比較簡單。ResponseCompressionProvider是咱們提供響應壓縮算法的核心類,具體如何自動選用壓縮算法都是由它提供的。這個類中的代碼比較多,咱們就不逐個方法講解了,具體源碼可自行查閱[點擊查看源碼👈],首先咱們先看ResponseCompressionProvider的構造函數json
public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseCompressionOptions> options) { var responseCompressionOptions = options.Value; _providers = responseCompressionOptions.Providers.ToArray(); //若是沒有設置壓縮類型默認採用Br和Gzip壓縮算法 if (_providers.Length == 0) { _providers = new ICompressionProvider[] { new CompressionProviderFactory(typeof(BrotliCompressionProvider)), new CompressionProviderFactory(typeof(GzipCompressionProvider)), }; } //根據CompressionProviderFactory建立對應的壓縮算法Provider好比GzipCompressionProvider for (var i = 0; i < _providers.Length; i++) { var factory = _providers[i] as CompressionProviderFactory; if (factory != null) { _providers[i] = factory.CreateInstance(services); } } //設置默認的壓縮目標類型默認爲text/plain、text/css、text/html、application/javascript、application/xml //text/xml、application/json、text/json、application/was var mimeTypes = responseCompressionOptions.MimeTypes; if (mimeTypes == null || !mimeTypes.Any()) { mimeTypes = ResponseCompressionDefaults.MimeTypes; } //將默認MimeType放入HashSet _mimeTypes = new HashSet<string>(mimeTypes, StringComparer.OrdinalIgnoreCase); _excludedMimeTypes = new HashSet<string>( responseCompressionOptions.ExcludedMimeTypes ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase ); _enableForHttps = responseCompressionOptions.EnableForHttps; }
其中BrotliCompressionProvider、GzipCompressionProvider是具體提供壓縮方法的地方,我們就看比較經常使用的Gzip的Provider的大體實現[點擊查看源碼👈]服務器
public class GzipCompressionProvider : ICompressionProvider { public GzipCompressionProvider(IOptions<GzipCompressionProviderOptions> options) { Options = options.Value; } private GzipCompressionProviderOptions Options { get; } // 對應的Encoding名稱 public string EncodingName { get; } = "gzip"; public bool SupportsFlush => true; // 核心代碼就是這句 將原始的輸出流轉換爲壓縮的GZipStream // 咱們設置的Level壓縮級別將決定壓縮的性能和質量 public Stream CreateStream(Stream outputStream) => new GZipStream(outputStream, Options.Level, leaveOpen: true); }
關於ResponseCompressionProvider其餘相關的方法我們在講解UseResponseCompression中間件的時候在具體看用到的方法,由於這個類是響應壓縮的核心類,如今提早說了,到中間件使用的地方可能會忘記了。接下來咱們就看UseResponseCompression的大體實現。app
UseResponseCompression具體也就一個無參的擴展方法,也比較簡單,由於配置和工做都由注入的地方完成了,因此咱們直接查看中間件裏的實現,找到中間件位置ResponseCompressionMiddleware[點擊查看源碼👈]
public class ResponseCompressionMiddleware { private readonly RequestDelegate _next; private readonly IResponseCompressionProvider _provider; public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider) { _next = next; _provider = provider; } public async Task Invoke(HttpContext context) { //判斷是否包含Accept-Encoding頭信息,不包含直接大喊一聲"擡走下一個" if (!_provider.CheckRequestAcceptsCompression(context)) { await _next(context); return; } //獲取原始輸出Body var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>(); var originalCompressionFeature = context.Features.Get<IHttpsCompressionFeature>(); //初始化響應壓縮Body var compressionBody = new ResponseCompressionBody(context, _provider, originalBodyFeature); //設置成壓縮Body context.Features.Set<IHttpResponseBodyFeature>(compressionBody); context.Features.Set<IHttpsCompressionFeature>(compressionBody); try { await _next(context); await compressionBody.FinishCompressionAsync(); } finally { //恢復原始Body context.Features.Set(originalBodyFeature); context.Features.Set(originalCompressionFeature); } } }
這個中間件很是的簡單,就是初始化了ResponseCompressionBody。看到這裏你也許會好奇,並無觸發調用壓縮相關的任何代碼,ResponseCompressionBody也只是調用了FinishCompressionAsync都是和釋放相關的,不要着急咱們來看ResponseCompressionBody類的結構
internal class ResponseCompressionBody : Stream, IHttpResponseBodyFeature, IHttpsCompressionFeature { }
這個類實現了IHttpResponseBodyFeature,咱們使用的Response.Body其實就是獲取的HttpResponseBodyFeature.Stream屬性。咱們使用的Response.WriteAsync相關的方法,其實內部都是在調用PipeWriter進行寫操做,而PipeWriter就是來自HttpResponseBodyFeature.Writer屬性。能夠大體歸納爲,輸出相關的操做其核心都是在操做IHttpResponseBodyFeature。有興趣的能夠自行查閱HttpResponse相關的源碼能夠了解相關信息。因此咱們的ResponseCompressionBody實際上是重寫了輸出操做相關方法。也就是說,只要你調用了Response相關的Write或Body相關的,其實本質都是在操做IHttpResponseBodyFeature,因爲咱們開啓了響應輸出相關的中間件,因此會調用IHttpResponseBodyFeature的實現類ResponseCompressionBody相關的方法完成輸出。和咱們常規理解的仍是有誤差的,通常狀況下咱們認爲,其實只要針對輸出的Stream作操做就能夠了,可是響應壓縮中間件居然重寫了輸出相關的操做。
瞭解到這個以後,相信你們就沒有太多疑問了。因爲ResponseCompressionBody重寫了輸出相關的操做,代碼相對也比較多,就不逐一粘貼出來了,咱們只查看設計到響應壓縮核心相關的代碼,關於ResponseCompressionBody源碼相關的細節有興趣的能夠自行查閱[點擊查看源碼👈],輸出的本質其實都是在調用Write方法,咱們就來查看一下Write方法相關的實現
public override void Write(byte[] buffer, int offset, int count) { //這是核心方法有關於壓縮相關的輸出都在這 OnWrite(); //_compressionStream初始化在OnWrite方法裏 if (_compressionStream != null) { _compressionStream.Write(buffer, offset, count); if (_autoFlush) { _compressionStream.Flush(); } } else { _innerStream.Write(buffer, offset, count); } }
經過上面的代碼咱們看到OnWrite方法是核心操做,咱們直接查看OnWrite方法實現
private void OnWrite() { if (!_compressionChecked) { _compressionChecked = true; //判斷是否知足執行壓縮相關的邏輯 if (_provider.ShouldCompressResponse(_context)) { //匹配Vary頭信息對應的值 var varyValues = _context.Response.Headers.GetCommaSeparatedValues(HeaderNames.Vary); var varyByAcceptEncoding = false; //判斷Vary的值是否爲Accept-Encoding for (var i = 0; i < varyValues.Length; i++) { if (string.Equals(varyValues[i], HeaderNames.AcceptEncoding, StringComparison.OrdinalIgnoreCase)) { varyByAcceptEncoding = true; break; } } if (!varyByAcceptEncoding) { _context.Response.Headers.Append(HeaderNames.Vary, HeaderNames.AcceptEncoding); } //獲取最佳的ICompressionProvider即最佳的壓縮方式 var compressionProvider = ResolveCompressionProvider(); if (compressionProvider != null) { //設置選定的壓縮算法,放入Content-Encoding頭的值裏 //客戶端能夠經過Content-Encoding頭信息判斷服務端採用的哪一種壓縮算法 _context.Response.Headers.Append(HeaderNames.ContentEncoding, compressionProvider.EncodingName); //進行壓縮時,將 Content-MD5 刪除該標頭,由於正文內容已更改且哈希再也不有效。 _context.Response.Headers.Remove(HeaderNames.ContentMD5); //進行壓縮時,將 Content-Length 刪除該標頭,由於在對響應進行壓縮時,正文內容會發生更改。 _context.Response.Headers.Remove(HeaderNames.ContentLength); //返回壓縮相關輸出流 _compressionStream = compressionProvider.CreateStream(_innerStream); } } } } private ICompressionProvider ResolveCompressionProvider() { if (!_providerCreated) { _providerCreated = true; //調用ResponseCompressionProvider的方法返回最合適的壓縮算法 _compressionProvider = _provider.GetCompressionProvider(_context); } return _compressionProvider; }
從上面的邏輯咱們能夠看到,在執行壓縮相關邏輯以前須要判斷是否知足執行壓縮相關的方法ShouldCompressResponse,這個方法是ResponseCompressionProvider裏的方法,這裏就再也不粘貼代碼了,原本就是判斷邏輯我直接整理出來大體就是一下幾種狀況
public virtual ICompressionProvider GetCompressionProvider(HttpContext context) { var accept = context.Request.Headers[HeaderNames.AcceptEncoding]; //判斷請求頭是否包含Accept-Encoding信心 if (StringValues.IsNullOrEmpty(accept)) { Debug.Assert(false, "Duplicate check failed."); return null; } //獲取Accept-Encoding裏的值,判斷是否包含gzip、br、identity等,並返回匹配信息 if (!StringWithQualityHeaderValue.TryParseList(accept, out var encodings) || !encodings.Any()) { return null; } //根據請求信息和設置信息計算匹配優先級 var candidates = new HashSet<ProviderCandidate>(); foreach (var encoding in encodings) { var encodingName = encoding.Value; //Quality涉及到一個很是複雜的算法,有興趣的能夠自行查閱 var quality = encoding.Quality.GetValueOrDefault(1); //quality需大於0 if (quality < double.Epsilon) { continue; } //匹配請求頭裏encodingName和設置的providers壓縮算法裏EncodingName一致的算法 //從這裏能夠看出匹配的優先級和註冊providers裏的順序也有關係 for (int i = 0; i < _providers.Length; i++) { var provider = _providers[i]; if (StringSegment.Equals(provider.EncodingName, encodingName, StringComparison.OrdinalIgnoreCase)) { candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); } } //若是請求頭裏EncodingName是*的狀況則在全部註冊的providers裏進行匹配 if (StringSegment.Equals("*", encodingName, StringComparison.Ordinal)) { for (int i = 0; i < _providers.Length; i++) { var provider = _providers[i]; candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); } break; } //若是請求頭裏EncodingName是identity的狀況,則不對響應進行編碼 if (StringSegment.Equals("identity", encodingName, StringComparison.OrdinalIgnoreCase)) { candidates.Add(new ProviderCandidate(encodingName.Value, quality, priority: int.MaxValue, provider: null)); } } ICompressionProvider selectedProvider = null; //若是匹配的只有一個則直接返回 if (candidates.Count <= 1) { selectedProvider = candidates.FirstOrDefault().Provider; } else { //若是匹配到多個則按照Quality倒序和Priority正序的負責匹配第一個 selectedProvider = candidates .OrderByDescending(x => x.Quality) .ThenBy(x => x.Priority) .First().Provider; } //若是沒有匹配到selectedProvider或是identity的狀況直接返回null if (selectedProvider == null) { return null; } return selectedProvider; }
經過以上的介紹咱們能夠大體瞭解到響應壓縮的大體工做方式,簡單總結一下
在查看相關代碼以前,原本覺得關於響應壓縮相關的邏輯會很是的簡單,看過了源碼才知道是本身想的太簡單了。其中和本身想法出入最大的莫過於在ResponseCompressionMiddleware中間件裏,本覺得是經過統一攔截輸出流來進行壓縮操做,沒想到是對總體輸出操做進行重寫。由於在以前咱們使用Asp.Net相關框架的時候是統一寫Filter或者HttpModule進行處理的,因此存在思惟定式。多是Asp.Net Core設計者有更深層次的理解,多是我理解的還不夠完全,不可以體會這樣作的好處到底是什麼,若是你有更好的理解或則答案歡迎在評論區裏留言解惑。