天天看點

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

在上一篇 .net core grpc 實作通信(一) 中,我們實作的grpc通信在.net core中的可行性,但要在微服務中真正使用,還缺少 服務注冊,服務發現及負載均衡等,本篇我們将在 .net core grpc 通信 的基礎上加上 服務注冊,服務發現,負載均衡。

如對.net core grpc 通信不太熟悉的,可以看上一篇 .net core grpc 實作通信(一) ,然後再看本篇。

grpc(https://grpc.io/)是google釋出的一個開源、高性能、通用RPC(Remote Procedure Call)架構,使用HTTP/2協定,支援多路複用,并用ProtoBuf作為序列化工具,提供跨語言、跨平台支援。

Consul(https://www.consul.io)是一個分布式,高可用、支援多資料中心的服務注冊、發現、健康檢查和配置共享的服務軟體,由 HashiCorp 公司用 Go 語言開發。

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

軟體版本

.net core:2.0

grpc:1.11.0

Consul:1.1.0

Consul Nuget注冊元件:0.7.2.5

項目結構

.net core 代碼部分:

Snai.GrpcClient 用戶端 .net core 2.0控制台程式

Snai.GrpcService.Hosting 服務端宿主,Api服務注冊,asp.net core 2.0網站程式

Snai.GrpcService.Impl 協定方法實作  .net standard 2.0類庫

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

Consul:

conf 配置目錄,本次用api注冊服務,可以删除

data 緩存資料目錄,可清空裡面内容

dist Consul UI目錄,本次用預設的UI,可以删除

consul.exe 注冊軟體

startup.bat 執行腳本

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

項目實作

 一、服務端

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

建立Snai.GrpcService解決方案,由于這次加入了 Consul Api 服務注冊,是以我們先從 Api 服務注冊開始。

1、實作 Consul Api 服務注冊

建立 Snai.GrpcService.Hosting 基于Asp.net Core 2.0空網站,在 依賴項 右擊 管理NuGet程式包 浏覽 找到 Consul 版本0.7.2.5安裝,用于Api服務注冊使用

建立 appsettings.json 配置檔案,配置 GrpcService Grpc服務端IP和端口,HealthService健康檢測名稱、IP和位址,ConsulService Consul的IP和端口,代碼如下

{
  "GrpcService": {
    "IP": "localhost",
    "Port": "5031"
  },
  "HealthService": {
    "Name": "GrpcService",
    "IP": "localhost",
    "Port": "5021"
  },
  "ConsulService": {
    "IP": "localhost",
    "Port": "8500"
  }
}      

建立Consul目錄,用于放Api注冊相關代碼

在Consul目錄下建立Entity目錄,在Entity目錄下建立HealthService.cs,ConsulService.cs類,分别對應HealthService,ConsulService兩個配置項,代碼如下

HealthService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Snai.GrpcService.Hosting.Consul.Entity
{
    public class HealthService
    {
        public string Name { get; set; }
        public string IP { get; set; }
        public int Port { get; set; }
        
    }
}      

 ConsulService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Snai.GrpcService.Hosting.Consul.Entity
{
    public class ConsulService
    {
        public string IP { get; set; }
        public int Port { get; set; }
    }
}      

 在 Consul 目錄下建立 AppRregister.cs 類,添加 IApplicationBuilder 擴充方法 RegisterConsul,來調用 Consul Api 實作服務注冊,代碼如下

using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
using Snai.GrpcService.Hosting.Consul.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Snai.GrpcService.Hosting.Consul
{
    public static class AppRregister
    {
        // 服務注冊
        public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IApplicationLifetime lifetime, IOptions<HealthService> healthService, IOptions<ConsulService> consulService)
        {
            var consulClient = new ConsulClient(x => x.Address = new Uri($"http://{consulService.Value.IP}:{consulService.Value.Port}"));//請求注冊的 Consul 位址
            var httpCheck = new AgentServiceCheck()
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服務啟動多久後注冊
                Interval = TimeSpan.FromSeconds(10),//健康檢查時間間隔,或者稱為心跳間隔
                HTTP = $"http://{healthService.Value.IP}:{healthService.Value.Port}/health",//健康檢查位址
                Timeout = TimeSpan.FromSeconds(5)
            };

            // Register service with consul
            var registration = new AgentServiceRegistration()
            {
                Checks = new[] { httpCheck },
                ID = healthService.Value.Name + "_" + healthService.Value.Port,
                Name = healthService.Value.Name,
                Address = healthService.Value.IP,
                Port = healthService.Value.Port,
                Tags = new[] { $"urlprefix-/{healthService.Value.Name}" }//添加 urlprefix-/servicename 格式的 tag 标簽,以便 Fabio 識别
            };

            consulClient.Agent.ServiceRegister(registration).Wait();//服務啟動時注冊,内部實作其實就是使用 Consul API 進行注冊(HttpClient發起)
            lifetime.ApplicationStopping.Register(() =>
            {
                consulClient.Agent.ServiceDeregister(registration.ID).Wait();//服務停止時取消注冊
            });

            return app;
        }
    }
}

      

修改 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服務 後面用到等下講

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Snai.GrpcService.Hosting.Consul;
using Snai.GrpcService.Hosting.Consul.Entity;
using Snai.GrpcService.Impl;

namespace Snai.GrpcService.Hosting
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            //注冊全局配置
            services.AddOptions();
            services.Configure<Impl.Entity.GrpcService>(Configuration.GetSection(nameof(Impl.Entity.GrpcService)));
            services.Configure<HealthService>(Configuration.GetSection(nameof(HealthService)));
            services.Configure<ConsulService>(Configuration.GetSection(nameof(ConsulService)));

            //注冊Rpc服務
            services.AddSingleton<IRpcConfig, RpcConfig>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime, IOptions<HealthService> healthService, IOptions<ConsulService> consulService, IRpcConfig rpc)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // 添加健康檢查路由位址
            app.Map("/health", HealthMap);

            // 服務注冊
            app.RegisterConsul(lifetime, healthService, consulService);

            // 啟動Rpc服務
            rpc.Start();
        }

        private static void HealthMap(IApplicationBuilder app)
        {
            app.Run(async context =>
            {
                await context.Response.WriteAsync("OK");
            });
        }
    }
}      

 修改 Program.cs 代碼,調置網站位址為 .UseUrls("http://localhost:5021"),代碼如下

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Snai.GrpcService.Hosting
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseUrls("http://localhost:5021")
                .UseStartup<Startup>()
                .Build();
    }
}
      

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

2、協定編寫,将協定生成C#代碼

由于在上一篇 .net core grpc 實作通信(一) 有過介紹,這裡就簡單說下

建立 Snai.GrpcService.Protocol協定類庫項目,在 依賴項 右擊 管理NuGet程式包 浏覽 找到 Grpc.Core 版本1.11.0,Google.Protobuf 版本3.5.1 包下載下傳安裝

在根目錄下建立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;
}      

建立.net framework 項目類庫,引用安裝 Grpc.Tools、Google.Protobuf.Tools 元件程式包,分别得到 grpc_csharp_plugin.exe、protoc.exe 工具

到package目錄下,找到與系統相應的grpc_csharp_plugin.exe、protoc.exe工具,拷到 Snai.GrpcService.Protocol 項目下

在Snai.GrpcService.Protocol根目錄下建立 ProtocGenerate.cmd 檔案,在其中輸入以下指令

protoc -I . --csharp_out . --grpc_out . --plugin=protoc-gen-grpc=grpc_csharp_plugin.exe msg.proto      

然後直接輕按兩下運作,項目下生成了“Msg.cs”和“MsgGrpc.cs”兩個檔案,這樣協定部分的所有工作就完成了,最終項目結構如下:

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

 3、編寫協定實作代碼

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

建立 Entity 目錄,在Entity目錄下建立 GrpcService.cs 類,對應 Snai.GrpcService.Hosting 項目下 appsettings.json 配置檔案的 GrpcService 配置項,代碼如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Snai.GrpcService.Impl.Entity
{
    public class GrpcService
    {
        public string IP { get; set; }
        public int Port { get; set; }
    }
}
      

在根目錄下建立 RpcService 目錄,在 RpcService 目錄下建立 MsgServiceImpl.cs 類,繼承 MsgService.MsgServiceBase 協定類,實作服務方法,代碼如下

using Grpc.Core;
using Snai.GrpcService.Protocol;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Snai.GrpcService.Impl.RpcService
{
    public class MsgServiceImpl : MsgService.MsgServiceBase
    {
        public override async Task<GetMsgSumReply> GetSum(GetMsgNumRequest request, ServerCallContext context)
        {
            var result = new GetMsgSumReply();

            result.Sum = request.Num1 + request.Num2;

            Console.WriteLine(request.Num1 + "+" + request.Num2 + "=" + result.Sum);

            return result;
        }
    }
}      

 在根目錄下建立IRpcConfig.cs接口,定義 Start() 用于Rpc啟動基方法,代碼如下

using System;
using System.Collections.Generic;
using System.Text;

namespace Snai.GrpcService.Impl
{
    public interface IRpcConfig
    {
        void Start();
    }
}
      

 在根目錄下建立 RpcConfig.cs 類,用于實作 IRpcConfig.cs 接口,啟動Rpc服務,代碼如下

using Grpc.Core;
using Microsoft.Extensions.Options;
using Snai.GrpcService.Impl.RpcService;
using Snai.GrpcService.Protocol;
using System;
using System.Collections.Generic;
using System.Text;

namespace Snai.GrpcService.Impl
{
    public class RpcConfig: IRpcConfig
    {
        private static Server _server;
        static IOptions<Entity.GrpcService> GrpcSettings;

        public RpcConfig(IOptions<Entity.GrpcService> grpcSettings)
        {
            GrpcSettings = grpcSettings;
        }

        public void Start()
        {
            _server = new Server
            {
                Services = { MsgService.BindService(new MsgServiceImpl()) },
                Ports = { new ServerPort(GrpcSettings.Value.IP, GrpcSettings.Value.Port, ServerCredentials.Insecure) }
            };
            _server.Start();

            Console.WriteLine($"Grpc ServerListening On Port {GrpcSettings.Value.Port}");
        }
    }
}
      

 在回到Snai.GrpcService.Hosting項目中,在 Startup.cs 中 ConfigureServices 中注冊 GrpcService 配置、注冊Rpc服務,在 Configure 中 啟動Rpc服務 就是上面說到的藍色字型辨別的,如圖

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

最終項目結構如下:

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

到此服務端的代碼實作已完成,下面我們啟動Consul和服務端,驗證 Api 注冊和Grpc啟動。

二、Consul和服務端啟動

啟動Consul,啟動Grpc服務、注冊服務到Consul

1、啟動Consul

首先下載下傳Consul:https://www.consul.io/downloads.html,本項目是windows下進行測試,得到consul.exe

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

由于本次用Api注冊,用Consul預設自帶UI,是以conf和dist可删除

清除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 檢視狀态檢視狀态,結果如下

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

打開Consul UI:http://localhost:8500 檢視情況

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

Consul 啟動成功。

在 .net core Ocelot Consul 實作API網關 服務注冊 服務發現 負載均衡 中後面 Consul 部分,有 Consul 叢集搭建等其他介紹,可以去參考看下。

2、啟動服務端,啟動Grpc服務、注冊服務到Consul

由于用戶端要實作負載,是以把 Snai.GrpcService.Hosting 項目生成兩次,啟動兩個一樣的服務端,隻是端口不同

服務5021 位址為5021: .UseUrls("http://localhost:5021"),GrpcService:5031,如下圖

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

 服務5022 修改位址為5022: .UseUrls("http://localhost:5022"),GrpcService:5032,如下圖

啟動 服務5021和服務5022兩個服務端,如下面

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)
.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)
.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

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

看到 Request starting HTTP/1.1 GET http://localhost:5021/health 說明 Consul 健康檢查成功

打開Consul服務檢視位址 http://localhost:8500/ui/#/grpc-consul/services/GrpcService 檢視,兩個GrpcService注冊成功,健康檢查狀态正常

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

到此,Grpc啟動正常,Consul Api服務注冊、健康檢查都正常,下面開始實作Grpc用戶端

三、用戶端

用戶端主要包括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請求,代碼如下

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
/*
 * 參考 pudefu https://www.cnblogs.com/pudefu/p/7581956.html ,在此表示感謝
 */
namespace Snai.GrpcClient.Utils
{
    public class HttpHelper
    {
        /// <summary>
        /// 同步GET請求
        /// </summary>
        /// <param name="url"></param>
        /// <param name="headers"></param>
        /// <param name="timeout">請求響應逾時時間,機關/s(預設100秒)</param>
        /// <returns></returns>
        public static string HttpGet(string url, Dictionary<string, string> headers = null, int timeout = 0)
        {
            using (HttpClient client = new HttpClient())
            {
                if (headers != null)
                {
                    foreach (KeyValuePair<string, string> header in headers)
                    {
                        client.DefaultRequestHeaders.Add(header.Key, header.Value);
                    }
                }
                if (timeout > 0)
                {
                    client.Timeout = new TimeSpan(0, 0, timeout);
                }
                Byte[] resultBytes = client.GetByteArrayAsync(url).Result;
                return Encoding.UTF8.GetString(resultBytes);
            }
        }

        /// <summary>
        /// 異步GET請求
        /// </summary>
        /// <param name="url"></param>
        /// <param name="headers"></param>
        /// <param name="timeout">請求響應逾時時間,機關/s(預設100秒)</param>
        /// <returns></returns>
        public static async Task<string> HttpGetAsync(string url, Dictionary<string, string> headers = null, int timeout = 0)
        {
            using (HttpClient client = new HttpClient())
            {
                if (headers != null)
                {
                    foreach (KeyValuePair<string, string> header in headers)
                    {
                        client.DefaultRequestHeaders.Add(header.Key, header.Value);
                    }
                }
                if (timeout > 0)
                {
                    client.Timeout = new TimeSpan(0, 0, timeout);
                }
                Byte[] resultBytes = await client.GetByteArrayAsync(url);
                return Encoding.Default.GetString(resultBytes);
            }
        }


        /// <summary>
        /// 同步POST請求
        /// </summary>
        /// <param name="url"></param>
        /// <param name="postData"></param>
        /// <param name="headers"></param>
        /// <param name="contentType"></param>
        /// <param name="timeout">請求響應逾時時間,機關/s(預設100秒)</param>
        /// <param name="encoding">預設UTF8</param>
        /// <returns></returns>
        public static string HttpPost(string url, string postData, Dictionary<string, string> headers = null, string contentType = null, int timeout = 0, Encoding encoding = null)
        {
            using (HttpClient client = new HttpClient())
            {
                if (headers != null)
                {
                    foreach (KeyValuePair<string, string> header in headers)
                    {
                        client.DefaultRequestHeaders.Add(header.Key, header.Value);
                    }
                }
                if (timeout > 0)
                {
                    client.Timeout = new TimeSpan(0, 0, timeout);
                }
                using (HttpContent content = new StringContent(postData ?? "", encoding ?? Encoding.UTF8))
                {
                    if (contentType != null)
                    {
                        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
                    }
                    using (HttpResponseMessage responseMessage = client.PostAsync(url, content).Result)
                    {
                        Byte[] resultBytes = responseMessage.Content.ReadAsByteArrayAsync().Result;
                        return Encoding.UTF8.GetString(resultBytes);
                    }
                }
            }
        }

        /// <summary>
        /// 異步POST請求
        /// </summary>
        /// <param name="url"></param>
        /// <param name="postData"></param>
        /// <param name="headers"></param>
        /// <param name="contentType"></param>
        /// <param name="timeout">請求響應逾時時間,機關/s(預設100秒)</param>
        /// <param name="encoding">預設UTF8</param>
        /// <returns></returns>
        public static async Task<string> HttpPostAsync(string url, string postData, Dictionary<string, string> headers = null, string contentType = null, int timeout = 0, Encoding encoding = null)
        {
            using (HttpClient client = new HttpClient())
            {
                if (headers != null)
                {
                    foreach (KeyValuePair<string, string> header in headers)
                    {
                        client.DefaultRequestHeaders.Add(header.Key, header.Value);
                    }
                }
                if (timeout > 0)
                {
                    client.Timeout = new TimeSpan(0, 0, timeout);
                }
                using (HttpContent content = new StringContent(postData ?? "", encoding ?? Encoding.UTF8))
                {
                    if (contentType != null)
                    {
                        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
                    }
                    using (HttpResponseMessage responseMessage = await client.PostAsync(url, content))
                    {
                        Byte[] resultBytes = await responseMessage.Content.ReadAsByteArrayAsync();
                        return Encoding.UTF8.GetString(resultBytes);
                    }
                }
            }
        }
    }
}
      

在項目根目錄下建立 Consul 目錄,在 Consul 目錄下建立 Entity 目錄,在 Entity 目錄下建立 HealthCheck.cs 類,用于接收 Consul Api發現的資訊實體,代碼如下

using System;
using System.Collections.Generic;
using System.Text;

namespace Snai.GrpcClient.Consul.Entity
{
    public class HealthCheck
    {
        public string Node { get; set; }
        public string CheckID { get; set; }
        public string Name { get; set; }
        public string Status { get; set; }
        public string Notes { get; set; }
        public string Output { get; set; }
        public string ServiceID { get; set; }
        public string ServiceName { get; set; }
        public string[] ServiceTags { get; set; }
        public dynamic Definition { get; set; }
        public int CreateIndex { get; set; }
        public int ModifyIndex { get; set; }
    }
}
      

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

using System;
using System.Collections.Generic;
using System.Text;

namespace Snai.GrpcClient.Consul
{
    public interface IAppFind
    {
        IEnumerable<string> FindConsul(string ServiceName);
    }
}
      

 在 Consul 目錄下建立 AppFind.cs 類,用于實作 IAppFind.cs 接口,實作 Consul 服務發現方法,代碼如下

using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Snai.GrpcClient.Consul.Entity;
using Snai.GrpcClient.Framework.Entity;
using Snai.GrpcClient.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Snai.GrpcClient.Consul
{
    /*
     * 服務發現
     * (服務和健康資訊)http://localhost:8500/v1/health/service/GrpcService
     * (健康資訊)http://localhost:8500/v1/health/checks/GrpcService
     */
    public class AppFind: IAppFind
    {
        static IOptions<GrpcServiceSettings> GrpcSettings;
        static IOptions<ConsulService> ConsulSettings;

        public AppFind(IOptions<GrpcServiceSettings> grpcSettings, IOptions<ConsulService> consulSettings)
        {
            GrpcSettings = grpcSettings;
            ConsulSettings = consulSettings;
        }
        
        public IEnumerable<string> FindConsul(string ServiceName)
        {
            Dictionary<string, string> headers = new Dictionary<string, string>();

            var consul = ConsulSettings.Value;
            string findUrl = $"http://{consul.IP}:{consul.Port}/v1/health/checks/{ServiceName}";

            string findResult = HttpHelper.HttpGet(findUrl, headers, 5);
            if (findResult.Equals(""))
            {
                var grpcServices = GrpcSettings.Value.GrpcServices;
                return grpcServices.Where(g=>g.ServiceName.Equals(ServiceName,StringComparison.CurrentCultureIgnoreCase)).Select(s => s.ServiceID);
            }

            var findCheck = JsonConvert.DeserializeObject<List<HealthCheck>>(findResult);

            return findCheck.Where(g => g.Status.Equals("passing", StringComparison.CurrentCultureIgnoreCase)).Select(g => g.ServiceID);
        }
    }
}

      

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

using Snai.GrpcClient.Framework.Entity;
using System;
using System.Collections.Generic;
using System.Text;

namespace Snai.GrpcClient.LoadBalance
{
    /*
     * 負載均衡接口
     */
    public interface ILoadBalance
    {
        string GetGrpcService(string ServiceName);
    }
}
      

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

using Snai.GrpcClient.Consul;
using Snai.GrpcClient.Utils;
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using Snai.GrpcClient.Framework.Entity;
using Microsoft.Extensions.Options;

namespace Snai.GrpcClient.LoadBalance
{
    /*
     * 權重輪詢
     */
    public class WeightRoundBalance : ILoadBalance
    {
        int Balance;
        IOptions<GrpcServiceSettings> GrpcSettings;
        IAppFind AppFind;

        public WeightRoundBalance(IOptions<GrpcServiceSettings> grpcSettings, IAppFind appFind)
        {
            Balance = 0;
            GrpcSettings = grpcSettings;
            AppFind = appFind;
        }

        public string GetGrpcService(string ServiceName)
        {
            var grpcServices = GrpcSettings.Value.GrpcServices;

            var healthServiceID = AppFind.FindConsul(ServiceName);

            if (grpcServices == null || grpcServices.Count() == 0 || healthServiceID == null || healthServiceID.Count() == 0)
            {
                return "";
            }

            //健康的服務
            var healthServices = new List<Framework.Entity.GrpcService>();

            foreach (var service in grpcServices)
            {
                foreach (var health in healthServiceID)
                {
                    if (service.ServiceID.Equals(health, StringComparison.CurrentCultureIgnoreCase))
                    {
                        healthServices.Add(service);
                        break;
                    }
                }
            }

            if (healthServices == null || healthServices.Count() == 0)
            {
                return "";
            }

            //權重輪詢
            var services = new List<string>();

            foreach (var service in healthServices)
            {
                services.AddRange(Enumerable.Repeat(service.IP + ":" + service.Port, service.Weight));
            }
            
            var servicesArray = services.ToArray();

            Balance = Balance % servicesArray.Length;
            var grpcUrl = servicesArray[Balance];
            Balance = Balance + 1;

            return grpcUrl;
        }
    }
}
      

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

using System;
using System.Collections.Generic;
using System.Text;

namespace Snai.GrpcClient.RpcClient
{
    public interface IMsgClient
    {
        void GetSum(int num1, int num2);
    }
}
      

 在 RpcClient 目錄下建立 MsgClient.cs 類,用于實作 IMsgClient.cs 接口,實作 GetSum() 方法用于Grpc用戶端調用,代碼如下

using Grpc.Core;
using Microsoft.Extensions.DependencyInjection;
using Snai.GrpcClient.LoadBalance;
using Snai.GrpcService.Protocol;
using System;
using System.Collections.Generic;
using System.Text;

namespace Snai.GrpcClient.RpcClient
{
    public class MsgClient: IMsgClient
    {
        ILoadBalance LoadBalance;
        Channel GrpcChannel;
        MsgService.MsgServiceClient GrpcClient;

        public MsgClient(ILoadBalance loadBalance)
        {
            LoadBalance = loadBalance;

            var grpcUrl = LoadBalance.GetGrpcService("GrpcService");

            if (!grpcUrl.Equals(""))
            {
                Console.WriteLine($"Grpc Service:{grpcUrl}");

                GrpcChannel = new Channel(grpcUrl, ChannelCredentials.Insecure);
                GrpcClient = new MsgService.MsgServiceClient(GrpcChannel);
            }
        }

        public void GetSum(int num1, int num2)
        {
            if (GrpcClient != null)
            {
                GetMsgSumReply msgSum = GrpcClient.GetSum(new GetMsgNumRequest
                {
                    Num1 = num1,
                    Num2 = num2
                });

                Console.WriteLine("Grpc Client Call GetSum():" + msgSum.Sum);
            }
            else
            {
                Console.WriteLine("所有負載都挂掉了!");
            }
        }
    }
}
      

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

ConsulService.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace Snai.GrpcClient.Framework.Entity
{
    public class ConsulService
    {
        public string IP { get; set; }
        public int Port { get; set; }
    }
}
      

 GrpcServiceSettings.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace Snai.GrpcClient.Framework.Entity
{
    public class GrpcServiceSettings
    {
        public List<GrpcService> GrpcServices { get; set; }
    }

    public class GrpcService
    {
        public string ServiceName { get; set; }
        public string ServiceID { get; set; }
        public string IP { get; set; }
        public int Port { get; set; }
        public int Weight { get; set; }
    }
}
      

在 Framework 目錄下建立 DependencyInitialize.cs 類,定義 AddImplement() 方法用于注冊全局配置和類到容器,實作依賴注入,代碼如下

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Snai.GrpcClient.Consul;
using Snai.GrpcClient.Framework.Entity;
using Snai.GrpcClient.LoadBalance;
using Snai.GrpcClient.RpcClient;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Snai.GrpcClient.Framework
{
    /*
     *  IServiceCollection 依賴注入生命周期
     *  AddTransient 每次都是全新的
     *  AddScoped    在一個範圍之内隻有同一個執行個體(同一個線程,同一個浏覽器請求隻有一個執行個體)
     *  AddSingleton 單例
     */
    public static class DependencyInitialize
    {
        /// <summary>
        /// 注冊對象
        /// </summary>
        /// <param name="services">The services.</param>
        /*
         * IAppFind AppFind;
         * 構造函數注入使用 IAppFind appFind
         * AppFind = appFind;
         */
        public static void AddImplement(this IServiceCollection services)
        {
            //添加 json 檔案路徑
            var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json");
            //建立配置根對象
            var configurationRoot = builder.Build();

            //注冊全局配置
            services.AddConfigImplement(configurationRoot);

            //注冊服務發現
            services.AddScoped<IAppFind, AppFind>();

            //注冊負載均衡
            if (configurationRoot["LoadBalancer"].Equals("WeightRound", StringComparison.CurrentCultureIgnoreCase))
            {
                services.AddSingleton<ILoadBalance, WeightRoundBalance>();
            }

            //注冊Rpc用戶端
            services.AddTransient<IMsgClient, MsgClient>();
        }

        /// <summary>
        /// 注冊全局配置
        /// </summary>
        /// <param name="services">The services.</param>
        /// <param name="configurationRoot">The configurationRoot.</param>
        /*  
         *  IOptions<GrpcServiceSettings> GrpcSettings;
         *  構造函數注入使用 IOptions<GrpcServiceSettings> grpcSettings
         *  GrpcSettings = grpcSettings;
         */
        public static void AddConfigImplement(this IServiceCollection services, IConfigurationRoot configurationRoot)
        {
            //注冊配置對象
            services.AddOptions();
            services.Configure<GrpcServiceSettings>(configurationRoot.GetSection(nameof(GrpcServiceSettings)));
            services.Configure<ConsulService>(configurationRoot.GetSection(nameof(ConsulService)));
        }
    }
}
      

 在根目錄下建立 appsettings.json 配置檔案,配置 GrpcServiceSettings 的 GrpcServices 為服務端釋出的兩個服務5021和5022,LoadBalancer 負載均衡為 WeightRound 權重輪詢(如實作其他負載方法可做相應配置,注冊負載均衡時也做相應修改),ConsulService Consul的IP和端口,代碼如下

{
  "GrpcServiceSettings": {
    "GrpcServices": [
      {
        "ServiceName": "GrpcService",
        "ServiceID": "GrpcService_5021",
        "IP": "localhost",
        "Port": "5031",
        "Weight": "2"
      },
      {
        "ServiceName": "GrpcService",
        "ServiceID": "GrpcService_5022",
        "IP": "localhost",
        "Port": "5032",
        "Weight": "1"
      }
    ]
  },
  "LoadBalancer": "WeightRound",
  "ConsulService": {
    "IP": "localhost",
    "Port": "8500"
  }
}
      

GrpcServices Grpc服務清單

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

  ServiceID:服務ID,保持唯一

  IP:服務IP

  Port:端口

  Weight:服務權重

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

using Microsoft.Extensions.DependencyInjection;
using Snai.GrpcClient.Framework;
using Snai.GrpcClient.RpcClient;
using System;

namespace Snai.GrpcClient
{
    class Program
    {
        static void Main(string[] args)
        {
            IServiceCollection service = new ServiceCollection();

            //注冊對象
            service.AddImplement();

            //注入使用對象
            var provider = service.BuildServiceProvider();

            string exeArg = string.Empty;
            Console.WriteLine("Grpc調用!");
            Console.WriteLine("-c\t調用Grpc服務;");
            Console.WriteLine("-q\t退出服務;");

            while (true)
            {
                exeArg = Console.ReadKey().KeyChar.ToString();
                Console.WriteLine();

                if (exeArg.ToLower().Equals("c", StringComparison.CurrentCultureIgnoreCase))
                {
                    //調用服務
                    var rpcClient = provider.GetService<IMsgClient>();
                    rpcClient.GetSum(10, 2);
                }
                else if (exeArg.ToLower().Equals("q", StringComparison.CurrentCultureIgnoreCase))
                {
                    break;
                }
                else
                {
                    Console.WriteLine("參數異常!");
                }
            }
        }
    }
}      

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

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

四、運作測試 Grpc+Consul 服務注冊、服務發現和負載均衡

 輕按兩下 startup.bat 啟動 Consul,再啟動服務5021和5022,啟動成功打開 http://localhost:8500/ui/#/grpc-consul/services/GrpcService 檢視服務情況

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)
.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)
.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

啟動 Snai.GrpcClient 用戶端

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

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

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

關掉服務5022,等10秒左右(因為設定健康檢查時間間隔10秒),再輸入 c 調用Grpc服務,隻調用5031

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

打開 http://localhost:8500/ui/#/grpc-consul/services/GrpcService 檢視,5022 狀态失敗,或消失

.net core grpc consul 實作服務注冊 服務發現 負載均衡(二)

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

Github源碼位址:https://github.com/Liu-Alan/Grpc-Consul

部落格位址:http://www.snaill.net/post/2

繼續閱讀