.Net Core Grpc Consul 實現服務註冊 服務發現 負載均衡

  本文是基於..net core grpc consul 實現服務註冊 服務發現 負載均衡(二)的,不少內容是直接複製過來的,..net core grpc consul 實現服務註冊 服務發現 負載均衡(二)的版權屬於原做者,此文的版權歸屬我及@蝸牛丨大神,所以,轉載前請必要聲明@蝸牛丨大神及本人。謝謝。html

 

文章內容以下:node

 

在上一篇 .net core grpc 實現通訊(一) 中,咱們實現的grpc通訊在.net core中的可行性,但要在微服務中真正使用,還缺乏 服務註冊,服務發現及負載均衡等,本篇咱們將在 .net core grpc 通訊 的基礎上加上 服務註冊,服務發現,負載均衡。git

如對.net core grpc 通訊不太熟悉的,能夠看上一篇 .net core grpc 實現通訊(一) ,而後再看本篇。github

grpc(https://grpc.io/)是google發佈的一個開源、高性能、通用RPC(Remote Procedure Call)框架,使用HTTP/2協議,支持多路複用,並用ProtoBuf做爲序列化工具,提供跨語言、跨平臺支持。web

Consul(https://www.consul.io)是一個分佈式,高可用、支持多數據中心的服務註冊、發現、健康檢查和配置共享的服務軟件,由 HashiCorp 公司用 Go 語言開發。算法

本次服務註冊、發現 經過 Consul Api 來實現,開發過程當中結合.net core 依賴注入,切面管道思想等。json

 

軟件版本bootstrap

.net core:3.0api

grpc:1.20.1瀏覽器

Consul:1.5.0

Consul Nuget註冊組件:0.7.2.6

 

項目結構

.net core 代碼部分:

Snai.GrpcClient 客戶端 .net core 3.0控制檯程序

Snai.GrpcService.Hosting 服務端宿主,Api服務註冊,asp.net core 3.0網站程序

Snai.GrpcService.Impl 協議方法實現  .net standard 2.0類庫

Snai.GrpcService.Protocol 生成協議方法 .net standard 2.0類庫

Consul:(個人Windows x64版本下載下來壓縮包裏面只有一個consul.exe文件,可能原做者使用的是Linux版本或當時環境下的版本致使)

conf 配置目錄,本次用api註冊服務,能夠刪除

data 緩存數據目錄,可清空裏面內容

dist Consul UI目錄,本次用默認的UI,能夠刪除

consul.exe 註冊軟件

startup.bat 執行腳本

 

項目實現

 1、服務端

服務端主要包括Grpc服務端,Consul Api服務註冊、健康檢查等。

新建Snai.GrpcService解決方案,因爲此次加入了 Consul Api 服務註冊,因此咱們先從 Api 服務註冊開始。

一、實現 Consul Api 服務註冊

新建 Snai.GrpcService.Hosting 基於Asp.net Core 3.0空網站,在 依賴項 右擊 管理NuGet程序包 瀏覽 找到 Consul 版本0.7.2.6安裝,用於Api服務註冊使用

編輯 appsettings.json 配置文件,配置 GrpcService Grpc服務端IP和端口,HealthService健康檢測名稱、IP和地址,ConsulService Consul的IP和端口,代碼以下

 1 {
 2   "GrpcService": {
 3     "IP": "localhost",
 4     "Port": "5031"
 5   },
 6   "HealthService": {
 7     "Name": "GrpcService",
 8     "IP": "localhost",
 9     "Port": "5021"
10   },
11   "ConsulService": {
12     "IP": "localhost",
13     "Port": "8500"
14   },
15   "Logging": {
16     "LogLevel": {
17       "Default": "Information",
18       "Microsoft": "Warning",
19       "Microsoft.Hosting.Lifetime": "Information"
20     }
21   },
22   "AllowedHosts": "*"
23 }

新建Consul目錄,用於放Api註冊相關代碼

在Consul目錄下新建Entity目錄,在Entity目錄下新建HealthService.cs,ConsulService.cs類,分別對應HealthService,ConsulService兩個配置項,代碼以下

HealthService.cs

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Threading.Tasks;
 5 
 6 namespace Snai.GrpcService.Hosting.Consul.Entity
 7 {
 8     public class HealthService
 9     {
10         public string Name { get; set; }
11         public string IP { get; set; }
12         public int Port { get; set; }
13 
14     }
15 }

ConsulService.cs

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Threading.Tasks;
 5 
 6 namespace Snai.GrpcService.Hosting.Consul.Entity
 7 {
 8     public class ConsulService
 9     {
10         public string IP { get; set; }
11         public int Port { get; set; }
12     }
13 }

在 Consul 目錄下新建 AppRregister.cs 類,添加 IApplicationBuilder 擴展方法 RegisterConsul,來調用 Consul Api 實現服務註冊,代碼以下

 1 using Consul;
 2 using Microsoft.AspNetCore.Builder;
 3 using Microsoft.Extensions.Hosting;
 4 using Microsoft.Extensions.Options;
 5 using Snai.GrpcService.Hosting.Consul.Entity;
 6 using System;
 7 using System.Collections.Generic;
 8 using System.Linq;
 9 using System.Threading.Tasks;
10 
11 namespace Snai.GrpcService.Hosting.Consul
12 {
13     public static class AppRregister
14     {
15         // 服務註冊
16         public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IHostApplicationLifetime lifetime, IOptions<HealthService> healthService, IOptions<ConsulService> consulService)
17         {
18             var consulClient = new ConsulClient(cfg => cfg.Address = new Uri($"http://{consulService.Value.IP}:{consulService.Value.Port}"));//請求註冊的 Consul 地址
19             var httpCheck = new AgentServiceCheck()
20             {
21                 DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服務啓動多久後註冊
22                 Interval = TimeSpan.FromSeconds(10),//健康檢查時間間隔,或者稱爲心跳間隔
23                 HTTP = $"http://{healthService.Value.IP}:{healthService.Value.Port}/health",//健康檢查地址
24                 Timeout = TimeSpan.FromSeconds(5)
25             };
26 
27             // Register service with consul
28             var registration = new AgentServiceRegistration()
29             {
30                 Checks = new[] { httpCheck },
31                 ID = healthService.Value.Name + "_" + healthService.Value.Port,
32                 Name = healthService.Value.Name,
33                 Address = healthService.Value.IP,
34                 Port = healthService.Value.Port,
35                 Tags = new[] { $"urlprefix-/{healthService.Value.Name}" }//添加 urlprefix-/servicename 格式的 tag 標籤,以便 Fabio 識別
36             };
37 
38             consulClient.Agent.ServiceRegister(registration).Wait();//服務啓動時註冊,內部實現其實就是使用 Consul API 進行註冊(HttpClient發起)
39             lifetime.ApplicationStopping.Register(() =>
40             {
41                 consulClient.Agent.ServiceDeregister(registration.ID).Wait();//服務中止時取消註冊
42             });
43 
44             return app;
45         }
46     }
47 }

修改 Startup.cs 代碼

加入 Startup(IConfiguration configuration) 構造函數,實現配置注入,若是建的是Web Api或MVC網站,默認是有的

修改 ConfigureServices(IServiceCollection services)  方法,註冊全局配置

修改 Configure() 方法,添加健康檢查路由地址 app.Map("/health", HealthMap),調用 RegisterConsul 擴展方法實現服務註冊

添加 HealthMap(IApplicationBuilder app) 實現health路由。因爲只有一個健康檢查地址,因此沒有建Web Api網站,只建了個空網站

代碼以下,註冊配置GrpcService 、 註冊Rpc服務、啓動Rpc服務 後面用到等下講

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Threading.Tasks;
 5 using Microsoft.AspNetCore.Builder;
 6 using Microsoft.AspNetCore.Hosting;
 7 using Microsoft.AspNetCore.Http;
 8 using Microsoft.AspNetCore.HttpsPolicy;
 9 using Microsoft.Extensions.Configuration;
10 using Microsoft.Extensions.DependencyInjection;
11 using Microsoft.Extensions.Hosting;
12 using Microsoft.Extensions.Options;
13 using Snai.GrpcService.Hosting.Consul;
14 using Snai.GrpcService.Hosting.Consul.Entity;
15 
16 namespace Snai.GrpcService.Hosting
17 {
18     public class Startup
19     {
20         public Startup(IConfiguration configuration)
21         {
22             Configuration = configuration;
23         }
24 
25         public IConfiguration Configuration { get; }
26 
27         // This method gets called by the runtime. Use this method to add services to the container.
28         public void ConfigureServices(IServiceCollection services)
29         {
30             //註冊全局配置
31             services.AddOptions();
32             services.Configure<Impl.Entity.GrpcService>(Configuration.GetSection(nameof(Impl.Entity.GrpcService)));
33             services.Configure<HealthService>(Configuration.GetSection(nameof(HealthService)));
34             services.Configure<ConsulService>(Configuration.GetSection(nameof(ConsulService)));
35 
36             //註冊Rpc服務
37             services.AddSingleton<IRpcConfig, RpcConfig>();
38         }
39 
40         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
41         public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime, IOptions<HealthService> healthService, IOptions<ConsulService> consulService, IRpcConfig rpc)
42         {
43             if (env.IsDevelopment())
44             {
45                 app.UseDeveloperExceptionPage();
46             }
47 
48             // 添加健康檢查路由地址
49             app.Map("/health", HealthMap);
50 
51             // 服務註冊
52             app.RegisterConsul(lifetime, healthService, consulService);
53 
54             // 啓動Rpc服務
55             rpc.Start();
56         }
57 
58         private static void HealthMap(IApplicationBuilder app)
59         {
60             app.Run(async context =>
61             {
62                 await context.Response.WriteAsync("OK");
63             });
64         }
65     }
66 }

修改 Program.cs 代碼,調置網站地址爲 .UseUrls("http://localhost:5021"),代碼以下

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Threading.Tasks;
 5 using Microsoft.AspNetCore.Hosting;
 6 using Microsoft.Extensions.Configuration;
 7 using Microsoft.Extensions.Hosting;
 8 using Microsoft.Extensions.Logging;
 9 
10 namespace Snai.GrpcService.Hosting
11 {
12     public class Program
13     {
14         public static void Main(string[] args)
15         {
16             CreateHostBuilder(args).Build().Run();
17         }
18 
19         public static IHostBuilder CreateHostBuilder(string[] args) =>
20             Host.CreateDefaultBuilder(args)
21                 .ConfigureWebHostDefaults(webBuilder =>
22                 {
23                     webBuilder.UseUrls("http://localhost:5021");
24                     webBuilder.UseStartup<Startup>();
25                 });
26     }
27 }

到此 Consul Api 服務註冊 已完成,最終項目結構以下:

 

二、協議編寫,將協議生成C#代碼

新建 Snai.GrpcService.Protocol協議類庫項目,在 依賴項 右擊 管理NuGet程序包 瀏覽 找到 Grpc.Core 版本1.20.1,Google.Protobuf 版本3.7.0,Grpc.Tools 版本1.20.1 包下載安裝

在項目根目錄下新建一個 Protos文件夾並新建 msg.proto 文件,編寫基於proto3語言的協議代碼,用於生成各語言協議,msg.proto 代碼以下

syntax = "proto3";

package Snai.GrpcService.Protocol;

service MsgService{
  rpc GetSum(GetMsgNumRequest) returns (GetMsgSumReply){}
}

message GetMsgNumRequest {
  int32 Num1 = 1;
  int32 Num2 = 2;
}

message GetMsgSumReply {
  int32 Sum = 1;
}

編寫csproj文件使項目自動生成C#代碼,添加以下節點。

1   <ItemGroup>
2     <Protobuf Include="Protos/*.proto" OutputDir="%(RelativeDir)" CompileOutputs="false" />
3   </ItemGroup>

點擊從新生成後,項目生成了C#代碼,代碼結構以下:

 三、編寫協議實現代碼

新建 Snai.GrpcService.Impl 實現類庫項目,在 依賴項 下載安裝Grpc.Core 包,項目引用 Snai.GrpcService.Protocol

新建 Entity 目錄,在Entity目錄下新建 GrpcService.cs 類,對應 Snai.GrpcService.Hosting 項目下 appsettings.json 配置文件的 GrpcService 配置項,代碼以下

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Text;
 4 
 5 namespace Snai.GrpcService.Impl.Entity
 6 {
 7     public class GrpcService
 8     {
 9         public string IP { get; set; }
10         public int Port { get; set; }
11     }
12 }

在根目錄下新建 RpcService 目錄,在 RpcService 目錄下新建 MsgServiceImpl.cs 類,繼承 MsgService.MsgServiceBase 協議類,實現服務方法,代碼以下

 1 using Grpc.Core;
 2 using Snai.GrpcService.Protocol;
 3 using System;
 4 using System.Collections.Generic;
 5 using System.Text;
 6 using System.Threading.Tasks;
 7 
 8 namespace Snai.GrpcService.Impl.RpcService
 9 {
10     public class MsgServiceImpl:MsgService.MsgServiceBase
11     {
12         public override Task<GetMsgSumReply> GetSum(GetMsgNumRequest request, ServerCallContext context)
13         {
14             var result = new GetMsgSumReply();
15 
16             result.Sum = request.Num1 + request.Num2;
17 
18             Console.WriteLine(request.Num1 + "+" + request.Num2 + "=" + result.Sum);
19 
20             return Task.FromResult(result);
21         }
22     }
23 }

在根目錄下新建IRpcConfig.cs接口,定義 Start() 用於Rpc啓動基方法,代碼以下

1 using System;
2 
3 namespace Snai.GrpcService.Impl
4 {
5     public interface IRpcConfig
6     {
7         void Start();
8     }
9 }

在根目錄下新建 RpcConfig.cs 類,用於實現 IRpcConfig.cs 接口,啓動Rpc服務,代碼以下

 1 using Grpc.Core;
 2 using Microsoft.Extensions.Options;
 3 using Snai.GrpcService.Impl.RpcService;
 4 using Snai.GrpcService.Protocol;
 5 using System;
 6 using System.Collections.Generic;
 7 using System.Text;
 8 
 9 namespace Snai.GrpcService.Impl
10 {
11     public class RpcConfig : IRpcConfig
12     {
13         private static Server _server;
14         private static IOptions<Entity.GrpcService> _grpcSettings;
15 
16         public RpcConfig(IOptions<Entity.GrpcService> grpcSettings)
17         {
18             _grpcSettings = grpcSettings;
19         }
20         public void Start()
21         {
22             _server = new Server
23             {
24                 Services = { MsgService.BindService(new MsgServiceImpl()) },
25                 Ports = { new ServerPort(_grpcSettings.Value.IP, _grpcSettings.Value.Port, ServerCredentials.Insecure) }
26             };
27             _server.Start();
28 
29             Console.WriteLine($"Grpc ServerListening On Port {_grpcSettings.Value.Port}");
30         }
31     }
32 }

在回到Snai.GrpcService.Hosting項目中,添加對Snai.GrpcService.Impl的引用。

在 Startup.cs 中 ConfigureServices 中註冊 GrpcService 配置、註冊Rpc服務,在 Configure 中 啓動Rpc服務 就是上面說到的藍色字體標識的,如圖

最終項目結構以下:

  

 到此服務端的代碼實現已完成,下面咱們啓動Consul和服務端,驗證 Api 註冊和Grpc啓動。

2、Consul和服務端啓動

啓動Consul,啓動Grpc服務、註冊服務到Consul

一、啓動Consul

首先下載Consul:https://www.consul.io/downloads.html,本項目是Windows下進行測試,下載相應壓縮包獲得consul.exe

因爲本次用Api註冊,用Consul默認自帶UI,因此conf和dist可刪除(個人壓縮包裏面只有consul.exe

清除Consul/data 內容,新建startup.bat文件,輸入下面代碼,雙擊啓動Consul,本項目測試時一臺機器,因此把 本機IP 改爲 127.0.0.1

consul agent -server -datacenter=grpc-consul -bootstrap -data-dir ./data -ui -node=grpc-consul1 -bind 本機IP -client=0.0.0.0

 

再在Consul目錄下啓動另外一個cmd命令行窗口,輸入命令:consul operator raft list-peers 查看狀態查看狀態,結果以下

輸入http://localhost:8500 打開Consul UI查看狀況

Consul 啓動成功。

在 .net core Ocelot Consul 實現API網關 服務註冊 服務發現 負載均衡 中後面 Consul 部分,有 Consul 集羣搭建等其餘介紹,能夠去參考看下。

二、啓動服務端,啓動Grpc服務、註冊服務到Consul

因爲客戶端要實現負載,因此把 Snai.GrpcService.Hosting 項目生成兩次,啓動兩個同樣的服務端,只是端口不一樣

服務5021 地址爲5021: .UseUrls("http://localhost:5021"),GrpcService:5031,以下圖

 

啓動 服務5021和服務5022兩個服務端,以下面

 看到 Grpc ServerListening On Port 5031,Grpc ServerListening On Port 5032 說明 Grpc 服務端啓動成功

打開Consul服務查看地址  http://localhost:8500/ui/grpc-consul/services 查看,兩個GrpcService註冊成功,健康檢查狀態正常

到此,Grpc啓動正常,Consul Api服務註冊、健康檢查都正常,下面開始實現Grpc客戶端

3、客戶端

客戶端主要包括Grpc客戶端,Consul Api服務發現、負載均衡等。

新建Snai.GrpcClient 控制檯程序,在 依賴項 下載安裝Grpc.Core 包,項目引用Snai.GrpcService.Protocol,在依賴項下載安裝下面工具組件包

用於讀取 json配置:Microsoft.Extensions.Configuration,Microsoft.Extensions.Configuration.Json 

用於依賴注入:Microsoft.Extensions.DependencyInjection

用於注入全局配置:Microsoft.Extensions.Options,Microsoft.Extensions.Options.ConfigurationExtensions

在項目根目錄下新建 Utils 目錄,在 Utils 目錄下新建 HttpHelper.cs 類,用於程序內發送http請求,代碼以下

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Net.Http;
  4 using System.Text;
  5 using System.Threading.Tasks;
  6 
  7 /// <summary>
  8 /// 參考 pudefu https://www.cnblogs.com/pudefu/p/7581956.html ,在此表示感謝
  9 /// </summary>
 10 namespace Snai.GrpcClient.Utils
 11 {
 12     public class HttpHelper
 13     {
 14         /// <summary>
 15         /// 同步GET請求
 16         /// </summary>
 17         /// <param name="url"></param>
 18         /// <param name="headers"></param>
 19         /// <param name="timeout">請求響應超時時間,單位/s(默認100秒)</param>
 20         /// <returns></returns>
 21         public static string HttpGet(string url, Dictionary<string, string> headers = null, int timeout = 0)
 22         {
 23             using (HttpClient client = new HttpClient())
 24             {
 25                 if (headers != null)
 26                 {
 27                     foreach (KeyValuePair<string, string> header in headers)
 28                     {
 29                         client.DefaultRequestHeaders.Add(header.Key, header.Value);
 30                     }
 31                 }
 32                 if (timeout > 0)
 33                 {
 34                     client.Timeout = new TimeSpan(0, 0, timeout);
 35                 }
 36                 Byte[] resultBytes = client.GetByteArrayAsync(url).Result;
 37                 return Encoding.UTF8.GetString(resultBytes);
 38             }
 39         }
 40 
 41         /// <summary>
 42         /// 異步GET請求
 43         /// </summary>
 44         /// <param name="url"></param>
 45         /// <param name="headers"></param>
 46         /// <param name="timeout">請求響應超時時間,單位/s(默認100秒)</param>
 47         /// <returns></returns>
 48         public static async Task<string> HttpGetAsync(string url, Dictionary<string, string> headers = null, int timeout = 0)
 49         {
 50             using (HttpClient client = new HttpClient())
 51             {
 52                 if (headers != null)
 53                 {
 54                     foreach (KeyValuePair<string, string> header in headers)
 55                     {
 56                         client.DefaultRequestHeaders.Add(header.Key, header.Value);
 57                     }
 58                 }
 59                 if (timeout > 0)
 60                 {
 61                     client.Timeout = new TimeSpan(0, 0, timeout);
 62                 }
 63                 Byte[] resultBytes = await client.GetByteArrayAsync(url);
 64                 return Encoding.Default.GetString(resultBytes);
 65             }
 66         }
 67 
 68 
 69         /// <summary>
 70         /// 同步POST請求
 71         /// </summary>
 72         /// <param name="url"></param>
 73         /// <param name="postData"></param>
 74         /// <param name="headers"></param>
 75         /// <param name="contentType"></param>
 76         /// <param name="timeout">請求響應超時時間,單位/s(默認100秒)</param>
 77         /// <param name="encoding">默認UTF8</param>
 78         /// <returns></returns>
 79         public static string HttpPost(string url, string postData, Dictionary<string, string> headers = null, string contentType = null, int timeout = 0, Encoding encoding = null)
 80         {
 81             using (HttpClient client = new HttpClient())
 82             {
 83                 if (headers != null)
 84                 {
 85                     foreach (KeyValuePair<string, string> header in headers)
 86                     {
 87                         client.DefaultRequestHeaders.Add(header.Key, header.Value);
 88                     }
 89                 }
 90                 if (timeout > 0)
 91                 {
 92                     client.Timeout = new TimeSpan(0, 0, timeout);
 93                 }
 94                 using (HttpContent content = new StringContent(postData ?? "", encoding ?? Encoding.UTF8))
 95                 {
 96                     if (contentType != null)
 97                     {
 98                         content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
 99                     }
100                     using (HttpResponseMessage responseMessage = client.PostAsync(url, content).Result)
101                     {
102                         Byte[] resultBytes = responseMessage.Content.ReadAsByteArrayAsync().Result;
103                         return Encoding.UTF8.GetString(resultBytes);
104                     }
105                 }
106             }
107         }
108 
109         /// <summary>
110         /// 異步POST請求
111         /// </summary>
112         /// <param name="url"></param>
113         /// <param name="postData"></param>
114         /// <param name="headers"></param>
115         /// <param name="contentType"></param>
116         /// <param name="timeout">請求響應超時時間,單位/s(默認100秒)</param>
117         /// <param name="encoding">默認UTF8</param>
118         /// <returns></returns>
119         public static async Task<string> HttpPostAsync(string url, string postData, Dictionary<string, string> headers = null, string contentType = null, int timeout = 0, Encoding encoding = null)
120         {
121             using (HttpClient client = new HttpClient())
122             {
123                 if (headers != null)
124                 {
125                     foreach (KeyValuePair<string, string> header in headers)
126                     {
127                         client.DefaultRequestHeaders.Add(header.Key, header.Value);
128                     }
129                 }
130                 if (timeout > 0)
131                 {
132                     client.Timeout = new TimeSpan(0, 0, timeout);
133                 }
134                 using (HttpContent content = new StringContent(postData ?? "", encoding ?? Encoding.UTF8))
135                 {
136                     if (contentType != null)
137                     {
138                         content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
139                     }
140                     using (HttpResponseMessage responseMessage = await client.PostAsync(url, content))
141                     {
142                         Byte[] resultBytes = await responseMessage.Content.ReadAsByteArrayAsync();
143                         return Encoding.UTF8.GetString(resultBytes);
144                     }
145                 }
146             }
147         }
148     }
149 }

在項目根目錄下新建 Consul 目錄,在 Consul 目錄下新建 Entity 目錄,在 Entity 目錄下新建 HealthCheck.cs 類,用於接收 Consul Api發現的信息實體,代碼以下

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Text;
 4 
 5 namespace Snai.GrpcClient.Consul.Entity
 6 {
 7     public class HealthCheck
 8     {
 9         public string Node { get; set; }
10         public string CheckID { get; set; }
11         public string Name { get; set; }
12         public string Status { get; set; }
13         public string Notes { get; set; }
14         public string Output { get; set; }
15         public string ServiceID { get; set; }
16         public string ServiceName { get; set; }
17         public string[] ServiceTags { get; set; }
18         public dynamic Definition { get; set; }
19         public int CreateIndex { get; set; }
20         public int ModifyIndex { get; set; }
21     }
22 }

 在項目根目錄下新建 Framework 目錄,在 Framework 目錄下新建 Entity 目錄,在 Entity 目錄下新建 ConsulService.cs 和 GrpcServiceSettings.cs 類,分別對應配置appsettings.json的 ConsulService,GrpcServiceSettings 兩個配置項,代碼以下

ConsulService.cs

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Text;
 4 
 5 namespace Snai.GrpcClient.Framework.Entity
 6 {
 7     public class ConsulService
 8     {
 9         public string IP { get; set; }
10         public int Port { get; set; }
11     }
12 }

 GrpcServiceSettings.cs

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Text;
 4 
 5 namespace Snai.GrpcClient.Framework.Entity
 6 {
 7     public class GrpcServiceSettings
 8     {
 9         public List<GrpcService> GrpcServices { get; set; }
10     }
11     public class GrpcService
12     {
13         public string ServiceName { get; set; }
14         public string ServiceID { get; set; }
15         public string IP { get; set; }
16         public int Port { get; set; }
17         public int Weight { get; set; }
18     }
19 }

在 Consul 目錄下新建 IAppFind.cs 接口,定義 FindConsul() 用於 Consul 服務發現基方法,代碼以下

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Text;
 4 
 5 namespace Snai.GrpcClient.Consul
 6 {
 7     public interface IAppFind
 8     {
 9         IEnumerable<string> FindConsul(string ServiceName);
10     }
11 }

在 Consul 目錄下新建 AppFind.cs 類,用於實現 IAppFind.cs 接口,實現 Consul 服務發現方法,代碼以下

 1 using Microsoft.Extensions.Options;
 2 using Newtonsoft.Json;
 3 using Snai.GrpcClient.Consul.Entity;
 4 using Snai.GrpcClient.Framework.Entity;
 5 using Snai.GrpcClient.Utils;
 6 using System;
 7 using System.Collections.Generic;
 8 using System.Linq;
 9 using System.Text;
10 
11 namespace Snai.GrpcClient.Consul
12 {
13     /// <summary>
14     /// 服務發現
15     /// (服務和健康信息)http://localhost:8500/v1/health/service/GrpcService
16     /// (健康信息)http://localhost:8500/v1/health/checks/GrpcService
17     /// </summary>
18     public class AppFind : IAppFind
19     {
20         private static IOptions<GrpcServiceSettings> _grpcSettings;
21         private static IOptions<ConsulService> _consulSettings;
22         public AppFind(IOptions<GrpcServiceSettings> grpcSettings, IOptions<ConsulService> consulSettings)
23         {
24             _grpcSettings = grpcSettings;
25             _consulSettings = consulSettings;
26         }
27         public IEnumerable<string> FindConsul(string ServiceName)
28         {
29             Dictionary<string, string> headers = new Dictionary<string, string>();
30 
31             var consul = _consulSettings.Value;
32             string findUrl = $"http://{consul.IP}:{consul.Port}/v1/health/checks/{ServiceName}";
33 
34             string findResult = HttpHelper.HttpGet(findUrl, headers, 5);
35             if (findResult.Equals(""))
36             {
37                 var grpcServices = _grpcSettings.Value.GrpcServices;
38                 return grpcServices.Where(g => g.ServiceName.Equals(ServiceName, StringComparison.CurrentCultureIgnoreCase)).Select(s => s.ServiceID);
39             }
40 
41             var findCheck = JsonConvert.DeserializeObject<List<HealthCheck>>(findResult);
42 
43             return findCheck.Where(g => g.Status.Equals("passing", StringComparison.CurrentCultureIgnoreCase)).Select(g => g.ServiceID);
44         }
45     }
46 }

 

在項目根目錄下新建 LoadBalance 目錄,在 LoadBalance 目錄下新建 ILoadBalance.cs 接口,定義 GetGrpcService() 用於負載均衡基方法,代碼以下

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Text;
 4 
 5 namespace Snai.GrpcClient.LoadBalance
 6 {
 7     public interface ILoadBalance
 8     {
 9         string GetGrpcService(string ServiceName);
10     }
11 }

在 LoadBalance 目錄下新建 WeightRoundBalance.cs 類,用於實現 ILoadBalance.cs 接口,實現 GetGrpcService() 負載均衡方法,本次負載均衡實現權重輪詢算法,代碼以下

 1 using Microsoft.Extensions.Options;
 2 using Snai.GrpcClient.Consul;
 3 using Snai.GrpcClient.Framework.Entity;
 4 using System;
 5 using System.Collections.Generic;
 6 using System.Linq;
 7 using System.Text;
 8 
 9 namespace Snai.GrpcClient.LoadBalance
10 {
11     /// <summary>
12     /// 權重輪詢
13     /// </summary>
14     public class WeightRoundBalance: ILoadBalance
15     {
16         int Balance;
17         IOptions<GrpcServiceSettings> GrpcSettings;
18         IAppFind AppFind;
19 
20         public WeightRoundBalance(IOptions<GrpcServiceSettings> grpcSettings, IAppFind appFind)
21         {
22             Balance = 0;
23             GrpcSettings = grpcSettings;
24             AppFind = appFind;
25         }
26 
27         public string GetGrpcService(string ServiceName)
28         {
29             var grpcServices = GrpcSettings.Value.GrpcServices;
30 
31             var healthServiceID = AppFind.FindConsul(ServiceName);
32 
33             if (grpcServices == null || grpcServices.Count() == 0 || healthServiceID == null || healthServiceID.Count() == 0)
34             {
35                 return "";
36             }
37 
38             //健康的服務
39             var healthServices = new List<Framework.Entity.GrpcService>();
40 
41             foreach (var service in grpcServices)
42             {
43                 foreach (var health in healthServiceID)
44                 {
45                     if (service.ServiceID.Equals(health, StringComparison.CurrentCultureIgnoreCase))
46                     {
47                         healthServices.Add(service);
48                         break;
49                     }
50                 }
51             }
52 
53             if (healthServices == null || healthServices.Count() == 0)
54             {
55                 return "";
56             }
57 
58             //加權輪詢
59             var services = new List<string>();
60 
61             foreach (var service in healthServices)
62             {
63                 services.AddRange(Enumerable.Repeat(service.IP + ":" + service.Port, service.Weight));
64             }
65 
66             var servicesArray = services.ToArray();
67 
68             Balance = Balance % servicesArray.Length;
69             var grpcUrl = servicesArray[Balance];
70             Balance = Balance + 1;
71 
72             return grpcUrl;
73         }
74     }
75 }

 

在項目根目錄下新建 RpcClient 目錄,在 RpcClient 目錄下新建 IMsgClient.cs 接口,定義 GetSum() 用於Grpc客戶端調用基方法,代碼以下

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Text;
 4 
 5 namespace Snai.GrpcClient.RpcClient
 6 {
 7     public interface IMsgClient
 8     {
 9         void GetSum(int num1, int num2);
10     }
11 }

在 RpcClient 目錄下新建 MsgClient.cs 類,用於實現 IMsgClient.cs 接口,實現 GetSum() 方法用於Grpc客戶端調用,代碼以下

 1 using Grpc.Core;
 2 using Snai.GrpcClient.LoadBalance;
 3 using Snai.GrpcService.Protocol;
 4 using System;
 5 using System.Collections.Generic;
 6 using System.Text;
 7 
 8 namespace Snai.GrpcClient.RpcClient
 9 {
10     public class MsgClient:IMsgClient
11     {
12         ILoadBalance LoadBalance;
13         Channel GrpcChannel;
14         MsgService.MsgServiceClient GrpcClient;
15 
16         public MsgClient(ILoadBalance loadBalance)
17         {
18             LoadBalance = loadBalance;
19 
20             var grpcUrl = LoadBalance.GetGrpcService("GrpcService");
21 
22             if (!grpcUrl.Equals(""))
23             {
24                 Console.WriteLine($"Grpc Service:{grpcUrl}");
25 
26                 GrpcChannel = new Channel(grpcUrl, ChannelCredentials.Insecure);
27                 GrpcClient = new MsgService.MsgServiceClient(GrpcChannel);
28             }
29         }
30 
31         public void GetSum(int num1, int num2)
32         {
33             if (GrpcClient != null)
34             {
35                 GetMsgSumReply msgSum = GrpcClient.GetSum(new GetMsgNumRequest
36                 {
37                     Num1 = num1,
38                     Num2 = num2
39                 });
40 
41                 Console.WriteLine("Grpc Client Call GetSum():" + msgSum.Sum);
42             }
43             else
44             {
45                 Console.WriteLine("全部負載都掛掉了!");
46             }
47         }
48     }
49 }

在 Framework 目錄下新建 DependencyInitialize.cs 類,定義 AddImplement() 方法用於註冊全局配置和類到容器,實現依賴注入,代碼以下

 1 using Microsoft.Extensions.Configuration;
 2 using Microsoft.Extensions.DependencyInjection;
 3 using Snai.GrpcClient.Consul;
 4 using Snai.GrpcClient.Framework.Entity;
 5 using Snai.GrpcClient.LoadBalance;
 6 using Snai.GrpcClient.RpcClient;
 7 using System;
 8 using System.Collections.Generic;
 9 using System.IO;
10 using System.Text;
11 
12 namespace Snai.GrpcClient.Framework
13 {
14     /// <summary>
15     /// IServiceCollection 依賴注入生命週期
16     /// AddTransient 每次都是全新的
17     /// AddScoped    在一個範圍以內只有同一個實例(同一個線程,同一個瀏覽器請求只有一個實例)
18     /// AddSingleton 單例
19     /// </summary>
20     public static class DependencyInitialize
21     {
22         /// <summary>
23         /// 註冊對象
24         /// </summary>
25         /// <param name="services">The services.</param>
26         /*
27          * IAppFind AppFind;
28          * 構造函數注入使用 IAppFind appFind
29          * AppFind = appFind;
30          */
31         public static void AddImplement(this IServiceCollection services)
32         {
33             //添加 json 文件路徑
34             var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json");
35             //建立配置根對象
36             var configurationRoot = builder.Build();
37 
38             //註冊全局配置
39             services.AddConfigImplement(configurationRoot);
40 
41             //註冊服務發現
42             services.AddScoped<IAppFind, AppFind>();
43 
44             //註冊負載均衡
45             if (configurationRoot["LoadBalancer"].Equals("WeightRound", StringComparison.CurrentCultureIgnoreCase))
46             {
47                 services.AddSingleton<ILoadBalance, WeightRoundBalance>();
48             }
49 
50             //註冊Rpc客戶端
51             services.AddTransient<IMsgClient, MsgClient>();
52         }
53 
54         /// <summary>
55         /// 註冊全局配置
56         /// </summary>
57         /// <param name="services">The services.</param>
58         /// <param name="configurationRoot">The configurationRoot.</param>
59         /*  
60          *  IOptions<GrpcServiceSettings> GrpcSettings;
61          *  構造函數注入使用 IOptions<GrpcServiceSettings> grpcSettings
62          *  GrpcSettings = grpcSettings;
63          */
64         public static void AddConfigImplement(this IServiceCollection services, IConfigurationRoot configurationRoot)
65         {
66             //註冊配置對象
67             services.AddOptions();
68             services.Configure<GrpcServiceSettings>(configurationRoot.GetSection(nameof(GrpcServiceSettings)));
69             services.Configure<ConsulService>(configurationRoot.GetSection(nameof(ConsulService)));
70         }
71     }
72 }

  在根目錄下新建 appsettings.json 配置文件,配置 GrpcServiceSettings 的 GrpcServices 爲服務端發佈的兩個服務5021和5022,LoadBalancer 負載均衡爲 WeightRound 權重輪詢(如實現其餘負載方法可作相應配置,註冊負載均衡時也作相應修改),ConsulService Consul的IP和端口,代碼以下

 1 {
 2   "GrpcServiceSettings": {
 3     "GrpcServices": [
 4       {
 5         "ServiceName": "GrpcService",
 6         "ServiceID": "GrpcService_5021",
 7         "IP": "localhost",
 8         "Port": "5031",
 9         "Weight": "2"
10       },
11       {
12         "ServiceName": "GrpcService",
13         "ServiceID": "GrpcService_5022",
14         "IP": "localhost",
15         "Port": "5032",
16         "Weight": "1"
17       }
18     ]
19   },
20   "LoadBalancer": "WeightRound",
21   "ConsulService": {
22     "IP": "localhost",
23     "Port": "8500"
24   }
25 }

GrpcServices Grpc服務列表

  ServiceName:服務名稱,負載同一服務名稱相同

  ServiceID:服務ID,保持惟一

  IP:服務IP

  Port:端口

  Weight:服務權重

 修改 Program.cs 的 Main() 方法,調用 AddImplement(),註冊全局配置和類到容器,注入使用 MsgClient 類的 GetSum() 方法,實現 Grpc 調用,代碼以下

 1 using Microsoft.Extensions.DependencyInjection;
 2 using Snai.GrpcClient.Framework;
 3 using Snai.GrpcClient.RpcClient;
 4 using System;
 5 
 6 namespace Snai.GrpcClient
 7 {
 8     class Program
 9     {
10         static void Main(string[] args)
11         {
12             IServiceCollection service = new ServiceCollection();
13 
14             //註冊對象
15             service.AddImplement();
16 
17             //注入使用對象
18             var provider = service.BuildServiceProvider();
19 
20             string exeArg = string.Empty;
21             Console.WriteLine("Grpc調用!");
22             Console.WriteLine("-c\t調用Grpc服務;");
23             Console.WriteLine("-q\t退出服務;");
24 
25             while (true)
26             {
27                 exeArg = Console.ReadKey().KeyChar.ToString();
28                 Console.WriteLine();
29 
30                 if (exeArg.ToLower().Equals("c", StringComparison.CurrentCultureIgnoreCase))
31                 {
32                     //調用服務
33                     var rpcClient = provider.GetService<IMsgClient>();
34                     rpcClient.GetSum(10, 2);
35                 }
36                 else if (exeArg.ToLower().Equals("q", StringComparison.CurrentCultureIgnoreCase))
37                 {
38                     break;
39                 }
40                 else
41                 {
42                     Console.WriteLine("參數異常!");
43                 }
44             }
45         }
46     }
47 }

右擊項目生成,最終項目結構以下:

到此客戶端的代碼實現已完成,下面運行測試 Grpc+Consul 服務註冊、服務發現和負載均衡。

4、運行測試 Grpc+Consul 服務註冊、服務發現和負載均衡

 雙擊 startup.bat 啓動 Consul,再啓動服務5021和5022,啓動成功打開 http://localhost:8500/ui/#/grpc-consul/services/GrpcService 查看服務狀況

啓動 Snai.GrpcClient 客戶端

輸入 c 調用Grpc服務,調用3次,5031調用2次,5032調用1次,成功實現負載均衡

關掉服務5022,等10秒左右(由於設置健康檢查時間間隔10秒),再輸入 c 調用Grpc服務,只調用5031

打開 http://localhost:8500/ui/#/grpc-consul/services/GrpcService 查看,5022 狀態失敗,或消失

Grpc+Consul實現服務註冊、服務發現、健康檢查和負載均衡已完成

源碼訪問地址:https://github.com/Liu-Alan/Grpc-Consul

相關文章
相關標籤/搜索