最近一段時間 因公司業務需要,需要使用.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; }