C# HttpClient 使用 Consul 发现服务

  试用了Overt.Core.Grpc, 把 GRPC 的使用改造得像 WCF, 性能测试也非常不错, 非常推荐各位使用.
  但已有项目大多是 http 请求, 改造成 GRPC 的话, 工作量比较大, 于是又找到了 Steeltoe.Discovery, 在 Startup 给 HttpClient 添加 DelegatingHandler, 动态改变请求url中的 host 和 port, 将http请求指向consul 发现的服务实例, 这样就实现了服务的动态发现.
  经过性能测试, Steeltoe.Discovery 只有 Overt.Core.Grpc 的20%, 非常难以接受, 于是自己实现了一套基于 consul 的服务发现工具. 嗯, 名字好难取啊, 暂定为 ConsulDiscovery.HttpClient 吧
  功能很简单:

  1. webapi 从json中读取配置信息 ConsulDiscoveryOptions;
  2. 如果自己是一个服务, 则将自己注册到consul中并设置健康检查Url;
  3. ConsulDiscovery.HttpClient 内有一个consul client 定时刷新所有服务的url访问地址.

  比较核心的两个类

1using Consul; 2using Microsoft.Extensions.Options; 3using System; 4using System.Collections.Generic; 5using System.Linq; 6using System.Threading; 7 8namespace ConsulDiscovery.HttpClient 9{ 10 public class DiscoveryClient : IDisposable 11 { 12 private readonly ConsulDiscoveryOptions consulDiscoveryOptions; 13 private readonly Timer timer; 14 private readonly ConsulClient consulClient; 15 private readonly string serviceIdInConsul; 16 17 public Dictionary<string, List<string>> AllServices { get; private set; } = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); 18 19 20 public DiscoveryClient(IOptions<ConsulDiscoveryOptions> options) 21 { 22 consulDiscoveryOptions = options.Value; 23 consulClient = new ConsulClient(x => x.Address = new Uri($"http://{consulDiscoveryOptions.ConsulServerSetting.IP}:{consulDiscoveryOptions.ConsulServerSetting.Port}")); 24 timer = new Timer(Refresh); 25 26 if (consulDiscoveryOptions.ServiceRegisterSetting != null) 27 { 28 serviceIdInConsul = Guid.NewGuid().ToString(); 29 } 30 } 31 32 public void Start() 33 { 34 var checkErrorMsg = CheckParams(); 35 if (checkErrorMsg != null) 36 { 37 throw new ArgumentException(checkErrorMsg); 38 } 39 RegisterToConsul(); 40 timer.Change(0, consulDiscoveryOptions.ConsulServerSetting.RefreshIntervalInMilliseconds); 41 } 42 43 public void Stop() 44 { 45 Dispose(); 46 } 47 48 private string CheckParams() 49 { 50 if (string.IsNullOrWhiteSpace(consulDiscoveryOptions.ConsulServerSetting.IP)) 51 { 52 return "Consul服务器地址 ConsulDiscoveryOptions.ConsulServerSetting.IP 不能为空"; 53 } 54 55 if (consulDiscoveryOptions.ServiceRegisterSetting != null) 56 { 57 var registerSetting = consulDiscoveryOptions.ServiceRegisterSetting; 58 if (string.IsNullOrWhiteSpace(registerSetting.ServiceName)) 59 { 60 return "服务名称 ConsulDiscoveryOptions.ServiceRegisterSetting.ServiceName 不能为空"; 61 } 62 if (string.IsNullOrWhiteSpace(registerSetting.ServiceIP)) 63 { 64 return "服务地址 ConsulDiscoveryOptions.ServiceRegisterSetting.ServiceIP 不能为空"; 65 } 66 } 67 return null; 68 } 69 70 private void RegisterToConsul() 71 { 72 if (string.IsNullOrEmpty(serviceIdInConsul)) 73 { 74 return; 75 } 76 77 var registerSetting = consulDiscoveryOptions.ServiceRegisterSetting; 78 var httpCheck = new AgentServiceCheck() 79 { 80 HTTP = $"{registerSetting.ServiceScheme}{Uri.SchemeDelimiter}{registerSetting.ServiceIP}:{registerSetting.ServicePort}/{registerSetting.HealthCheckRelativeUrl.TrimStart('/')}", 81 Interval = TimeSpan.FromMilliseconds(registerSetting.HealthCheckIntervalInMilliseconds), 82 Timeout = TimeSpan.FromMilliseconds(registerSetting.HealthCheckTimeOutInMilliseconds), 83 DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(10), 84 }; 85 var registration = new AgentServiceRegistration() 86 { 87 ID = serviceIdInConsul, 88 Name = registerSetting.ServiceName, 89 Address = registerSetting.ServiceIP, 90 Port = registerSetting.ServicePort, 91 Check = httpCheck, 92 Meta = new Dictionary<string, string>() { ["scheme"] = registerSetting.ServiceScheme }, 93 }; 94 consulClient.Agent.ServiceRegister(registration).Wait(); 95 } 96 97 private void DeregisterFromConsul() 98 { 99 if (string.IsNullOrEmpty(serviceIdInConsul)) 100 { 101 return; 102 } 103 try 104 { 105 consulClient.Agent.ServiceDeregister(serviceIdInConsul).Wait(); 106 } 107 catch 108 { } 109 } 110 111 private void Refresh(object state) 112 { 113 Dictionary<string, AgentService>.ValueCollection serversInConsul; 114 try 115 { 116 serversInConsul = consulClient.Agent.Services().Result.Response.Values; 117 } 118 catch // (Exception ex) 119 { 120 // 如果连接consul出错, 则不更新服务列表. 继续使用以前获取到的服务列表 121 // 但是如果很长时间都不能连接consul, 服务列表里的一些实例已经不可用了, 还一直提供这样旧的列表也不合理, 所以要不要在这里实现 健康检查? 这样的话, 就得把检查地址变成不能设置的 122 return; 123 } 124 125 // 1. 更新服务列表 126 // 2. 如果这个程序提供了服务, 还要检测 服务Id 是否在服务列表里 127 var tempServices = new Dictionary<string, HashSet<string>>(); 128 bool needReregisterToConsul = true; 129 foreach (var service in serversInConsul) 130 { 131 var serviceName = service.Service; 132 if (!service.Meta.TryGetValue("scheme", out var serviceScheme)) 133 { 134 serviceScheme = Uri.UriSchemeHttp; 135 } 136 var serviceHost = $"{serviceScheme}{Uri.SchemeDelimiter}{service.Address}:{service.Port}"; 137 if (!tempServices.TryGetValue(serviceName, out var serviceHosts)) 138 { 139 serviceHosts = new HashSet<string>(); 140 tempServices[serviceName] = serviceHosts; 141 } 142 serviceHosts.Add(serviceHost); 143 144 if (needReregisterToConsul && !string.IsNullOrEmpty(serviceIdInConsul) && serviceIdInConsul == service.ID) 145 { 146 needReregisterToConsul = false; 147 } 148 } 149 150 if (needReregisterToConsul) 151 { 152 RegisterToConsul(); 153 } 154 155 var tempAllServices = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); 156 foreach (var item in tempServices) 157 { 158 tempAllServices[item.Key] = item.Value.ToList(); 159 } 160 AllServices = tempAllServices; 161 } 162 163 164 public void Dispose() 165 { 166 DeregisterFromConsul(); 167 consulClient.Dispose(); 168 timer.Dispose(); 169 } 170 } 171}

View Code

1using System; 2using System.Net.Http; 3using System.Threading; 4using System.Threading.Tasks; 5 6namespace ConsulDiscovery.HttpClient 7{ 8 public class DiscoveryHttpMessageHandler : DelegatingHandler 9 { 10 private static readonly Random random = new Random((int)DateTime.Now.Ticks); 11 12 private readonly DiscoveryClient discoveryClient; 13 14 public DiscoveryHttpMessageHandler(DiscoveryClient discoveryClient) 15 { 16 this.discoveryClient = discoveryClient; 17 } 18 19 protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 20 { 21 if (discoveryClient.AllServices.TryGetValue(request.RequestUri.Host, out var serviceHosts)) 22 { 23 if (serviceHosts.Count > 0) 24 { 25 var index = random.Next(serviceHosts.Count); 26 request.RequestUri = new Uri(new Uri(serviceHosts[index]), request.RequestUri.PathAndQuery); 27 } 28 } 29 return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); 30 } 31 } 32}

View Code

  使用方法

  为了简单, 我为新建的WebApi 增加了一个 HelloController, 提供 SayHelloService 服务, 并把自己注册到Consul.

  当我们访问这个WebApi的 /WeatherForecast 时, 其Get()方法会访问 http://SayHelloService/Hello/NetCore, 这就相当于一次远程调用, 只是调用的就是这个WebApi的/Hello/NetCore

  1. appsettings.json 增加

1"ConsulDiscoveryOptions": { 2 "ConsulServerSetting": { 3 "IP": "127.0.0.1", // 必填 4 "Port": 8500, // 必填 5 "RefreshIntervalInMilliseconds": 1000 6 }, 7 "ServiceRegisterSetting": { 8 "ServiceName": "SayHelloService", // 必填 9 "ServiceIP": "127.0.0.1", // 必填 10 "ServicePort": 5000, // 必填 11 "ServiceScheme": "http", // 只能是http 或者 https, 默认http, 12 "HealthCheckRelativeUrl": "/HealthCheck", 13 "HealthCheckIntervalInMilliseconds": 500, 14 "HealthCheckTimeOutInMilliseconds": 2000 15 } 16 }

  2.修改Startup.cs

1using ConsulDiscovery.HttpClient; 2using Microsoft.AspNetCore.Builder; 3using Microsoft.AspNetCore.Hosting; 4using Microsoft.Extensions.Configuration; 5using Microsoft.Extensions.DependencyInjection; 6using Microsoft.Extensions.Hosting; 7using System; 8 9namespace WebApplication1 10{ 11 public class Startup 12 { 13 public Startup(IConfiguration configuration) 14 { 15 Configuration = configuration; 16 } 17 18 public IConfiguration Configuration { get; } 19 20 public void ConfigureServices(IServiceCollection services) 21 { 22 services.AddControllers(); 23 24 // 注册 ConsulDiscovery 相关配置 25 services.AddConsulDiscovery(Configuration); 26 // 配置 SayHelloService 的HttpClient 27 services.AddHttpClient("SayHelloService", c => 28 { 29 c.BaseAddress = new Uri("http://SayHelloService"); 30 }) 31 .AddHttpMessageHandler<DiscoveryHttpMessageHandler>(); 32 } 33 34 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime) 35 { 36 if (env.IsDevelopment()) 37 { 38 app.UseDeveloperExceptionPage(); 39 } 40 41 app.UseRouting(); 42 43 app.UseAuthorization(); 44 45 app.UseEndpoints(endpoints => 46 { 47 endpoints.MapControllers(); 48 }); 49 50 // 启动 ConsulDiscovery 51 app.StartConsulDiscovery(lifetime); 52 } 53 } 54}

  3. 添加 HelloController

1using Microsoft.AspNetCore.Mvc; 2 3namespace WebApplication1.Controllers 4{ 5 [ApiController] 6 [Route("[controller]")] 7 public class HelloController : ControllerBase 8 { 9 [HttpGet] 10 [Route("{name}")] 11 public string Get(string name) 12 { 13 return $"Hello {name}"; 14 } 15 } 16}

  4. 修改WeatherForecast

1using Microsoft.AspNetCore.Mvc; 2using System.Net.Http; 3using System.Threading.Tasks; 4 5namespace WebApplication1.Controllers 6{ 7 [ApiController] 8 [Route("[controller]")] 9 public class WeatherForecastController : ControllerBase 10 { 11 private readonly IHttpClientFactory httpClientFactory; 12 13 public WeatherForecastController(IHttpClientFactory httpClientFactory) 14 { 15 this.httpClientFactory = httpClientFactory; 16 } 17 18 [HttpGet] 19 public async Task<string> Get() 20 { 21 var httpClient = httpClientFactory.CreateClient("SayHelloService"); 22 var result = await httpClient.GetStringAsync("Hello/NetCore"); 23 return $"WeatherForecast return: {result}"; 24 } 25 } 26}

  5. 启动consul

consul agent -dev

  6. 启动 WebApplication1 并访问 http://localhost:5000/weatherforecast

  以上示例可以到 https://github.com/zhouandke/ConsulDiscovery.HttpClient 下载, 请记住一定要 启动consul: consul agent -dev

  End

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )