ASP.NET Core中的响应压缩
介绍
响应压缩技术是目前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比如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的大致实现。
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;
}
//获取原始输出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里的方法,这里就不再粘贴代码了,本来就是判断逻辑我直接整理出来大致就是一下几种情况
- 如果请求是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需大于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;
}
通过以上的介绍我们可以大致了解到响应压缩的大致工作方式,简单总结一下
- 首先设置压缩相关的算法类型或是压缩目标的MimeType
- 其次我们可以设置压缩级别,这将决定压缩的质量和压缩性能
- 通过响应压缩中间件,我们可以获取到一个优先级最高的压缩算法进行压缩,这种情况主要是针对多种压缩类型的情况。这个压缩算法与内部机制和注册压缩算法的顺序都有一定的关系,最终会选择权重最大的返回。
- 响应压缩中间件的核心工作类ResponseCompressionBody通过实现IHttpResponseBodyFeature,重写输出相关的方法实现对响应的压缩,不需要我们手动进行调用相关方法,而是替换掉默认的输出方式。只要设置了响应压缩,并且请求满足响应压缩,那么有调用输出的地方默认都是执行ResponseCompressionBody里压缩相关的方法,而不是拦截具体的输出进行统一处理。至于为什么这么做,目前我还没有理解到设计者真正的考虑。
总结
在查看相关代码之前,本来以为关于响应压缩相关的逻辑会非常的简单,看过了源码才知道是自己想的太简单了。其中和自己想法出入最大的莫过于在ResponseCompressionMiddleware中间件里,本以为是通过统一拦截输出流来进行压缩操作,没想到是对整体输出操作进行重写。因为在之前我们使用Asp.Net相关框架的时候是统一写Filter或者HttpModule进行处理的,所以存在思维定式。可能是Asp.Net Core设计者有更深层次的理解,可能是我理解的还不够彻底,不能够体会这样做的好处究竟是什么,如果你有更好的理解或则答案欢迎在评论区里留言解惑。
ASP.NET Core中的响应压缩的更多相关文章
- asp.net core 中配合响应 html5 的音视频播放流,以及文件下载
一.asp.net core 中配合响应 html5 的音视频播放流,以及文件下载 问题描述: 目前测试了在 Windows(谷歌浏览器).Android(系统浏览器.QQ.微信).iOS 三个系统不 ...
- [小技巧]ASP.NET Core中如何预压缩静态文件
原文地址:Pre-compressed static files with ASP.NET Core 作者:Gunnar Peipman 译者:Lamond Lu 译文:https://www.cnb ...
- 在ASP.NET Core中使用brotli压缩
Brotli是一种全新的数据格式,可以提供比Zopfli高20-26%的压缩比.据谷歌研究,Brotli压缩速度同zlib的Deflate实现大致相同,而在Canterbury语料库上的压缩密度比LZ ...
- .Net Core HttpClient处理响应压缩
前言 在上篇文章[ASP.NET Core中的响应压缩]中我们谈到了在ASP.NET Core服务端处理关于响应压缩的请求,服务端的主要工作就是根据Content-Encoding头信息判断采 ...
- C#调用接口注意要点 socket,模拟服务器、客户端通信 在ASP.NET Core中构建路由的5种方法
C#调用接口注意要点 在用C#调用接口的时候,遇到需要通过调用登录接口才能调用其他的接口,因为在其他的接口需要在登录的状态下保存Cookie值才能有权限调用, 所以首先需要通过调用登录接口来保存c ...
- ASP.NET Core 中的SEO优化(1):中间件实现服务端静态化缓存
分享 最近在公司成功落地了一个用ASP.NET Core 开发前台的CMS项目,虽然对于表层的开发是兼容MVC5的,但是作为爱好者当然要用尽量多的ASP.NET Core新功能了. 背景 在项目开发的 ...
- gRPC在 ASP.NET Core 中应用学习(二)
前言: 上一篇文章中简单的对gRPC进行了简单了解,并实现了gRPC在ASP.NET Core中服务实现.客户端调用:那么本篇继续对gRPC的4中服务方法定义.其他使用注意点进一步了解学习 一.gRP ...
- ASP.NET Core中的依赖注入(1):控制反转(IoC)
ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化&qu ...
- ASP.NET Core 中文文档 第二章 指南(4.6)Controller 方法与视图
原文:Controller methods and views 作者:Rick Anderson 翻译:谢炀(Kiler) 校对:孟帅洋(书缘) .张仁建(第二年.夏) .许登洋(Seay) .姚阿勇 ...
随机推荐
- Pycharm连接MySQL后出现不出现数据库或表,出现其他文件的问题
在使用pycharm连接MySQL,配置完成,测试连接通过之后,还是不能显示数据库中的表,出现了许多像armscii8_bin.armscii8_general_ci和ascii_bin等的文件. 解 ...
- js中取el表达式问题
例如常用的${pageContext.request.contextPath} 如果需要在js中用到 分两种情况: 如果js是直接写在jsp中 可以直接写el表达式 例如: 如果js是写在外部,jsp ...
- C#数据结构与算法系列(二十一):希尔排序算法(ShellSort)
1.介绍 希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法.希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序. 2.基本思想 希尔排 ...
- Redundant Paths 分离的路径【边双连通分量】
Redundant Paths 分离的路径 题目描述 In order to get from one of the F (1 <= F <= 5,000) grazing fields ...
- ES6入门(二)
目录 ES6入门(二) es6之解构赋值 数组的解构赋值 对象的解构赋值 字符串的解构赋值 数值和布尔值的解构赋值 函数参数的解构赋值 圆括号问题 ES6入门(二) es6之解构赋值 数组的解构赋值 ...
- vue+element-ui JYAdmin后台管理系统模板-集成方案【项目搭建篇2】
项目搭建时间:2020-06-29 本章节:讲述基于vue/cli, 项目的基础搭建. 本主题讲述了: 1.跨域配置 2.axios请求封装 3.eslint配置 4.环境dev,test,pro(开 ...
- 06 Vue生命周期钩子
生命周期钩子 表示一个vue实例从创建到销毁的这个过程,将这个过程的一些时间节点赋予了对应的钩子函数 钩子函数: 满足特点条件被回调的方法 new Vue({ el: "#app" ...
- Unity-JobSystom
什么是Job System? 一个job system通过创建jobs而不是线程来管理多线程的代码.Job是一个小的工作单元,不等同线程.管理运行在多个核心上的一组工人线程(worker thread ...
- day1:注释和变量
1.注释的作用:对代码的解释,方便以后阅读代码 2.常用的快捷键:ctrl+q:notepad++的注释ctrl+/:pycharm的注释ctrl+c:复制ctrl+v:粘贴ctrl+z:撤销ctrl ...
- IDEA 2020.1 查看内存使用情况