介绍

响应压缩技术是目前Web开发领域中比较常用的技术,在带宽资源受限的情况下,使用压缩技术是提升带宽负载的首选方案。我们熟悉的Web服务器,比如IIS、Tomcat、Nginx、Apache等都可以使用压缩技术,常用的压缩类型包括Brotli、Gzip、Deflate,它们对CSS、JavaScript、HTML、XML 和 JSON等类型的效果还是比较明显的,但是也存在一定的限制对于图片效果可能没那么好,因为图片本身就是压缩格式。其次,对于小于大约150-1000 字节的文件(具体取决于文件的内容和压缩的效率,压缩小文件的开销可能会产生比未压缩文件更大的压缩文件。在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 =>{//可以添加多种压缩类型,程序会根据级别自动获取最优方式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判断服务端是否采用了压缩技术,并根据对应的值判断使用了哪种压缩类型,然后使用对应的解压算法得到原始数据。

源码探究

通过上面的介绍,相信大家对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的构造函数

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比如GzipCompressionProviderfor (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/wasvar 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的大致实现。

UseResponseCompression

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;}//获取原始输出Bodyvar originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();var originalCompressionFeature = context.Features.Get<IHttpsCompressionFeature>();//初始化响应压缩Bodyvar compressionBody = new ResponseCompressionBody(context, _provider, originalBodyFeature);//设置成压缩Bodycontext.Features.Set<IHttpResponseBodyFeature>(compressionBody);context.Features.Set<IHttpsCompressionFeature>(compressionBody);try{await _next(context);await compressionBody.FinishCompressionAsync();}finally{//恢复原始Bodycontext.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-Encodingfor (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里的方法,这里就不再粘贴代码了,本来就是判断逻辑我直接整理出来大致就是一下几种情况

  • 如果请求是Https的情况下,是否设置了允许Https情况下压缩的设置,即ResponseCompressionOptions的EnableForHttps属性设置

  • Response.Head里不能包含Content-Range头信息

  • Response.Head里之前不能包含Content-Encoding头信息

  • Response.Head里之前必须要包含Content-Type头信息

  • 返回的MimeType里不能包含配置的不需要压缩的类型,即ResponseCompressionOptions的ExcludedMimeTypes

  • 返回的MimeType里需要包含配置的需要压缩的类型,即ResponseCompressionOptions的MimeTypes

  • 如果不满足上面的两种情况,返回的MimeType里包含*/*也可以执行响应压缩

    接下来我们查看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;}//根据请求信息和设置信息计算匹配优先级var candidates = new HashSet<ProviderCandidate>();foreach (var encoding in encodings){var encodingName = encoding.Value;//Quality涉及到一个非常复杂的算法,有兴趣的可以自行查阅var quality = encoding.Quality.GetValueOrDefault(1);//quality需大于0if (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的情况直接返回nullif (selectedProvider == null){return null;}return selectedProvider;
}

通过以上的介绍我们可以大致了解到响应压缩的大致工作方式,简单总结一下

  • 首先设置压缩相关的算法类型或是压缩目标的MimeType

  • 其次我们可以设置压缩级别,这将决定压缩的质量和压缩性能

  • 通过响应压缩中间件,我们可以获取到一个优先级最高的压缩算法进行压缩,这种情况主要是针对多种压缩类型的情况。这个压缩算法与内部机制和注册压缩算法的顺序都有一定的关系,最终会选择权重最大的返回。

  • 响应压缩中间件的核心工作类ResponseCompressionBody通过实现IHttpResponseBodyFeature,重写输出相关的方法实现对响应的压缩,不需要我们手动进行调用相关方法,而是替换掉默认的输出方式。只要设置了响应压缩,并且请求满足响应压缩,那么有调用输出的地方默认都是执行ResponseCompressionBody里压缩相关的方法,而不是拦截具体的输出进行统一处理。至于为什么这么做,目前我还没有理解到设计者真正的考虑。

总结

在查看相关代码之前,本来以为关于响应压缩相关的逻辑会非常的简单,看过了源码才知道是自己想的太简单了。其中和自己想法出入最大的莫过于在ResponseCompressionMiddleware中间件里,本以为是通过统一拦截输出流来进行压缩操作,没想到是对整体输出操作进行重写。因为在之前我们使用Asp.Net相关框架的时候是统一写Filter或者HttpModule进行处理的,所以存在思维定式。可能是Asp.Net Core设计者有更深层次的理解,可能是我理解的还不够彻底,不能够体会这样做的好处究竟是什么,如果你有更好的理解或则答案欢迎在评论区里留言解惑。

????欢迎扫码关注我的公众号????

ASP.NET Core中的响应压缩相关推荐

  1. [小技巧]ASP.NET Core中如何预压缩静态文件

    原文地址:Pre-compressed static files with ASP.NET Core 作者:Gunnar Peipman 译者:Lamond Lu 译文:https://www.cnb ...

  2. 在ASP.NET Core中使用brotli压缩

    Brotli是一种全新的数据格式,可以提供比Zopfli高20-26%的压缩比.据谷歌研究,Brotli压缩速度同zlib的Deflate实现大致相同,而在Canterbury语料库上的压缩密度比LZ ...

  3. .Net Core HttpClient处理响应压缩

    前言 在上篇文章[ASP.NET Core中的响应压缩]中我们谈到了在ASP.NET Core服务端处理关于响应压缩的请求,服务端的主要工作就是根据Content-Encoding头信息判断采用哪种方 ...

  4. 在asp.net core中使用托管服务实现后台任务

    在业务场景中经常需要后台服务不停的或定时处理一些任务,这些任务是不需要及时响应请求的. 在 asp.net中会使用windows服务来处理. 在 asp.net core中,可以使用托管服务来实现,托 ...

  5. 如何简单的在 ASP.NET Core 中集成 JWT 认证?

    前情提要:ASP.NET Core 使用 JWT 搭建分布式无状态身份验证系统 文章超长预警(1万字以上),不想看全部实现过程的同学可以直接跳转到末尾查看成果或者一键安装相关的 nuget 包 自上一 ...

  6. .ASP NET Core中缓存问题案例

    本篇博客中,我将描述一个关于会话状态(Session State)的问题, 这个问题我已经被询问了好几次了. 问题的场景 创建一个新的ASP.NET Core应用程序 一个用户在会话状态中设置了一个字 ...

  7. ASP.NET Core 中文文档 第三章 原理(13)管理应用程序状态

    原文:Managing Application State 作者:Steve Smith 翻译:姚阿勇(Dr.Yao) 校对:高嵩 在 ASP.NET Core 中,有多种途径可以对应用程序的状态进行 ...

  8. Api网关Kong集成Consul做服务发现及在Asp.Net Core中的使用

     1622219047536 写在前面   Api网关我们之前是用 .netcore写的 Ocelot的,使用后并没有完全达到我们的预期,花了些时间了解后觉得kong可能是个更合适的选择. 简单说下 ...

  9. 如何在 ASP.Net Core 中使用 HTTP.sys WebServer ?

    ASP.Net Core 是一个开源的,跨平台的,轻量级模块化框架,可用它来构建高性能的Web程序,大家都知道 Kestrel 是 ASP.Net Core 内置的跨平台web服务器,但是它有一定的局 ...

最新文章

  1. 《精通并发与Netty》学习笔记(13 - 解决TCP粘包拆包(一)概念及实例演示)
  2. 电脑回收站删除的文件怎么恢复,原来这么简单
  3. 深入思考:算法工程师的落地能力具体指什么,如何提升自己的落地能力?
  4. win7系统备份还原软件_收藏备用!操作系统的快速备份与还原技巧
  5. 文献记录(part19)--Cross modal similarity learning with active queries
  6. Kotlin: class的参数列表加val/var与不加的区别
  7. android studio: 让项目通过阿里云 maven jcenter 下载依赖资源
  8. code review平台Rietveld应用指南
  9. KS(Kolmogorov–Smirnov)模型区分能力指标
  10. mysql类exadata功能_Exadata:Smart Scan(一)
  11. ios开发 之 简体和繁体中文转换
  12. 《指弹:风居住的街道》
  13. 基于MATLAB的三重积分与曲线积分
  14. 代码随想录第十五天 二叉树层序遍历 226、101
  15. 天津大学计算机专硕_天津大学计算机复试攻略
  16. 数据简化社区Google和Linux代码风格指南(附PDF公号发“代码风格”下载)
  17. 使用wxjava实现发表内容、预览信息以及推送文章
  18. 2019年末,来一发基于Hexo自建博客生态指南!
  19. 释放自我。回归本性。要成功。
  20. Ubuntu虚拟机没有网络图标或者无法获取IP地址

热门文章

  1. mysql常见监控项
  2. 关于c# .net爬虫
  3. Python easy_install
  4. UVa 12100 - Printer Queue
  5. WPF企业内训全程实录(下)
  6. 终于收到HacktoberFest的奖品啦
  7. (原創) 07/28/1982 少女A (中森明菜)
  8. 前端实现连连看小游戏(1)
  9. js温故而知新11(AJAX)——学习廖雪峰的js教程
  10. 《面向对象的思考过程(原书第4版)》一1.11 组合