zoukankan      html  css  js  c++  java
  • .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}");
                    }
                }
            }
        }

    虽然项目已经运行结束,但是连接依然存在,状态为" 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;
           
    请求管道定义
            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• 命名客户端模式

            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

    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;
    }
    proto定义
        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="Protosgreet.proto" GrpcServices="Client" />
        <Protobuf Include="Protosorder.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="Protosgreet.proto" GrpcServices="Client" />
        <Protobuf Include="Protosorder.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>();
                });
            }
  • 相关阅读:
    函数模板
    可以接收数量不定的参数的函数
    Poco库的学习
    分割字符串,字符串去除空格
    初始化时添加click方法
    【Mybatis】传Map与传String
    jsp全局变量
    【oracle】Oracle中字符串连接的实现方法【转】
    斜杠(右斜杠)【/】 与 反斜杠(右斜杠)【】
    【Java】@ResponseBody 返回JsonObject
  • 原文地址:https://www.cnblogs.com/wudequn/p/13149990.html
Copyright © 2011-2022 走看看