Asp.Net Core 响应压缩中间件

起因

通常我们在返回数据的时候,是要进行数据压缩,减少数据的大小,从而减少减少网络带宽的.这里的数据指文本(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" });
});

//配置BrotliCompression 压缩级别  Optimal(最佳的压缩方式,耗费的时间较长)  Fastest(最快的压缩方式) NoCompression(不进行压缩)
services.Configure<BrotliCompressionProviderOptions>(config =>
{
    config.Level = CompressionLevel.Fastest;
});

//配置Gzip  压缩级别  Optimal(最佳的压缩方式,耗费的时间较长)  Fastest(最快的压缩方式) NoCompression(不进行压缩)
services.Configure<GzipCompressionProviderOptions>(config =>
{
    config.Level = CompressionLevel.Fastest;
});

在Configure函数中使用:

//官方早期推荐在app.UseMvc之前调用
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中具体响应报文的压缩.

响应压缩中间件 源码分析

Asp.Net Core 响应压缩中间件

按照使用顺序,进行源码查看,先分析AddResponseCompression
public static class ResponseCompressionServicesExtensions
{
    /// <summary>
    /// Add response compression services.
    /// 没有参数配置,使用默认配置
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
    /// <returns></returns>
    public static IServiceCollection AddResponseCompression(this IServiceCollection services)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }

        //将ResponseCompressionProvider和对应的接口,以单例的方式添加到容器
        services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>();
        return services;
    }

    /// <summary>
    /// Add response compression services and configure the related options.
    /// 有参数配置的委托
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
    /// <param name="configureOptions">A delegate to configure the <see cref="ResponseCompressionOptions"/>.</param>
    /// <returns></returns>
    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);
        //将ResponseCompressionProvider和对应的接口,以单例的方式添加到容器
        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;

    /// <summary>
    /// If no compression providers are specified then GZip is used by default.
    /// </summary>
    /// <param name="services">Services to use when instantiating compression providers.</param>
    /// <param name="options">从容器从获取响应压缩中间件配置</param>
    public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseCompressionOptions> options)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }
        // 1. 无参数的时候  services.AddResponseCompression();
        // 2. 有参数,添加BrotliCompressionProvider和GzipCompressionProvider
        //services.AddResponseCompression(options =>
        //{
        //    //往内部添加的时候,new CompressionProviderFactory(typeof(BrotliCompressionProvider))
        //    options.Providers.Add<BrotliCompressionProvider>();  
        //    options.Providers.Add<GzipCompressionProvider>();
        //});

        var responseCompressionOptions = options.Value;

        _providers = responseCompressionOptions.Providers.ToArray();
        if (_providers.Length == 0)
        {
            //1.1 无参数,会默认创建BrotliCompressionProvider和GzipCompressionProvider
            // Use the factory so it can resolve IOptions<GzipCompressionProviderOptions> from DI.
            _providers = new ICompressionProvider[]
            {
                new CompressionProviderFactory(typeof(BrotliCompressionProvider)),
                new CompressionProviderFactory(typeof(GzipCompressionProvider)),
            };
        }
        //循环 通过factory创建实现ICompressionProvider的类
        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>>();
    }

    /// <inheritdoc />
    public bool CheckRequestAcceptsCompression(HttpContext context)
    {
        if (string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]))
        {
            _logger.NoAcceptEncoding();
            return false;
        }

        _logger.RequestAcceptsCompression(); // Trace, there will be more logs
        return true;
    }
}

GzipCompressionProvider代码:

public class GzipCompressionProvider : ICompressionProvider
{
    /// <summary>
    /// Creates a new instance of GzipCompressionProvider with options.
    /// </summary>
    /// <param name="options"></param>
    public GzipCompressionProvider(IOptions<GzipCompressionProviderOptions> options)
    {
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        Options = options.Value;
    }

    private GzipCompressionProviderOptions Options { get; }

    /// <inheritdoc />
    public string EncodingName { get; } = "gzip";

    /// <inheritdoc />
    public bool SupportsFlush => true;

    /// <inheritdoc />
    public Stream CreateStream(Stream outputStream)
        => new GZipStream(outputStream, Options.Level, leaveOpen: true);
}

ResponseCompressionMiddleware代码:

public class ResponseCompressionMiddleware
{
    private readonly RequestDelegate _next;

    private readonly IResponseCompressionProvider _provider;


    /// <summary>
    /// Initialize the Response Compression middleware.
    /// </summary>
    /// <param name="next">从容器获取</param>
    /// <param name="provider">是从容器获取后,注入到构造函数中</param>
    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;
    }

    /// <summary>
    /// Invoke the middleware.
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    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>();

        //实例化ResponseCompressionBody
        var compressionBody = new ResponseCompressionBody(context, _provider, originalBodyFeature);
        context.Features.Set<IHttpResponseBodyFeature>(compressionBody);
        context.Features.Set<IHttpsCompressionFeature>(compressionBody);

        try
        {            //去执行其他的中间件,最后处理完后续的请求(中间件),再去把http进行响应压缩
            await _next(context);            //完成压缩响应
            await compressionBody.FinishCompressionAsync();
        }
        finally
        {
            context.Features.Set(originalBodyFeature);
            context.Features.Set(originalCompressionFeature);
        }
    }
}
分析源码没有完全走完.因为没有编译生成的调试环境,编译是需要另外一个版本SDK,需要联网安装,一直无法下载的.等有了调试环境在分析具体如何实现的.这种环境还是需要在虚拟机进行,不让系统会产生很多垃圾文件的.
秋风 2020-08-01