.net5+nacos+ocelot 配置中心和服务发现实现
最近一段时间 因公司业务需要,需要使用.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 }else19 {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; }
View Code
将配置信息放入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; }