.NETCore远程调用
HttpClient
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}"); } } } }
netstat -ano windows下查看当前所有的tcp连接
netstat -ano |findstr "8080" windows下查看所有8080端口的tcp连接
netstat -ano |findstr "TIME_WAIT" windows下查看所有的“TIME_WAIT”状态的tcp连接
netstat -ano |find /i /c "TIME_WAIT" windows下统计time_wait出现的次数(按行统计) /i 忽略大小写
虽然项目已经运行结束,但是连接依然存在,状态为" 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,那么一些公共的设置就没办法灵活的调整了,如请求头的自定义。
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。
从微软源码分析,HttpClient继承自HttpMessageInvoker,而HttpMessageInvoker实质就是HttpClientHandler。
HttpClientFactory 创建的HttpClient,也即是HttpClientHandler,只是这些个HttpClient被放到了“池子”中,工厂每次在create的时候会自动判断是新建还是复用。(默认生命周期为2min)
还理解不了的话,可以参考Task和Thread的关系,以前碰到HttpClient这个问题的时候,就一直在想微软什么时候官方出一个HttpClient的Factory,虽然时隔了这么多年直到.NET CORE 2.1才出,但也很是兴奋。
一、ASP.NET CORE MVC
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"); }); }
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>(); }); }