响应压缩技术是目前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设计者有更深层次的理解,多是我理解的还不够完全,不可以体会这样作的好处到底是什么,若是你有更好的理解或则答案欢迎在评论区里留言解惑。