C# HttpClient 如何使用 Consul 發(fā)現(xiàn)服務(wù)
試用了Overt.Core.Grpc, 把 GRPC 的使用改造得像 WCF, 性能測試也非常不錯(cuò), 非常推薦各位使用.
但已有項(xiàng)目大多是 http 請求, 改造成 GRPC 的話, 工作量比較大, 于是又找到了 Steeltoe.Discovery, 在 Startup 給 HttpClient 添加 DelegatingHandler, 動(dòng)態(tài)改變請求url中的 host 和 port, 將http請求指向consul 發(fā)現(xiàn)的服務(wù)實(shí)例, 這樣就實(shí)現(xiàn)了服務(wù)的動(dòng)態(tài)發(fā)現(xiàn).
經(jīng)過性能測試, Steeltoe.Discovery 只有 Overt.Core.Grpc 的20%, 非常難以接受, 于是自己實(shí)現(xiàn)了一套基于 consul 的服務(wù)發(fā)現(xiàn)工具. 嗯, 名字好難取啊, 暫定為 ConsulDiscovery.HttpClient 吧
功能很簡單:
- webapi 從json中讀取配置信息 ConsulDiscoveryOptions;
- 如果自己是一個(gè)服務(wù), 則將自己注冊到consul中并設(shè)置健康檢查Url;
- ConsulDiscovery.HttpClient 內(nèi)有一個(gè)consul client 定時(shí)刷新所有服務(wù)的url訪問地址.
比較核心的兩個(gè)類
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服務(wù)器地址 ConsulDiscoveryOptions.ConsulServerSetting.IP 不能為空";
}
if (consulDiscoveryOptions.ServiceRegisterSetting != null)
{
var registerSetting = consulDiscoveryOptions.ServiceRegisterSetting;
if (string.IsNullOrWhiteSpace(registerSetting.ServiceName))
{
return "服務(wù)名稱 ConsulDiscoveryOptions.ServiceRegisterSetting.ServiceName 不能為空";
}
if (string.IsNullOrWhiteSpace(registerSetting.ServiceIP))
{
return "服務(wù)地址 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出錯(cuò), 則不更新服務(wù)列表. 繼續(xù)使用以前獲取到的服務(wù)列表
// 但是如果很長時(shí)間都不能連接consul, 服務(wù)列表里的一些實(shí)例已經(jīng)不可用了, 還一直提供這樣舊的列表也不合理, 所以要不要在這里實(shí)現(xiàn) 健康檢查? 這樣的話, 就得把檢查地址變成不能設(shè)置的
return;
}
// 1. 更新服務(wù)列表
// 2. 如果這個(gè)程序提供了服務(wù), 還要檢測 服務(wù)Id 是否在服務(wù)列表里
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();
}
}
}
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 增加了一個(gè) HelloController, 提供 SayHelloService 服務(wù), 并把自己注冊到Consul.
當(dāng)我們訪問這個(gè)WebApi的 /WeatherForecast 時(shí), 其Get()方法會(huì)訪問 http://SayHelloService/Hello/NetCore, 這就相當(dāng)于一次遠(yuǎn)程調(diào)用, 只是調(diào)用的就是這個(gè)WebApi的/Hello/NetCore
1. appsettings.json 增加
"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, 默認(rèn)http,
"HealthCheckRelativeUrl": "/HealthCheck",
"HealthCheckIntervalInMilliseconds": 500,
"HealthCheckTimeOutInMilliseconds": 2000
}
}
2.修改Startup.cs
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 相關(guān)配置
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();
});
// 啟動(dòng) ConsulDiscovery
app.StartConsulDiscovery(lifetime);
}
}
}
3. 添加 HelloController
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
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. 啟動(dòng)consul
consul agent -dev
6. 啟動(dòng) WebApplication1 并訪問 http://localhost:5000/weatherforecast

以上示例可以到 https://github.com/zhouandke/ConsulDiscovery.HttpClient 下載, 請記住一定要 啟動(dòng)consul: consul agent -dev
End
以上就是C# HttpClient 如何使用 Consul 發(fā)現(xiàn)服務(wù)的詳細(xì)內(nèi)容,更多關(guān)于C# HttpClient使用 Consul 發(fā)現(xiàn)服務(wù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#調(diào)用AForge實(shí)現(xiàn)攝像頭錄像的示例代碼
這篇文章主要介紹了C#調(diào)用AForge實(shí)現(xiàn)攝像頭錄像的示例代碼,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-09-09
.NET中的靜態(tài)與非靜態(tài)的區(qū)別分析
.NET中的靜態(tài)與非靜態(tài)的區(qū)別分析,需要的朋友可以參考一下2013-03-03
C# 用什么方法將BitConverter.ToString產(chǎn)生字符串再轉(zhuǎn)換回去
這篇文章主要介紹了C# 用什么方法將BitConverter.ToString產(chǎn)生字符串再轉(zhuǎn)換回去,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
解析c#在未出現(xiàn)異常情況下查看當(dāng)前調(diào)用堆棧的解決方法
本篇文章是對c#在未出現(xiàn)異常情況下查看當(dāng)前調(diào)用堆棧的解決方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
對WPF中的TreeView實(shí)現(xiàn)右鍵選定
這篇文章介紹了WPF實(shí)現(xiàn)右鍵選定TreeView的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
Unity3D實(shí)現(xiàn)飛機(jī)大戰(zhàn)游戲(2)
這篇文章主要為大家詳細(xì)介紹了Unity3D實(shí)現(xiàn)飛機(jī)大戰(zhàn)游戲的第二部分,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06

