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 吧
功能很简单:
- webapi 从json中读取配置信息 ConsulDiscoveryOptions;
- 如果自己是一个服务, 则将自己注册到consul中并设置健康检查Url;
- ConsulDiscovery.HttpClient 内有一个consul client 定时刷新所有服务的url访问地址.
比较核心的两个类
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(, 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(),
};
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();
}
}
}
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 > )
{
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 增加
"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
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
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. 启动consul
consul agent -dev
6. 启动 WebApplication1 并访问 http://localhost:5000/weatherforecast

以上示例可以到 https://github.com/zhouandke/ConsulDiscovery.HttpClient 下载, 请记住一定要 启动consul: consul agent -dev
End
C# HttpClient 使用 Consul 发现服务的更多相关文章
- .NET Core HttpClient+Consul实现服务发现
简介 随着.NET Core的不断发展与成熟,基于.NET Core实现微服务的解决方案也越来越多.这其中必然需要注册中心,Consul成为了.NET Core实现服务注册与发现的首选.类似的解决方案 ...
- .net core grpc consul 实现服务注册 服务发现 负载均衡(二)
在上一篇 .net core grpc 实现通信(一) 中,我们实现的grpc通信在.net core中的可行性,但要在微服务中真正使用,还缺少 服务注册,服务发现及负载均衡等,本篇我们将在 .net ...
- .netcore consul实现服务注册与发现-集群完整版
原文:.netcore consul实现服务注册与发现-集群完整版 一.Consul的集群介绍 Consul Agent有两种运行模式:Server和Client.这里的Server和Clien ...
- .netcore consul实现服务注册与发现-单节点部署
原文:.netcore consul实现服务注册与发现-单节点部署 一.Consul的基础介绍 Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置.与其他分 ...
- .Net Core Grpc Consul 实现服务注册 服务发现 负载均衡
本文是基于..net core grpc consul 实现服务注册 服务发现 负载均衡(二)的,很多内容是直接复制过来的,..net core grpc consul 实现服务注册 服务发现 负载均 ...
- .NET Core HttpClientFactory+Consul实现服务发现
前言 上篇文章.NET Core HttpClient+Consul实现服务发现提到过,HttpClient存在套接字延迟释放的问题,高并发情况导致端口号被耗尽引起服务器拒绝服务的问题.好在微软意识到 ...
- 一个故事,一段代码告诉你如何使用不同语言(Golang&C#)提供相同的能力基于Consul做服务注册与发现
目录 引言 什么是微服务 传统服务 微服务 什么是服务注册与服务发现 为什么要使用不同的语言提供相同的服务能力 服务协调器 服务注册 Golang C#(.NetCore3.1) 服务发现 通过Htt ...
- 简单RPC框架-基于Consul的服务注册与发现
*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...
- Spring Cloud Consul 实现服务注册和发现
Spring Cloud 是一个基于 Spring Boot 实现的云应用开发工具,它为基于 JVM 的云应用开发中涉及的配置管理.服务发现.断路器.智能路由.微代理.控制总线.全局锁.决策竞选.分布 ...
随机推荐
- 基于 react 的Java web 应用—— 组件复用(后续需更新)
前言 实习第二周,被告知要用React与导师进行基于React的Javaweb 的开发,jinzhangaaaaa~由于React 这款框架没学过,看了一峰老师的基础入门教程,硬着头皮开始上了... ...
- 两种方法直接删除数组中特定值的项(JavaScript)
一.问题详情: 直接删除意为原数组需要被改变,而不是得到另一个数组. 二.JavaScript实现 (一)巧用数组的push( ).shift( )方法 function del(arr,num) { ...
- ACM算法--二分法--模板
// 在单调递增序列a中查找>=x的数中最小的一个(即x或x的后继) while (l < r) { int mid = (l + r) / 2; if (a[mid] >= x) ...
- C++编程入门题目--No.5
题目: 输入三个整数x,y,z,请把这三个数由小到大输出. 程序分析: 我们想办法把最小的数放到x上,先将x与y进行比较,如果x>y则将x与y的值进行交换, 然后再用x与z进行比较,如果x> ...
- lintcode 826电脑维修
826,一个n * m矩阵代表一个电脑的阵列,给你一个list< Point >代表坏掉的电脑坐标.现在我们从(0,0)出发修电脑,要求: 1.必须修完当前行所有坏掉的电脑才能走向下一 ...
- centos下的redis一键安装shell脚本
#!/bin/bash yum install cpp binutils glibc-kernheaders glibc-common glibc-devel gcc make wget #安装依赖库 ...
- OpenCV 4下darknet修改
darknet的安装使用直接在官网上获取.https://pjreddie.com/darknet/ 但我用的是OpenCV4.1.1,make时会在image_opencv.cpp中有两个错误. 1 ...
- 王颖奇 201771010129 第三周 Java基本程序设计总结
实验三 Java基本程序设计 实验时间 2018-9-13 201771010129 王颖奇 1.实验目的与要求 (1)进一步掌握Eclipse集成开发环境下java程序开发基本步骤: (2)熟悉PT ...
- 【Scala】关于集合的各种知识点
目录 映射Map 不可变Map 概述 操作实例 可变Map 概述 操作实例 Map的遍历 for循环遍历 格式 操作实例 模式匹配遍历 格式 操作实例 Tuple 元祖 概述 定义格式 获取元素方法 ...
- Kubernetes笔记(三):Gitlab+Jenkins Pipeline+Docker+k8s+Helm自动化部署实践(干货分享!)
通过前面两篇文章,我们已经有了一个"嗷嗷待哺"的K8s集群环境,也对相关的概念与组件有了一个基本了解(前期对概念有个印象即可,因为只有实践了才能对其有深入理解,所谓"纸上 ...