.NETCore远程调用

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}");
                }
            }
        }
    }
复制代码

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

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

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


解决办法复用HttpClient

 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

管道模型:

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

SocketsHttpHandler才是真正请求的地方。

用户自定义管道

复制代码
    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;
       
复制代码
 注册自定义请求管道

.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• 命名客户端模式

复制代码
        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");
            });

        }
复制代码

services.AddHttpClient("NamedOrderServiceClient",

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

获取httpclient的地方

_httpClientFactory.CreateClient("名字");

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

3• 类型化客户端模式(建议做法  为不同的客户端提供不同的host  生命周期  管道等等)

本质和命名客户端一样,唯一区别以名称作为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

Grpc

什么是Grpc:

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

远程调用框架

google公司发起并开源的

Grpc特点:

• 提供几乎所有主流语言的实现,打破语言隔阂

客户端可以用一种语言,服务端可以用一种语言

• 基于HTTP/2 ,开放协议,受到广泛的支持,易于实现和集成

• 默认使用Protocol Buffers 序列化,性能相较于RESTful Json 好很多

• 工具链成熟,代码生成便捷,开箱即用

• 支持双向流式的请求和响应,对批量处理、低延时场景友好

感觉吧soap那一套体验那过来了

.NET的Grpc支持情况

• 提供基于HttpClient 的原生框架实现

• 提供原生的ASP.NET Core 集成库

• 提供完整的代码生成工具

• Visual Studio 和Visual StuidoCode 提供proto 文件的智能提示

.NET服务端引用包

Grpc.AspNetCore

.NET客户端引用包

• Google.Protobuf

序列化协议的包

• Grpc.Net.Client

客户端包

• Grpc.Net.ClientFactory

引入httpclientfactory

• Grpc.Tools

提供命令行工具的包,用来基于.proto文件生成我们的客户端以及服务端代码

.proto 文件

• 定义包、库名

• 定义服务“service”

• 定义输入输出模型“message"

这个文件可以生成服务端代码和客户端代码

gRPC异常处理

• 使用Grpc.Core.RpcException

• 使用Grpc.Core.Interceptors.Interceptor

gRPC与HTTPS证书

• 使用自制证书

• 使用非加密的HTTP2

proto文件介绍

.proto文件

复制代码
syntax = "proto3";

option csharp_namespace = "GrpcServices";

package GrpcServices;

service OrderGrpc {
    rpc CreateOrder(CreateOrderCommand) returns (CreateOrderResult);
}


message CreateOrderCommand {
    string buyerId = 1;
    int32 productId = 2;
    double unitPrice = 3;
    double discount = 4;
    int32 units = 5;
}

message CreateOrderResult {
    int32 orderId = 1;
}
复制代码

syntax = "proto3";

用的是proto3协议

option csharp_namespace = "GrpcServices";

表示命名空间是  GrpcServices

package GrpcServices;

包 定义一个作用域,用来防止不同的消息类型有命名冲突。就行程序集名字一样

 

service OrderGrpc {
rpc CreateOrder(CreateOrderCommand) returns (CreateOrderResult);
}

定义一个服务名字OrderGrpc,服务有一个方法叫做CreateOrder

message CreateOrderCommand {
string buyerId = 1;
int32 productId = 2;
double unitPrice = 3;
double discount = 4;
int32 units = 5;
}

message CreateOrderResult {
int32 orderId = 1;
}

数据模型都叫message,里面的1  2   3  4  是字段序列化顺序,也是通过这个匹配字段名字的。

-------------------------------------------------------------------------------------------------------------------------------------------

当我们完之后一个.proto之后,他会自动帮我们生成对应代码。 大家可以看生成的代码和这个配置文件比对一下。

https://www.jianshu.com/p/da7ed5914088

https://www.cnblogs.com/tohxyblog/p/8974763.html

服务端定义

复制代码
syntax = "proto3";

option csharp_namespace = "GrpcServer";

package orderser;

service OrderGrpc {
    rpc CreateOrder(CreateOrderCommand) returns (CreateOrderResult);
}


message CreateOrderCommand {
    string buyerId = 1;
    int32 productId = 2;
    double unitPrice = 3;
    double discount = 4;
    int32 units = 5;
}

message CreateOrderResult {
    int32 orderId = 1;
}
复制代码
复制代码
    public class OrderService : OrderGrpc.OrderGrpcBase
    {
        private readonly ILogger<GreeterService> _logger;
        public OrderService(ILogger<GreeterService> logger)
        {
            _logger = logger;
        }

        public override Task<CreateOrderResult> CreateOrder(CreateOrderCommand request, ServerCallContext context)
        {
            throw new System.Exception("order error");

            //添加创建订单的内部逻辑,录入将订单信息存储到数据库
            return Task.FromResult(new CreateOrderResult { OrderId = 24 });
        }
    }
复制代码
复制代码
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc(
                option => {
                    //生产环境关掉
                    option.EnableDetailedErrors = false;
                    //异常拦截器
                    option.Interceptors.Add<ExceptionInterceptor>();
                }
                );
        }
复制代码
复制代码
            app.UseEndpoints(endpoints =>
            {
                //终结点map
                endpoints.MapGrpcService<GreeterService>();
                endpoints.MapGrpcService<OrderService>();

                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
                });
            });
复制代码

 项目下载

客户端定义

 客户端属于调用方,客户端属于任何程序都可以。主要是要把gRPC的代码生成,像调用本地方法一样调用gRPC。(客户端和服务端的语言可以不一样)

控制台调用

引用包Google.Protobuf           Grpc.Net.Client           Grpc.Tools

 项目下载

添加grpc配置文件,可以从服务端复制过来。设置项目对该文件的应用

  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
    <Protobuf Include="Protos\order.proto" GrpcServices="Client" />
  </ItemGroup>

调用

复制代码
        static async Task Main(string[] args)
        {
            var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = new Greeter.GreeterClient(channel);
            var reply = await client.SayHelloAsync(new HelloRequest { Name = "西伯利亚的狼" });
            Console.WriteLine("Greeter 服务返回数据: " + reply.Message);
            Console.ReadKey();


            Console.WriteLine("Hello World!");
        }
复制代码

WEB项目调用

 

 项目下载

引入包   Google.Protobuf           Grpc.Net.Client           Grpc.Tools       Grpc.Net.ClientFactory          Grpc.AspNetCore

添加grpc配置文件,可以从服务端复制过来。设置项目对该文件的应用

  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
    <Protobuf Include="Protos\order.proto" GrpcServices="Client" />
  </ItemGroup>

服务注入

像addhttpclient一样addgrpcclient

            services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(options =>
            {
                options.Address = new Uri("https://localhost:5001");
            });

grpc是使用http2的,http2是基于https的。那么我们怎么改成http,这个就可以不配置i证书,就可以使用grpc。

复制代码
            AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); //允许使用不加密的HTTP/2协议
            services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(options =>
            {
                options.Address = new Uri("http://localhost:5002");
            })
复制代码

还有子签名证书的处理

控制器调用

复制代码
    [Route("api/[controller]")]
    [ApiController]
    public class GRPCClientController : ControllerBase
    {
        private readonly OrderGrpcClient _orderclient;
        public GRPCClientController(OrderGrpcClient orderclient)
        {
            _orderclient = orderclient;
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            var r = _orderclient.CreateOrder(new CreateOrderCommand { BuyerId = "abc" });
            return Ok(r.OrderId);
        }
    }
复制代码

 服务端异常

复制代码
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc(options =>
            {
                options.EnableDetailedErrors = true;
                options.Interceptors.Add<ExceptionInterceptor>();
            });
        }
复制代码
 
分类: .NET Core
原文地址:https://www.cnblogs.com/bruce1992/p/15610171.html