最近一段时间 因公司业务需要,需要使用.net5做一套微服务的接口,使用nacos 做注册中心和配置中心,ocelot做网关。
因为ocelot 支持的是consol和eureka,如果使用nacos做服务发现,需要自己集成,以此记录下
Nacos 支持基于 DNS 和基于 RPC 的服务发现(可以作为注册中心)、动态配置服务(可以做配置中心)、动态 DNS 服务。官网地址:https://nacos.io/en-us/
ocelot 相信大家都比较熟悉,官网:https://ocelot.readthedocs.io/en/latest/index.html
环境安装:
nacos参考地址:https://blog.csdn.net/ooyhao/article/details/102744904
基于.net版本的nacos sdk:nacos-sdk-csharp-unofficial.AspNetCore (此处有坑 :Nacos.AspNetCore 已经停止更新,代码已迁移,服务注册会有问题)
SDK源码地址:https://github.com/catcherwong/nacos-sdk-csharp
配置中心:
1.在nacos添加配置
2.在.net 项目中 配置文件中 添加相关配置
1 "nacos": {
2 "ServerAddresses": [ "http://127.0.0.1:8849/" ],
3 "DefaultTimeOut": 30000,
4 "Namespace": "",
5 "ListenInterval": 30000,
6 "ServiceName": "ServiceName",
"RegisterEnabled": true,
7 "Weight": 10
8 },
9 "nacosData": {
10 "DataId": "nacosConfig",
11 "Group": "Pro"
12 }
3.在Startup.cs添加nacos sdk的DI注册
1 services.AddNacos(Configuration);
4.创建AppConfig类,定义构造函数:(从DI中获取INacosConfigClient对象)
public AppConfig(IServiceCollection _services, IConfiguration _configuration)
{
services = _services; configuration = _configuration;
var serviceProvider = services.BuildServiceProvider();
_configClient = serviceProvider.GetService<INacosConfigClient>();
}
5.添加LoadConfig方法,加载配置中心的配置
代码说明:sectionKey入参 为配置文件中的key(nacosData),responseJson为配置中心的完整配置字符串,可以是json,可以是key=value模式,根据字符串格式,转为Dictionary中,放入静态私有对象中
/// <summary>
/// 加载nacos配置中心
/// </summary>
/// <param name="sectionKey"></param>
private async Task LoadConfig(string sectionKey)
{
try
{
GetConfigRequest configRequest = configuration.GetSection(sectionKey).Get<GetConfigRequest>();
if (configRequest == null || string.IsNullOrEmpty(configRequest.DataId))
{
return;
}
var responseJson = await _configClient.GetConfigAsync(configRequest);
Console.WriteLine(responseJson);
if (string.IsNullOrEmpty(responseJson))
{
return;
}
var dic = LoadDictionary(responseJson);
if (sectionKey == commonNacosKey)
{
commonConfig = dic;
}
else
{
dicConfig = dic;
} }
catch (Exception ex)
{
throw ex;
}
}
6.添加监听:
ps:
AddListenerRequest对象为nacos sdk的监听请求对象,通过INacosConfigClient对象的AddListenerAsync方法,可以添加对nacos dataid=nacosConfig 的监听,监听的时间间隔设置可以为:ListenInterval
1 /// <summary>
2 /// 监控
3 /// </summary>
4 private void ListenerConfig()
5 {
6 AddListenerRequest request = configuration.GetSection("nacosData").Get<AddListenerRequest>();
7 request.Callbacks = new List<Action<string>>() {
8 x=>{
9 var dic = LoadDictionary(x);
10 foreach (var item in dicConfig.Keys)
11 {
12 if (dic.Keys.Any(p=>p==item))
13 {
14 if (dic[item] != dicConfig[item])
15 {
16 dicConfig[item]=dic[item].Trim();
17 }
18 }else
19 {
20 dicConfig.Remove(item);
21 }
22 }
23 foreach (var item in dic.Keys)
24 {
25 if (!dicConfig.Keys.Any(p=>p==item)){
26 dicConfig.Add(item,dic[item]);
27 }
28 }
29 }
30 };
31 var serviceProvider = services.BuildServiceProvider();
32 INacosConfigClient _configClient = serviceProvider.GetService<INacosConfigClient>();
33 _configClient.AddListenerAsync(request);
34 }
7.添加自动注入:
ps:如果需要在同一个项目中,获取多个配置,需要AppConfig对象的单列模式,这一点 自己注意下
public static IServiceCollection AddLoadConfig(this IServiceCollection _services, IConfiguration _configuration)
{
var config = new AppConfig(_services, _configuration);
config.Load();
return _services;
}
/******************************************************* 配置中心 end *************************************************************/
服务注册:
基于上面的配置信息 在自动注入中添加如下代码即可
services.AddNacosAspNetCore(conf);
启动之后 在nacos中,就可以看到此服务 如果在nacos 看到多个实列 或者 端口号和项目启动的端口号不一致,最好添加IP和port。
/***************************************************************服务注册 end***********************************************************************/
基于ocelot 的服务发现:
新建网关项目,添加ocelot和 nacos sdk 的nuget依赖
查看ocelot的官方文档就会发现,如果 能够取到nacos 中 服务的名称和服务里边可用实列的ip和port,然后把这些信息放入ocelot里边的, 就可以通过ocelot访问到这些服务接口
然后在通过自定义监听器,就可以实现想要的效果
通过上面的配置中心的配置方式,在nacos中 添加 ocelot 的模板配置
{
"Routes": [{
"DownstreamHostAndPorts": [{
"Host": "localhost",
"Port": 5000
}],
"DownstreamPathTemplate": "/{url}",
"DownstreamScheme": "http",
"UpstreamHttpMethod": ["GET", "POST", "DELETE", "PUT"],
"UpstreamPathTemplate": "/OrderServer/{url}",
"LoadBalancerOptions": {
"Type": "RoundRobin"
}
}, {
"DownstreamHostAndPorts": [{
"Host": "localhost",
"Port": 5000
}],
"DownstreamPathTemplate": "/{url}",
"DownstreamScheme": "http",
"UpstreamHttpMethod": ["GET", "POST", "DELETE", "PUT"],
"UpstreamPathTemplate": "/ProductServer/{url}"
}
],
"ServiceDiscoveryProvider": {
},
"GlobalConfiguration": {}
}
关于ocelot的Startup.cs的相关配置 这里就不赘述了,网上有很多。
这里的关键是,从nacos中拉取服务列表,然后根据ocelot的配置模板,生成需要的ocelot的配置信息,然后放入ocelot中
获取nacos中所有的服务列表
ps:通过INacosNamingClient对象的ListServicesAsync方法,获取nacos 的服务
/// <summary>
/// 获取所有服务
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="_servicesRequest"></param>
/// <returns></returns>
private async Task<List<ListInstancesResult>> GetServerListener(IServiceProvider serviceProvider, object _servicesRequest)
{ ListServicesRequest request = (ListServicesRequest)_servicesRequest;
try
{
var _namingClient = serviceProvider.GetService<INacosNamingClient>(); var res = await _namingClient.ListServicesAsync(request); List<ListInstancesResult> resultList = new List<ListInstancesResult>();
if (res.Count > 0)
{
List<Task<ListInstancesResult >> taskList = new List<Task<ListInstancesResult>>();
foreach (var item in res.Doms)
{
var taskItem = GetListInstancesResult(_namingClient,item);
taskList.Add(taskItem);
}
Task.WaitAll(taskList.ToArray());
foreach (var item in taskList)
{
resultList.Add(item.Result);
}
}
return resultList;
}
catch (Exception ex)
{ LoggerLocal.Error(ex.Message, ex);
return new List<ListInstancesResult>();
}
}
将nacos的服务和配置中心的ocelot模板转换为ocelot的配置对象
/// <summary>
/// nacos中的服务 转为ocelot对象的路由
/// </summary>
/// <param name="fileConfiguration"></param>
/// <param name="listInstancesResults"></param>
/// <returns></returns>
private FileConfiguration InstancesResultToFileConfiguration(FileConfigurationTemplate fileConfiguration, List<ListInstancesResult> listInstancesResults) { if (fileConfiguration.RouteTemplate == null || fileConfiguration.RouteTemplate.Count == 0)
{
throw new Exception("路由不能为空");
}
var result = new FileConfiguration() {
GlobalConfiguration = fileConfiguration.GlobalConfiguration,
Aggregates = fileConfiguration.Aggregates,
DynamicRoutes = fileConfiguration.DynamicRoutes,
Routes = new List<FileRoute>()
};
nacosServerModelList.ServerInfo = new List<ServerInfo>();
var routeList = fileConfiguration.RouteTemplate;
var defaultRoute = fileConfiguration.RouteTemplate.Find(p=>p.RoutesTemplateName=="common");
fileConfiguration.Routes = new List<FileRoute>();
foreach (var item in listInstancesResults)
{
var routeTemp = routeList.Find(p => p.ServiceName.ToLower() == item.Dom.ToLower());
if (routeTemp == null)
{
routeTemp = defaultRoute;
}
var newRouteTmp = CopyTo(routeTemp);
newRouteTmp.UpstreamPathTemplate = "/" + item.Dom + "/{url}";
newRouteTmp.DownstreamPathTemplate = "/{url}";
newRouteTmp.DownstreamHostAndPorts = new List<FileHostAndPort>();
if (item.Hosts.Count > 0)
{
foreach (var host in item.Hosts)
{
newRouteTmp.DownstreamHostAndPorts.Add(new FileHostAndPort()
{
Host = host.Ip,
Port = host.Port,
});
}
}
if (newRouteTmp.DownstreamHostAndPorts.Count > 0)
{
result.Routes.Add(newRouteTmp);
nacosServerModelList.ServerInfo.Add(new ServerInfo() { Name = item.Dom });
}
} UpdSwaggerUrlAction(serviceProvider, nacosServerModelList);
return result;
} private FileRoute CopyTo(RouteTemplate s)
{
var result = new FileRoute() {
AddClaimsToRequest=s.AddClaimsToRequest,
DangerousAcceptAnyServerCertificateValidator=s.DangerousAcceptAnyServerCertificateValidator,
DelegatingHandlers=s.DelegatingHandlers,
DownstreamHeaderTransform=s.DownstreamHeaderTransform,
DownstreamHostAndPorts=s.DownstreamHostAndPorts,
DownstreamHttpMethod=s.DownstreamHttpMethod,
DownstreamHttpVersion=s.DownstreamHttpVersion,
DownstreamPathTemplate=s.DownstreamPathTemplate,
SecurityOptions=s.SecurityOptions,
DownstreamScheme=s.DownstreamScheme,
ChangeDownstreamPathTemplate=s.ChangeDownstreamPathTemplate,
AddHeadersToRequest=s.AddHeadersToRequest,
AddQueriesToRequest=s.AddQueriesToRequest,
AuthenticationOptions=s.AuthenticationOptions,
FileCacheOptions=s.FileCacheOptions,
HttpHandlerOptions=s.HttpHandlerOptions,
Key=s.Key,
LoadBalancerOptions=s.LoadBalancerOptions,
Priority=s.Priority,
QoSOptions=s.QoSOptions,
RateLimitOptions=s.RateLimitOptions,
RequestIdKey=s.RequestIdKey,
RouteClaimsRequirement=s.RouteClaimsRequirement,
RouteIsCaseSensitive=s.RouteIsCaseSensitive,
ServiceName=s.ServiceName,
ServiceNamespace=s.ServiceNamespace,
Timeout=s.Timeout,
UpstreamHeaderTransform=s.UpstreamHeaderTransform,
UpstreamHost=s.UpstreamHost,
UpstreamHttpMethod=s.UpstreamHttpMethod,
UpstreamPathTemplate=s.UpstreamPathTemplate, };
return result;
}
将配置信息放入ocelot里边
ps:这个地方 需要看ocelot的源码,才知道这中间的对象转换逻辑
1 private void SetOcelotConfig(FileConfiguration configuration)
2 {
3
4 var internalConfigCreator = serviceProvider.GetService<IInternalConfigurationCreator>();
5 Task<Response<IInternalConfiguration>> taskResponse = internalConfigCreator.Create(configuration);
6 taskResponse.Wait();
7 IInternalConfigurationRepository internalConfigurationRepository = serviceProvider.GetService<IInternalConfigurationRepository>();
8 internalConfigurationRepository.AddOrReplace(taskResponse.Result.Data);
9 }
自定义监听器:
ps:isLoadUri 防止处理过慢,监听服务 多次监听
routesMd5:判断监听到的服务 是否需要放入ocelot
自定义监听的方式与nacos sdk中,监听配置中心的方式类似,有兴趣可以看看sdk的源码
/// <summary>
/// 获取nacos里边的所有服务信息,同时自定义服务监听
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
private async Task<List<ListInstancesResult>> GetServerList(IServiceProvider serviceProvider)
{
var request = new ListServicesRequest
{
PageNo = 1,
PageSize = 100,
};
List<ListInstancesResult> listInstancesResults = await GetServerListener(serviceProvider, request);
//return listInstancesResults;
var timeSeconds = 1000 * 10;
Timer timer = new Timer(async x =>
{
//防止重复Timer
if (isLoadUri)
{
return;
}
isLoadUri = true;
List<ListInstancesResult> listInstancesList = await GetServerListener(serviceProvider, x);
GetConfigRequest configRequest = configuration.GetSection("nacosData").Get<GetConfigRequest>(); INacosConfigClient _configClient = serviceProvider.GetService<INacosConfigClient>();
Task<string> taskResult = _configClient.GetConfigAsync(configRequest);
taskResult.Wait();
var responseJson = taskResult.Result;
if (listInstancesList.Count>0)
{
var fileConfiguration = InstancesResultToFileConfiguration(JsonConvert.DeserializeObject<FileConfigurationTemplate>(responseJson), listInstancesList);
responseJson = JsonConvert.SerializeObject(fileConfiguration);
var rMd5 = HashUtil.GetMd5(responseJson);
if (!rMd5.Equals(routesMd5))
{
SetOcelotConfig(fileConfiguration);
routesMd5 = rMd5;
}
}
isLoadUri = false;
}, request, timeSeconds, timeSeconds);
timers.Add(timer);
return listInstancesResults;
}