起因
通常我们在返回数据的时候,是要进行数据压缩,减少数据的大小,从而减少减少网络带宽的.这里的数据指文本(json/字符数据/xml),像图片这些一般不进行压缩,因为压缩比例不是很大.不像文本压缩比很高.对图片的优化处理一般是先生成一张小图,在需要的时候在加载大图.
使用响应压缩中间件
在Asp.Net Core提供了响应压缩的中间件,默认提供了两种压缩方式Brotli和Gzip两种方式,如果这两种方式还不满足你的需求,是可以自己进行定制和扩展的.
在ConfigureServices方法,进行添加AddResponseCompression
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml" });
});
services.Configure<BrotliCompressionProviderOptions>(config =>
{
config.Level = CompressionLevel.Fastest;
});
services.Configure<GzipCompressionProviderOptions>(config =>
{
config.Level = CompressionLevel.Fastest;
});
在Configure函数中使用:
app.UseResponseCompression();
对比效果还是很明显:
定制压缩方式
这个一般是在客户端使用的.需要在客户端在请求的时候,在请求头加入Accept-Encoding:xxx,然后在服务端看到请求头是Accept-Encoding:xxx就使用对应的压缩的方式进行响应.
1. 实现CustomResponseCompressionOptions
public class CustomResponseCompressionOptions : IOptions<CustomResponseCompressionOptions>
{
public CompressionLevel Level { get; set; } = CompressionLevel.Fastest;
public CustomResponseCompressionOptions Value => this;
}
2. 实现ICompressionProvider接口
public class CustomResponseCompressionProvider : ICompressionProvider
{
public CustomResponseCompressionProvider(IOptions<CustomResponseCompressionOptions> options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
Options = options.Value;
}
//因为这里还是浏览器,所以这里还是使用gzip, 单独的客户端可以定制化
public string EncodingName => "gzip";
public bool SupportsFlush => true;
public CustomResponseCompressionOptions Options { get; }
public Stream CreateStream(Stream outputStream)
{
return new GZipStream(outputStream, Options.Level, leaveOpen: true);
}
}
其实1不是必须的.如果不需要后期的扩展的,第二步的构造函数可以不注入IOptions实现的.只需要在指定EncodingName和CreateStream中具体响应报文的压缩.
响应压缩中间件 源码分析
按照使用顺序,进行源码查看,先分析AddResponseCompression
public static class ResponseCompressionServicesExtensions
{
public static IServiceCollection AddResponseCompression(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>();
return services;
}
public static IServiceCollection AddResponseCompression(this IServiceCollection services, Action<ResponseCompressionOptions> configureOptions)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.Configure(configureOptions);
services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>();
return services;
}
}
发现AddResponseCompression进行了方法重载.
1. 无参时,直接将ResponseCompressionProvider以单例的方式添加到容器中.
2. 有参数时,先将ResponseCompressionOptions添加到容器中,再去将ResponseCompressionProvider以单例的方式添加到容器中.
分析UseResponseCompression:
public static IApplicationBuilder UseResponseCompression(this IApplicationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return builder.UseMiddleware<ResponseCompressionMiddleware>();
}
ResponseCompressionMiddleware实现:
1. 在构造函数注入ResponseCompressionProvider实例
2. 在Invoke函数中
ResponseCompressionProvider代码:
public class ResponseCompressionProvider : IResponseCompressionProvider
{
private readonly ICompressionProvider[] _providers;
private readonly HashSet<string> _mimeTypes;
private readonly HashSet<string> _excludedMimeTypes;
private readonly bool _enableForHttps;
private readonly ILogger _logger;
public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseCompressionOptions> options)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
var responseCompressionOptions = options.Value;
_providers = responseCompressionOptions.Providers.ToArray();
if (_providers.Length == 0)
{
_providers = new ICompressionProvider[]
{
new CompressionProviderFactory(typeof(BrotliCompressionProvider)),
new CompressionProviderFactory(typeof(GzipCompressionProvider)),
};
}
for (var i = 0; i < _providers.Length; i++)
{
var factory = _providers[i] as CompressionProviderFactory;
if (factory != null)
{
_providers[i] = factory.CreateInstance(services);
}
}
var mimeTypes = responseCompressionOptions.MimeTypes;
if (mimeTypes == null || !mimeTypes.Any())
{
mimeTypes = ResponseCompressionDefaults.MimeTypes;
}
_mimeTypes = new HashSet<string>(mimeTypes, StringComparer.OrdinalIgnoreCase);
_excludedMimeTypes = new HashSet<string>(
responseCompressionOptions.ExcludedMimeTypes ?? Enumerable.Empty<string>(),
StringComparer.OrdinalIgnoreCase
);
_enableForHttps = responseCompressionOptions.EnableForHttps;
_logger = services.GetRequiredService<ILogger<ResponseCompressionProvider>>();
}
public bool CheckRequestAcceptsCompression(HttpContext context)
{
if (string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]))
{
_logger.NoAcceptEncoding();
return false;
}
_logger.RequestAcceptsCompression();
return true;
}
}
GzipCompressionProvider代码:
public class GzipCompressionProvider : ICompressionProvider
{
public GzipCompressionProvider(IOptions<GzipCompressionProviderOptions> options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
Options = options.Value;
}
private GzipCompressionProviderOptions Options { get; }
public string EncodingName { get; } = "gzip";
public bool SupportsFlush => true;
public Stream CreateStream(Stream outputStream)
=> new GZipStream(outputStream, Options.Level, leaveOpen: true);
}
ResponseCompressionMiddleware代码:
public class ResponseCompressionMiddleware
{
private readonly RequestDelegate _next;
private readonly IResponseCompressionProvider _provider;
public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (provider == null)
{
throw new ArgumentNullException(nameof(provider));
}
_next = next;
_provider = provider;
}
public async Task Invoke(HttpContext context)
{
if (!_provider.CheckRequestAcceptsCompression(context))
{
await _next(context);
return;
}
var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();
var originalCompressionFeature = context.Features.Get<IHttpsCompressionFeature>();
var compressionBody = new ResponseCompressionBody(context, _provider, originalBodyFeature);
context.Features.Set<IHttpResponseBodyFeature>(compressionBody);
context.Features.Set<IHttpsCompressionFeature>(compressionBody);
try
{
await _next(context);
await compressionBody.FinishCompressionAsync();
}
finally
{
context.Features.Set(originalBodyFeature);
context.Features.Set(originalCompressionFeature);
}
}
}
分析源码没有完全走完.因为没有编译生成的调试环境,编译是需要另外一个版本SDK,需要联网安装,一直无法下载的.等有了调试环境在分析具体如何实现的.这种环境还是需要在虚拟机进行,不让系统会产生很多垃圾文件的.