ASP.NET Core中的響應壓縮的實現(xiàn)
介紹#
響應壓縮技術是目前Web開發(fā)領域中比較常用的技術,在帶寬資源受限的情況下,使用壓縮技術是提升帶寬負載的首選方案。我們熟悉的Web服務器,比如IIS、Tomcat、Nginx、Apache等都可以使用壓縮技術,常用的壓縮類型包括Brotli、Gzip、Deflate,它們對CSS、JavaScript、HTML、XML 和 JSON等類型的效果還是比較明顯的,但是也存在一定的限制對于圖片效果可能沒那么好,因為圖片本身就是壓縮格式。其次,對于小于大約150-1000 字節(jié)的文件(具體取決于文件的內容和壓縮的效率,壓縮小文件的開銷可能會產生比未壓縮文件更大的壓縮文件。在ASP.NET Core中我們可以使用非常簡單的方式來使用響應壓縮。
使用方式#
在ASP.NET Core中使用響應壓縮的方式比較簡單。首先,在ConfigureServices中添加services.AddResponseCompression注入響應壓縮相關的設置,比如使用的壓縮類型、壓縮級別、壓縮目標類型等。其次,在Configure添加app.UseResponseCompression攔截請求判斷是否需要壓縮,大致使用方式如下
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseResponseCompression();
}
}
如果需要自定義一些配置的話還可以手動設置壓縮相關
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression(options =>
{
//可以添加多種壓縮類型,程序會根據級別自動獲取最優(yōu)方式
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;
});
}
關于響應壓縮大致的工作方式就是,當發(fā)起Http請求的時候在Request Header中添加Accept-Encoding:gzip或者其他你想要的壓縮類型,可以傳遞多個類型。服務端接收到請求獲取Accept-Encoding判斷是否支持該種類型的壓縮方式,如果支持則壓縮輸出內容相關并且設置Content-Encoding為當前使用的壓縮方式一起返回??蛻舳说玫巾憫螳@取Content-Encoding判斷服務端是否采用了壓縮技術,并根據對應的值判斷使用了哪種壓縮類型,然后使用對應的解壓算法得到原始數(shù)據。
源碼探究#
通過上面的介紹,相信大家對ResponseCompression有了一定的了解,接下來我們通過查看源碼的方式了解一下它大致的工作原理。
AddResponseCompression#
首先我們來查看注入相關的代碼,具體代碼承載在ResponseCompressionServicesExtensions擴展類中[點擊查看源碼👈]
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的構造函數(shù)
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創(chuàng)建對應的壓縮算法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的大致實現(xiàn)[點擊查看源碼👈]
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中間件的時候在具體看用到的方法,因為這個類是響應壓縮的核心類,現(xiàn)在提前說了,到中間件使用的地方可能會忘記了。接下來我們就看UseResponseCompression的大致實現(xiàn)。
UseResponseCompression#
UseResponseCompression具體也就一個無參的擴展方法,也比較簡單,因為配置和工作都由注入的地方完成了,所以我們直接查看中間件里的實現(xiàn),找到中間件位置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??吹竭@里你也許會好奇,并沒有觸發(fā)調用壓縮相關的任何代碼,ResponseCompressionBody也只是調用了FinishCompressionAsync都是和釋放相關的,不要著急我們來看ResponseCompressionBody類的結構
internal class ResponseCompressionBody : Stream, IHttpResponseBodyFeature, IHttpsCompressionFeature
{
}
這個類實現(xiàn)了IHttpResponseBodyFeature,我們使用的Response.Body其實就是獲取的HttpResponseBodyFeature.Stream屬性。我們使用的Response.WriteAsync相關的方法,其實內部都是在調用PipeWriter進行寫操作,而PipeWriter就是來自HttpResponseBodyFeature.Writer屬性??梢源笾赂爬?,輸出相關的操作其核心都是在操作IHttpResponseBodyFeature。有興趣的可以自行查閱HttpResponse相關的源碼可以了解相關信息。所以我們的ResponseCompressionBody其實是重寫了輸出操作相關方法。也就是說,只要你調用了Response相關的Write或Body相關的,其實本質都是在操作IHttpResponseBodyFeature,由于我們開啟了響應輸出相關的中間件,所以會調用IHttpResponseBodyFeature的實現(xiàn)類ResponseCompressionBody相關的方法完成輸出。和我們常規(guī)理解的還是有偏差的,一般情況下我們認為,其實只要針對輸出的Stream做操作就可以了,但是響應壓縮中間件竟然重寫了輸出相關的操作。
了解到這個之后,相信大家就沒有太多疑問了。由于ResponseCompressionBody重寫了輸出相關的操作,代碼相對也比較多,就不逐一粘貼出來了,我們只查看設計到響應壓縮核心相關的代碼,關于ResponseCompressionBody源碼相關的細節(jié)有興趣的可以自行查閱[點擊查看源碼👈],輸出的本質其實都是在調用Write方法,我們就來查看一下Write方法相關的實現(xiàn)
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方法實現(xiàn)
private void OnWrite()
{
if (!_compressionChecked)
{
_compressionChecked = true;
//判斷是否滿足執(zhí)行壓縮相關的邏輯
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 刪除該標頭,因為在對響應進行壓縮時,正文內容會發(fā)生更改。
_context.Response.Headers.Remove(HeaderNames.ContentLength);
//返回壓縮相關輸出流
_compressionStream = compressionProvider.CreateStream(_innerStream);
}
}
}
}
private ICompressionProvider ResolveCompressionProvider()
{
if (!_providerCreated)
{
_providerCreated = true;
//調用ResponseCompressionProvider的方法返回最合適的壓縮算法
_compressionProvider = _provider.GetCompressionProvider(_context);
}
return _compressionProvider;
}
從上面的邏輯我們可以看到,在執(zhí)行壓縮相關邏輯之前需要判斷是否滿足執(zhí)行壓縮相關的方法ShouldCompressResponse,這個方法是ResponseCompressionProvider里的方法,這里就不再粘貼代碼了,本來就是判斷邏輯我直接整理出來大致就是一下幾種情況
- 如果請求是Https的情況下,是否設置了允許Https情況下壓縮的設置,即ResponseCompressionOptions的EnableForHttps屬性設置
- Response.Head里不能包含Content-Range頭信息
- Response.Head里之前不能包含Content-Encoding頭信息
- Response.Head里之前必須要包含Content-Type頭信息
- 返回的MimeType里不能包含配置的不需要壓縮的類型,即ResponseCompressionOptions的ExcludedMimeTypes
- 返回的MimeType里需要包含配置的需要壓縮的類型,即ResponseCompressionOptions的MimeTypes
- 如果不滿足上面的兩種情況,返回的MimeType里包含*/*也可以執(zhí)行響應壓縮
接下來我們查看ResponseCompressionProvider的GetCompressionProvider方法看它是如何確定返回哪一種壓縮類型的
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;
}
//根據請求信息和設置信息計算匹配優(yōu)先級
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一致的算法
//從這里可以看出匹配的優(yōu)先級和注冊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;
}
通過以上的介紹我們可以大致了解到響應壓縮的大致工作方式,簡單總結一下
- 首先設置壓縮相關的算法類型或是壓縮目標的MimeType
- 其次我們可以設置壓縮級別,這將決定壓縮的質量和壓縮性能
- 通過響應壓縮中間件,我們可以獲取到一個優(yōu)先級最高的壓縮算法進行壓縮,這種情況主要是針對多種壓縮類型的情況。這個壓縮算法與內部機制和注冊壓縮算法的順序都有一定的關系,最終會選擇權重最大的返回。
- 響應壓縮中間件的核心工作類ResponseCompressionBody通過實現(xiàn)IHttpResponseBodyFeature,重寫輸出相關的方法實現(xiàn)對響應的壓縮,不需要我們手動進行調用相關方法,而是替換掉默認的輸出方式。只要設置了響應壓縮,并且請求滿足響應壓縮,那么有調用輸出的地方默認都是執(zhí)行ResponseCompressionBody里壓縮相關的方法,而不是攔截具體的輸出進行統(tǒng)一處理。至于為什么這么做,目前我還沒有理解到設計者真正的考慮。
總結#
在查看相關代碼之前,本來以為關于響應壓縮相關的邏輯會非常的簡單,看過了源碼才知道是自己想的太簡單了。其中和自己想法出入最大的莫過于在ResponseCompressionMiddleware中間件里,本以為是通過統(tǒng)一攔截輸出流來進行壓縮操作,沒想到是對整體輸出操作進行重寫。因為在之前我們使用Asp.Net相關框架的時候是統(tǒng)一寫Filter或者HttpModule進行處理的,所以存在思維定式。可能是Asp.Net Core設計者有更深層次的理解,可能是我理解的還不夠徹底,不能夠體會這樣做的好處究竟是什么,如果你有更好的理解或則答案歡迎在評論區(qū)里留言解惑。
到此這篇關于ASP.NET Core中的響應壓縮的實現(xiàn)的文章就介紹到這了,更多相關ASP.NET Core 響應壓縮內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- ASP.NET Core 文件響應壓縮的常見使用誤區(qū)
- asp.net core為IHttpClientFactory添加動態(tài)命名配置
- 如何在ASP.NET Core中使用HttpClientFactory
- 在ASP.NET Core中用HttpClient發(fā)送POST, PUT和DELETE請求
- .NET CORE HttpClient的使用方法
- .NET Core使用HttpClient進行表單提交時遇到的問題
- .Net Core下HTTP請求IHttpClientFactory示例詳解
- Asp.Net Core2.1前后使用HttpClient的兩種方式
- ASP.NET Core針對一個使用HttpClient對象的類編寫單元測試詳解
- .NET Core中HttpClient的正確打開方式
- .NET Core中使用HttpClient的正確姿勢
- .NET Core 2.1中HttpClientFactory的最佳實踐記錄
- .Net Core HttpClient處理響應壓縮詳細
相關文章
關于服務器或虛擬主機不支持 AjaxPro 的問題終極解決方法
asp.net的網站,訪問時提示不支持 AjaxPro,那就因為誤刪的映射導致,可以通過下面的方法解決2012-03-03
.NET?如何使用?OpenTelemetry?metrics?監(jiān)控應用程序指標
這篇文章主要介紹了.NET?使用?OpenTelemetry?metrics?監(jiān)控應用程序指標,通過代碼演示了如何通過 OpenTelemetry 把 Metrics 的數(shù)據發(fā)送到 Prometheus 里進行查詢與展示,然后又演示了自定義相關指標來滿足業(yè)務數(shù)據指標的監(jiān)控,需要的朋友可以參考下2024-06-06
HttpWebRequest的常見錯誤使用TcpClient可避免
有時使用HttpWebRequest對象會出現(xiàn)錯誤有三種服務器提交了協(xié)議沖突/基礎連接已經關閉:連接被意外關閉/無法發(fā)送具有此謂詞類型的內容正文,感興趣的朋友可以參考下本文2013-02-02
在ASP.NET Core中實現(xiàn)一個Token base的身份認證實例
以前在web端的身份認證都是基于Cookie | Session的身份認證,本篇文章主要介紹了在ASP.NET Core中實現(xiàn)一個Token base的身份認證實例,有興趣的可以了解一下。2016-12-12

