.Net Core HttpClient处理响应压缩

前言

    在上篇文章[ASP.NET Core中的响应压缩]中咱们谈到了在ASP.NET Core服务端处理关于响应压缩的请求,服务端的主要工做就是根据Content-Encoding头信息判断采用哪一种方式压缩并返回。以前在群里有人问道过,如今的网络带宽这么高了还有必要在服务端针对请求进行压缩吗?确实,现在分布式和负载均衡技术这么成熟,不少须要处理高并发大数据的场景均可以经过增长服务器节点来进行。可是,在资源受限的状况下,或者是还不必为了某一个点去增长新的服务器节点的时候,咱们仍是要采用一些程序自己的常规处理手段来进行处理。笔者我的认为响应压缩的使用场景是这样的,在带宽压力比较紧张的状况,且CPU资源比较充足的状况下,使用响应压缩总体效果仍是比较明显的。
    有压缩就有解压,而解压的工做就是在请求客户端处理的。好比浏览器,这是咱们最经常使用的Http客户端,许多浏览器都是默认在咱们发出请求的时候(好比咱们浏览网页的时候)在Request Head中添加Content-Encoding,而后根据响应信息处理相关解压。这些都源于浏览器已经内置了关于请求压缩和解压的机制。相似的还有许多,好比经常使用的代理抓包工具Filder也是内置这种机制的。只不过须要手动去处理,但实现方式都是同样的。有时候咱们在本身写程序的过程当中也须要使用这种机制,在传统的.Net HttpWebRequest类库中,并无这种机制,后来版本中加入了HttpClient,有自带的机制能够处理这种操做,.Net Core做为后起之秀直接将HttpClient扶正,而且在此基础上改良了HttpClientFactory,接下来咱们就来探究一下在.Net Core中使用HttpClient处理响应压缩的机制。html

使用方式

首先咱们来看一下直接在HttpClient中如何处理响应压缩git

//自定义HttpClientHandler实例
HttpClientHandler httpClientHandler = new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip
};
//使用传递自定义HttpClientHandler实例的构造函数
using (HttpClient client = new HttpClient(httpClientHandler))
{
    var response = await client.GetAsync($"http://MyDemo/Home/GetPerson?userId={userId}");
}

这个操做仍是很是简单的,咱们操做的并非HttpClient的属性而是HttpClientHandler中的属性,咱们在以前的文章[.NET Core HttpClient源码探究]中曾探讨过,HttpClient的本质其实就是HttpMessageHandler,而HttpClient真正使用到的是HttpMessageHandler最重要的一个子类HttpClientHandler,全部的请求操做都是经过HttpMessageHandler进行的。咱们能够看到AutomaticDecompression接受的是DecompressionMethods枚举,既然是枚举就说明包含了不止一个值,接下来咱们查看DecompressionMethods中的源码github

[Flags]
public enum DecompressionMethods
{
    // 使用全部压缩解压缩算法。
    All = -1,
    // 不使用解压
    None = 0x0,
    // 使用gzip解压算法
    GZip = 0x1,
    // 使用deflate解压算法
    Deflate = 0x2,
    // 使用Brotli解压算法
    Brotli = 0x4
}

该枚举默认都是针对经常使用输出解压算法,接下来咱们看一下在HttpClientFactory中如何处理响应压缩。在以前的文章[.NET Core HttpClientFactory+Consul实现服务发现]中咱们曾探讨过HttpClientFactory的大体工做方式默认PrimaryHandler传递的就是HttpClientHandler实例,并且在咱们注册HttpClientFactory的时候是能够经过ConfigurePrimaryHttpMessageHandler自定义PrimaryHandler的默认值,接下来咱们具体代码实现算法

services.AddHttpClient("mydemo", c =>
{
    c.BaseAddress = new Uri("http://MyDemo/");
}).ConfigurePrimaryHttpMessageHandler(provider=> new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip
});

其实在注册HttpClientFactory的时候还可使用自定义的HttpClient,具体的使用方式是这样的浏览器

services.AddHttpClient("mydemo", c =>
{
    c.BaseAddress = new Uri("http://MyDemo/");
}).ConfigureHttpClient(provider => new HttpClient(new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip
}));

HttpClient确实帮咱们作了好多事情,只须要简单的配置一下就开启了针对响应压缩的处理。这更勾起了咱们对HttpClient的探讨,接下来咱们就经过源码的方式查看它是如何发起可响应压缩请求,并解压响应结果的。服务器

源码探究

经过上面的使用方式咱们得知,不管使用哪一种形式,最终都是针对HttpClientHandler作配置操做,接下来咱们查看HttpClientHandler类[点击查看源码👈]中AutomaticDecompression属性的代码网络

public DecompressionMethods AutomaticDecompression
{
    get => _underlyingHandler.AutomaticDecompression;
    set => _underlyingHandler.AutomaticDecompression = value;
}

它自己的值操做来自_underlyingHandler这个对象,也就是说读取和设置都是在操做_underlyingHandler.AutomaticDecompression,咱们查找到_underlyingHandler对象的声明位置并发

private readonly SocketsHttpHandler _underlyingHandler;

这里说明一下,HttpClient的实质工做类是HttpClientHandler,而HttpClientHandler真正发起请求是依靠的SocketsHttpHandler这个类,也就是说SocketsHttpHandler是最原始发起请求的类。HttpClientHandler本质仍是经过SocketsHttpHandler发起的Http请求,接下来咱们就查看SocketsHttpHandler类[点击查看源码👈]是如何处理AutomaticDecompression这个属性的负载均衡

public DecompressionMethods AutomaticDecompression
{
    get => _settings._automaticDecompression;
    set
    {
        CheckDisposedOrStarted();
        _settings._automaticDecompression = value;
    }
}

这里的_settings再也不是具体的功能类,而是用于初始化或者保存SocketsHttpHandler的部分属性值的配置类async

private readonly HttpConnectionSettings _settings = new HttpConnectionSettings();

这里咱们不在分析SocketsHttpHandler出处理响应压缩以外的其余代码,因此具体就再也不看这些了,直接查找_settings._automaticDecompression属性引用的地方,最终找到了这段代码

if (settings._automaticDecompression != DecompressionMethods.None)
{
    handler = new DecompressionHandler(settings._automaticDecompression, handler);
}

这里就比较清晰了,真正处理请求响应压缩相关的都是在DecompressionHandler中。正如咱们以前所说的,HttpClient真正的工做方式就是一些实现自HttpMessageHandler的子类在工做,它把不一样功能的实现模块都封装成了具体的Handler中。当你须要使用哪一个模块的功能,直接使用对应的Handler操做类去发送处理请求便可。这种设计思路在ASP.NET Core中体现的也是淋漓尽致,ASP.NET Core采用的是构建不一样终结点去处理和输出请求。经过这些咱们能够得知DecompressionHandler才是今天的主题,接下来咱们就来查看DecompressionHandler类的源码[点击查看源码👈]就不粘贴所有源码了,咱们先来看最核心的SendAsync方法,这个方法是发送请求的执行方法

internal override async ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
{
    //判断是不是GZIP压缩请求,若是是则添加请求头Accept-Encoding头为gzip
    if (GZipEnabled && !request.Headers.AcceptEncoding.Contains(s_gzipHeaderValue))
    {
        request.Headers.AcceptEncoding.Add(s_gzipHeaderValue);
    }
    //判断是不是Deflate压缩请求,若是是则添加请求头Accept-Encoding头为deflate
    if (DeflateEnabled && !request.Headers.AcceptEncoding.Contains(s_deflateHeaderValue))
    {
        request.Headers.AcceptEncoding.Add(s_deflateHeaderValue);
    }
    //判断是不是Brotli压缩请求,若是是则添加请求头Accept-Encoding头为brotli
    if (BrotliEnabled && !request.Headers.AcceptEncoding.Contains(s_brotliHeaderValue))
    {
        request.Headers.AcceptEncoding.Add(s_brotliHeaderValue);
    }
    //发送请求
    HttpResponseMessage response = await _innerHandler.SendAsync(request, async, cancellationToken).ConfigureAwait(false);

    Debug.Assert(response.Content != null);
    //获取返回的Content-Encoding输出头信息
    ICollection<string> contentEncodings = response.Content.Headers.ContentEncoding;
    if (contentEncodings.Count > 0)
    {
        string? last = null;
        //获取最后一个值
        foreach (string encoding in contentEncodings)
        {
            last = encoding;
        }
        //根据响应头判断服务端采用的是否为gzip压缩
        if (GZipEnabled && last == Gzip)
        {
            //使用gzip解压算法解压返回内容,并重新赋值到response.Content
            response.Content = new GZipDecompressedContent(response.Content);
        }
        //根据响应头判断服务端采用的是否为deflate压缩
        else if (DeflateEnabled && last == Deflate)
        {
            //使用deflate解压算法解压返回内容,并重新赋值到response.Content
            response.Content = new DeflateDecompressedContent(response.Content);
        }
        //根据响应头判断服务端采用的是否为brotli压缩
        else if (BrotliEnabled && last == Brotli)
        {
            //使用brotli解压算法解压返回内容,并重新赋值到response.Content
            response.Content = new BrotliDecompressedContent(response.Content);
        }
    }
    return response;
}

经过上面的逻辑咱们能够看到GZipEnabled、DeflateEnabled、BrotliEnabled三个bool类型的变量,中三个变量决定了采用哪一种请求压缩方式,主要实现方式是

internal bool GZipEnabled => (_decompressionMethods & DecompressionMethods.GZip) != 0;
internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0;
internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0;

主要就是根据咱们配置的DecompressionMethods枚举值判断想获取哪一种方式的压缩结果,解压的实现逻辑都封装在GZipDecompressedContent、DeflateDecompressedContent、BrotliDecompressedContent中,咱们看一下他们的具体的代码

private sealed class GZipDecompressedContent : DecompressedContent
{
        public GZipDecompressedContent(HttpContent originalContent)
            : base(originalContent)
        { }
        //使用GZipStream类对返回的流进行解压
        protected override Stream GetDecompressedStream(Stream originalStream) =>
            new GZipStream(originalStream, CompressionMode.Decompress);
    }

    private sealed class DeflateDecompressedContent : DecompressedContent
    {
        public DeflateDecompressedContent(HttpContent originalContent)
            : base(originalContent)
        { }
        //使用DeflateStream类对返回的流进行解压
        protected override Stream GetDecompressedStream(Stream originalStream) =>
            new DeflateStream(originalStream, CompressionMode.Decompress);
    }

    private sealed class BrotliDecompressedContent : DecompressedContent
    {
        public BrotliDecompressedContent(HttpContent originalContent) :
            base(originalContent)
        { }
        //使用BrotliStream类对返回的流进行解压
        protected override Stream GetDecompressedStream(Stream originalStream) =>
            new BrotliStream(originalStream, CompressionMode.Decompress);
    }
}

    其主要的工做方式就是使用对应压缩算法的解压方法获得原始信息。简单总结一下,HttpClient关于压缩相关的处理机制是,首先根据你配置的DecompressionMethods判断你想使用那种压缩算法。而后匹配到对应的压缩算法后添加Accept-Encoding请求头为你指望的压缩算法。最后根据响应结果获取Content-Encoding输出头信息,判断服务端采用的是哪一种压缩算法,并采用对应的解压方法解压获取原始数据。

总结

    经过本次探讨HttpClient关于响应压缩的处理咱们能够了解到,HttpClient不管从设计上仍是实现方式上都有很是高的灵活性和扩展性,这也是为何到了.Net Core上官方只推荐使用HttpClient一种Http请求方式。因为使用比较简单,实现方式比较清晰,这里就不过多拗述。主要是是想告诉你们HttpClient默承认以直接处理响应压缩,而不是和以前咱们使用HttpWebRequest的时候还须要手动编码的方式去实现。

👇欢迎扫码关注个人公众号👇
相关文章
相关标签/搜索