.NETCore远程调用

HttpClient
HttpClient这个对象有点特殊,虽然继承了IDisposable接口,但它是可以被共享的(或者说可以被复用),且线程安全。从项目经验来看,倒是建议在整个应用的生命周期内,复用HttpClient实例,而不是每次RPC请求的时候就实例化一个。
    class Program
    {
        static void Main(string[] args)
        {
            HttpAsync();
            Console.WriteLine("Hello World!");
            Console.Read();
        }

        public static async void HttpAsync()
        {
            for (int i = 0; i < 10; i++)
            {
                using (var client = new HttpClient())
                {
                    var result = await client.GetAsync("http://www.baidu.com");
                    Console.WriteLine($"{i}:{result.StatusCode}");
                }
            }
        }
    }

 

.NETCore远程调用

 

 

虽然项目已经运行结束,但是连接依然存在,状态为" TIME_WAIT"(继续等待看是否还有延迟的包会传输过来。),240秒(4分钟)后才真正关闭连接。对于高并发的场景,比如每秒 1000 个请求,每个请求都用到 HttpClient ,4分钟内会堆积24万个 tcp 连接,这样的连接爆棚会拖垮服务器。为了避开这个坑,通常采用的变通方法是使用静态的 HttpClient ,但会带来另外一个臭名还没昭著的问题,当 HttpClient 请求的主机名对应的 IP 地址变更时,HttpClient 会蒙在鼓里毫不知情,除非重启应用程序。

默认在windows下,TIME_WAIT状态将会使系统将会保持该连接 240s。

这里也就引出了我上面说的载过的一次坑:在高并发的情况下,连接来不及释放,socket被耗尽,耗尽之后就会出现喜闻乐见的一个错误:


解决办法复用HttpClient

.NETCore远程调用

 

 10个变成一个。

好处:

1、可以看到,原先10个连接变成了1个连接。(请不要在意两次示例的目标IP不同---SLB(负载均衡)导致的,都是百度的ip)

2、另外,因为复用了HttpClient,每次RPC请求的时候,实际上还节约了创建通道的时间,在性能压测的时候也是很明显的提升。曾经因为这一举动,将web项目的TPS(系统吞吐量)从单台600瞬间提升到了2000+,页面请求时间也从1-3s减少至100-300ms,甚是让测试组小伙伴膜拜(当然也包括了一些业务代码的细调。),但知道个中缘由后,一个小改动带来的项目性能提升。。。会让人上瘾:)

3、至于如何创建一个静态HttpClient进行复用,大家可以按项目实际来,如直接创建一个“全局”静态对象,或者通过各类DI框架来创建均可。

坏处:
1、因为是复用的HttpClient,那么一些公共的设置就没办法灵活的调整了,如请求头的自定义。
2、因为HttpClient请求每个url时,会缓存该url对应的主机ip,从而会导致DNS更新失效(TTL失效了)
那么有没有办法解决HttpClient的这些个问题?直到我遇到了 HttpClientFactory,瞬间写代码幸福感倍升,也感慨新时代的童鞋们真的太幸福了,老一辈踩的坑可以“完美”规避掉了。
 
一些用法:
        public static async void HttpMul2Async()
        {
            //https://docs.microsoft.com/zh-cn/dotnet/api/system.net.http.httprequestmessage?redirectedfrom=MSDN&view=netframework-4.7.2
            var request = new HttpRequestMessage(HttpMethod.Get, "www.baidu.com");
            //request.RequestUri
            //request.Headers.Accept;
            //request.Headers.
            //HttpRequestHeaders hrh = new HttpRequestHeaders();
            //request.Method  
            //StreamContent sc = new StreamContent();
            MultipartFormDataContent mfdc = new MultipartFormDataContent();
            //mfdc.Add
            // mfdc.Headers

            //request.Content = 
            await _client.SendAsync(request);
            for (int i = 0; i < 10; i++)
            {
                var result = await _client.GetAsync("http://www.baidu.com");
                Console.WriteLine($"{i}:{result.StatusCode}");
            }
        }

 

HttpClientFactory初识

介绍:

ASP.NET CORE 2.1中新增加的功能。安装包  Microsoft.Extensions.Http 

HttpClientFacotry很高效,可以最大程度上节省系统socket。

Factory,顾名思义HttpClientFactory就是HttpClient的工厂,内部已经帮我们处理好了对HttpClient的管理,不需要我们人工进行对象释放,同时,支持自定义请求头,支持DNS更新等等等。

从微软源码分析,HttpClient继承自HttpMessageInvoker,而HttpMessageInvoker实质就是HttpClientHandler。

HttpClientFactory 创建的HttpClient,也即是HttpClientHandler,只是这些个HttpClient被放到了“池子”中,工厂每次在create的时候会自动判断是新建还是复用。(默认生命周期为2min)

还理解不了的话,可以参考Task和Thread的关系,以前碰到HttpClient这个问题的时候,就一直在想微软什么时候官方出一个HttpClient的Factory,虽然时隔了这么多年直到.NET CORE 2.1才出,但也很是兴奋。

推荐文章

使用:
一、ASP.NET CORE MVC
1、注册httpclient
 public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //other codes
            
            services.AddHttpClient("client_1",config=>  //这里指定的name=client_1,可以方便我们后期服用该实例
            {
                config.BaseAddress= new Uri("http://client_1.com");
                config.DefaultRequestHeaders.Add("header_1","header_1");
            });
            services.AddHttpClient("client_2",config=>
            {
                config.BaseAddress= new Uri("http://client_2.com");
                config.DefaultRequestHeaders.Add("header_2","header_2");
            }).SetHandlerLifetime(TimeSpan.FromMinutes(5));;
            services.AddHttpClient();

            //other codes
            services.AddMvc().AddFluentValidation();
        }
      }

2、使用

    public class TestController : ControllerBase
    {
        private readonly IHttpClientFactory _httpClient;
        public TestController(IHttpClientFactory httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<ActionResult> Test()
        {
            var client = _httpClient.CreateClient("client_1"); //复用在Startup中定义的client_1的httpclient
            var result = await client.GetStringAsync("/page1.html");

            var client2 = _httpClient.CreateClient(); //新建一个HttpClient
            var result2 = await client.GetStringAsync("http://www.site.com/XXX.html");

            return null;
        }
    }

 

二、自定义请求类

1、定义http请求类

public class SampleClient
{
    public HttpClient Client { get; private set; }
    
    public SampleClient(HttpClient httpClient)
    {
        httpClient.BaseAddress = new Uri("https://api.SampleClient.com/");
        httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
        httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
        Client = httpClient;
    }
}

2、注入

services.AddHttpClient<SampleClient>();

3、调用

public class ValuesController : Controller
{
    private readonly SampleClient  _sampleClient;;
  
    public ValuesController(SampleClient  sampleClient)
    {
        _sampleClient = sampleClient;
    }
  
    [HttpGet]
    public async Task<ActionResult> Get()
    {
        string result = await  _sampleClient.client.GetStringAsync("/");
        return Ok(result);
    }
}

三、自定义请求 接口  实现

1、定义请求接口,请求类

public interface ISampleClient
{
    Task<string> GetData();
}
 
public class SampleClient : ISampleClient
{
    private readonly HttpClient _client;
 
    public SampleClient(HttpClient httpClient)
    {
        httpClient.BaseAddress = new Uri("https://api.SampleClient.com/");
        httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
        httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
        _client = httpClient;
    }
 
    public async Task<string> GetData()
    {
        return await _client.GetStringAsync("/");
    }
}

设置了BaseAddress ,请求地址的时候,可以直接写想对地址。

2、注入

services.AddHttpClient<ISampleClient, SampleClient>();

3、调用

public class ValuesController : Controller
{
    private readonly ISampleClient  _sampleClient;;
     
    public ValuesController(ISampleClient  sampleClient)
    {
        _sampleClient = sampleClient;
    }
     
    [HttpGet]
    public async Task<ActionResult> Get()
    {
        string result = await _sampleClient.GetData();
        return Ok(result);
    }
}

 

HttpClientFactory进阶

核心功能:

• 管理内部HttpMessageHandler 的生命周期(管理socket链接),灵活应对资源问题和DNS刷新问题

• 支持命名化、类型化配置,集中管理配置,避免冲突

• 灵活的出站请求管道配置,轻松管理请求生命周期

• 内置管道最外层和最内层日志记录器,有Information 和Trace 输出

核心对象:

• HttpClient

• HttpMessageHandler

• SocketsHttpHandler

• DelegatingHandler

• IHttpClientFactory

• IHttpClientBuilde

管道模型:

.NETCore远程调用

 

 

类似于中间件的设计模式。中间的 DelegatingHandler 就是中间件处理,里面内置中间件LoggingScopeHttp MesageHandler  位于做外层, 记录日志用。最内层的LoggingHttp MessageHandler 记录内层日志。中间是可以自定义的中间件。

SocketsHttpHandler才是真正请求的地方。

用户自定义管道

 

.NETCore远程调用
    public class RequestIdDelegatingHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            //处理请求
            request.Headers.Add("x-guid", Guid.NewGuid().ToString());

            var result = await base.SendAsync(request, cancellationToken); //调用内部handler

            //处理响应

            return result;
       
请求管道定义

 

.NETCore远程调用
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().AddControllersAsServices();

            services.AddHttpClient();
            services.AddScoped<OrderServiceClient>();
            services.AddSingleton<RequestIdDelegatingHandler>();
            services.AddHttpClient("NamedOrderServiceClient", client =>
            {
                client.DefaultRequestHeaders.Add("client-name", "namedclient");
                client.BaseAddress = new Uri("https://localhost:5003");
            }).SetHandlerLifetime(TimeSpan.FromMinutes(20))
            .AddHttpMessageHandler(provider => provider.GetService<RequestIdDelegatingHandler>());
            

            services.AddScoped<NamedOrderServiceClient>();
            services.AddHttpClient<TypedOrderServiceClient>(client =>
            {
                client.BaseAddress = new Uri("https://localhost:5003");
            });

        }
注册自定义请求管道

.AddHttpMessageHandler(provider => provider.GetService<RequestIdDelegatingHandler>());

 

 

创建HttpClient

1• 工厂模式

    public class OrderServiceClient
    {
        IHttpClientFactory _httpClientFactory;

        public OrderServiceClient(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }


        public async Task<string> Get()
        {
            var client = _httpClientFactory.CreateClient();

            //使用client发起HTTP请求
            return await client.GetStringAsync("https://localhost:5003/OrderService");
        }
    }
_httpClientFactory.CreateClient();

 

2• 命名客户端模式

.NETCore远程调用
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().AddControllersAsServices();

            services.AddHttpClient();
            services.AddScoped<OrderServiceClient>();
            services.AddSingleton<RequestIdDelegatingHandler>();
            services.AddHttpClient("NamedOrderServiceClient", client =>
            {
                client.DefaultRequestHeaders.Add("client-name", "namedclient");
                client.BaseAddress = new Uri("https://localhost:5003");
            }).SetHandlerLifetime(TimeSpan.FromMinutes(20))
            .AddHttpMessageHandler(provider => provider.GetService<RequestIdDelegatingHandler>());
            

            services.AddScoped<NamedOrderServiceClient>();
            services.AddHttpClient<TypedOrderServiceClient>(client =>
            {
                client.BaseAddress = new Uri("https://localhost:5003");
            });

        }
View Code

services.AddHttpClient("NamedOrderServiceClient",

第一个参数是名字,第二个参数是默认的配置。

获取httpclient的地方

_httpClientFactory.CreateClient("名字");

好处:命名客户端,可以为不同的服务配置不同的客户端,不同的客户端有不同而http默认配置,大家的socket都是各自管理。

3• 类型化客户端模式(建议做法)

本质和命名客户端一样,唯一区别以名称作为httpclient的名称。  好处不需要名字  这个字符串去获取。

客户端定义

    public class TypedOrderServiceClient
    {
        HttpClient _client;
        public TypedOrderServiceClient(HttpClient client)
        {
            _client = client;
        }


        public async Task<string> Get()
        {
           return  await _client.GetStringAsync("/OrderService"); //这里使用相对路径来访问
        }
    }

服务注入  

            services.AddHttpClient<TypedOrderServiceClient>(client =>
            {
                client.BaseAddress = new Uri("https://localhost:5003");
            });

 

Grpc

推荐文章:

https://baijiahao.baidu.com/s?id=1633335936037018920&wfr=spider&for=pc

 

.NETCore远程调用

上一篇:net core 3.1 MVC如何返回JSONP


下一篇:jquery: siblings使用删除当前样式,添加所有siblings样式