试用了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 吧
功能很简单:
- webapi 从json中读取配置信息 ConsulDiscoveryOptions;
- 如果自己是一个服务, 则将自己注册到consul中并设置健康检查Url;
- ConsulDiscovery.HttpClient 内有一个consul client 定时刷新所有服务的url访问地址.
比较核心的两个类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
using Consul; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; using System.Threading; namespace ConsulDiscovery.HttpClient { public class DiscoveryClient : IDisposable { private readonly ConsulDiscoveryOptions consulDiscoveryOptions; private readonly Timer timer; private readonly ConsulClient consulClient; private readonly string serviceIdInConsul; public Dictionary< string , List< string >> AllServices { get ; private set ; } = new Dictionary< string , List< string >>(StringComparer.OrdinalIgnoreCase); public DiscoveryClient(IOptions<ConsulDiscoveryOptions> options) { consulDiscoveryOptions = options.Value; consulClient = new ConsulClient(x => x.Address = new Uri($ "http://{consulDiscoveryOptions.ConsulServerSetting.IP}:{consulDiscoveryOptions.ConsulServerSetting.Port}" )); timer = new Timer(Refresh); if (consulDiscoveryOptions.ServiceRegisterSetting != null ) { serviceIdInConsul = Guid.NewGuid().ToString(); } } public void Start() { var checkErrorMsg = CheckParams(); if (checkErrorMsg != null ) { throw new ArgumentException(checkErrorMsg); } RegisterToConsul(); timer.Change(0, consulDiscoveryOptions.ConsulServerSetting.RefreshIntervalInMilliseconds); } public void Stop() { Dispose(); } private string CheckParams() { if ( string .IsNullOrWhiteSpace(consulDiscoveryOptions.ConsulServerSetting.IP)) { return "Consul服务器地址 ConsulDiscoveryOptions.ConsulServerSetting.IP 不能为空" ; } if (consulDiscoveryOptions.ServiceRegisterSetting != null ) { var registerSetting = consulDiscoveryOptions.ServiceRegisterSetting; if ( string .IsNullOrWhiteSpace(registerSetting.ServiceName)) { return "服务名称 ConsulDiscoveryOptions.ServiceRegisterSetting.ServiceName 不能为空" ; } if ( string .IsNullOrWhiteSpace(registerSetting.ServiceIP)) { return "服务地址 ConsulDiscoveryOptions.ServiceRegisterSetting.ServiceIP 不能为空" ; } } return null ; } private void RegisterToConsul() { if ( string .IsNullOrEmpty(serviceIdInConsul)) { return ; } var registerSetting = consulDiscoveryOptions.ServiceRegisterSetting; var httpCheck = new AgentServiceCheck() { HTTP = $ "{registerSetting.ServiceScheme}{Uri.SchemeDelimiter}{registerSetting.ServiceIP}:{registerSetting.ServicePort}/{registerSetting.HealthCheckRelativeUrl.TrimStart('/')}" , Interval = TimeSpan.FromMilliseconds(registerSetting.HealthCheckIntervalInMilliseconds), Timeout = TimeSpan.FromMilliseconds(registerSetting.HealthCheckTimeOutInMilliseconds), DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(10), }; var registration = new AgentServiceRegistration() { ID = serviceIdInConsul, Name = registerSetting.ServiceName, Address = registerSetting.ServiceIP, Port = registerSetting.ServicePort, Check = httpCheck, Meta = new Dictionary< string , string >() { [ "scheme" ] = registerSetting.ServiceScheme }, }; consulClient.Agent.ServiceRegister(registration).Wait(); } private void DeregisterFromConsul() { if ( string .IsNullOrEmpty(serviceIdInConsul)) { return ; } try { consulClient.Agent.ServiceDeregister(serviceIdInConsul).Wait(); } catch { } } private void Refresh( object state) { Dictionary< string , AgentService>.ValueCollection serversInConsul; try { serversInConsul = consulClient.Agent.Services().Result.Response.Values; } catch // (Exception ex) { // 如果连接consul出错, 则不更新服务列表. 继续使用以前获取到的服务列表 // 但是如果很长时间都不能连接consul, 服务列表里的一些实例已经不可用了, 还一直提供这样旧的列表也不合理, 所以要不要在这里实现 健康检查? 这样的话, 就得把检查地址变成不能设置的 return ; } // 1. 更新服务列表 // 2. 如果这个程序提供了服务, 还要检测 服务Id 是否在服务列表里 var tempServices = new Dictionary< string , HashSet< string >>(); bool needReregisterToConsul = true ; foreach (var service in serversInConsul) { var serviceName = service.Service; if (!service.Meta.TryGetValue( "scheme" , out var serviceScheme)) { serviceScheme = Uri.UriSchemeHttp; } var serviceHost = $ "{serviceScheme}{Uri.SchemeDelimiter}{service.Address}:{service.Port}" ; if (!tempServices.TryGetValue(serviceName, out var serviceHosts)) { serviceHosts = new HashSet< string >(); tempServices[serviceName] = serviceHosts; } serviceHosts.Add(serviceHost); if (needReregisterToConsul && ! string .IsNullOrEmpty(serviceIdInConsul) && serviceIdInConsul == service.ID) { needReregisterToConsul = false ; } } if (needReregisterToConsul) { RegisterToConsul(); } var tempAllServices = new Dictionary< string , List< string >>(StringComparer.OrdinalIgnoreCase); foreach (var item in tempServices) { tempAllServices[item.Key] = item.Value.ToList(); } AllServices = tempAllServices; } public void Dispose() { DeregisterFromConsul(); consulClient.Dispose(); timer.Dispose(); } } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace ConsulDiscovery.HttpClient { public class DiscoveryHttpMessageHandler : DelegatingHandler { private static readonly Random random = new Random(( int )DateTime.Now.Ticks); private readonly DiscoveryClient discoveryClient; public DiscoveryHttpMessageHandler(DiscoveryClient discoveryClient) { this .discoveryClient = discoveryClient; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (discoveryClient.AllServices.TryGetValue(request.RequestUri.Host, out var serviceHosts)) { if (serviceHosts.Count > 0) { var index = random.Next(serviceHosts.Count); request.RequestUri = new Uri( new Uri(serviceHosts[index]), request.RequestUri.PathAndQuery); } } return await base .SendAsync(request, cancellationToken).ConfigureAwait( false ); } } } |
使用方法
为了简单, 我为新建的WebApi 增加了一个 HelloController, 提供 SayHelloService 服务, 并把自己注册到Consul.
当我们访问这个WebApi的 /WeatherForecast 时, 其Get()方法会访问 http://SayHelloService/Hello/NetCore, 这就相当于一次远程调用, 只是调用的就是这个WebApi的/Hello/NetCore
1. appsettings.json 增加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
"ConsulDiscoveryOptions" : { "ConsulServerSetting" : { "IP" : "127.0.0.1" , // 必填 "Port" : 8500, // 必填 "RefreshIntervalInMilliseconds" : 1000 }, "ServiceRegisterSetting" : { "ServiceName" : "SayHelloService" , // 必填 "ServiceIP" : "127.0.0.1" , // 必填 "ServicePort" : 5000, // 必填 "ServiceScheme" : "http" , // 只能是http 或者 https, 默认http, "HealthCheckRelativeUrl" : "/HealthCheck" , "HealthCheckIntervalInMilliseconds" : 500, "HealthCheckTimeOutInMilliseconds" : 2000 } } |
2.修改Startup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
using ConsulDiscovery.HttpClient; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; namespace WebApplication1 { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get ; } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // 注册 ConsulDiscovery 相关配置 services.AddConsulDiscovery(Configuration); // 配置 SayHelloService 的HttpClient services.AddHttpClient( "SayHelloService" , c => { c.BaseAddress = new Uri( "http://SayHelloService" ); }) .AddHttpMessageHandler<DiscoveryHttpMessageHandler>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); // 启动 ConsulDiscovery app.StartConsulDiscovery(lifetime); } } } |
3. 添加 HelloController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
using Microsoft.AspNetCore.Mvc; namespace WebApplication1.Controllers { [ApiController] [Route( "[controller]" )] public class HelloController : ControllerBase { [HttpGet] [Route( "{name}" )] public string Get( string name) { return $ "Hello {name}" ; } } } |
4. 修改WeatherForecast
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
using Microsoft.AspNetCore.Mvc; using System.Net.Http; using System.Threading.Tasks; namespace WebApplication1.Controllers { [ApiController] [Route( "[controller]" )] public class WeatherForecastController : ControllerBase { private readonly IHttpClientFactory httpClientFactory; public WeatherForecastController(IHttpClientFactory httpClientFactory) { this .httpClientFactory = httpClientFactory; } [HttpGet] public async Task< string > Get() { var httpClient = httpClientFactory.CreateClient( "SayHelloService" ); var result = await httpClient.GetStringAsync( "Hello/NetCore" ); return $ "WeatherForecast return: {result}" ; } } } |
5. 启动consul
1
|
consul agent -dev |
6. 启动 WebApplication1 并访问 http://localhost:5000/weatherforecast
以上示例可以到 https://github.com/zhouandke/ConsulDiscovery.HttpClient 下载, 请记住一定要 启动consul: consul agent -dev
End
以上就是C# HttpClient 如何使用 Consul 发现服务的详细内容,更多关于C# HttpClient使用 Consul 发现服务的资料请关注服务器之家其它相关文章!
原文链接:https://www.cnblogs.com/zhouandke/p/12957508.html