.Net Core(.Net6)建立grpc

2023-02-15 06:00:46

1.環境要求

.Net6,Visual Studio 2019 以上

官方檔案: https://learn.microsoft.com/zh-cn/aspnet/core/tutorials/grpc/grpc-start
Net Framework 版本: https://www.cnblogs.com/dennisdong/p/17119944.html

2.搭建幫助類

2.1 新建類庫

GrpcCommon

2.2 新建資料夾

資料夾:Certs,Helpers,Models

2.3 安裝依賴

NuGet依賴包Microsoft.AspNetCore.Authentication.JwtBeare 6.0.12,Newtonsoft.Json 13.0.2

2.4 新建專案檔案

Models下新建JwtToken.csUserDetails.cs

namespace GrpcCommon.Models
{
    public class JwtToken
    {
        public string? UserId { get; set; }
        public string? Exp { get; set; }
        public string? Iss { get; set; }
    }
}
namespace GrpcCommon.Models
{
    public class UserDetails
    {
        public string? UserName { get; set; }
        public int Age { get; set; }
        public IEnumerable<string>? Friends { get; set; }
    }
}

Helpers下新建JwtHelper.cs

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;

namespace GrpcCommon.Helpers
{
    public class JwtHelper
    {
        /// <summary>
        /// 頒發JWT Token
        /// </summary>
        /// <param name="securityKey"></param>
        /// <param name="accountName"></param>
        /// <returns></returns>
        public static string GenerateJwt(string securityKey, string accountName)
        {
            var claims = new List<Claim>
            {
                new Claim("userid", accountName)
            };

            //祕鑰 (SymmetricSecurityKey 對安全性的要求,金鑰的長度太短會報出異常)
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwt = new JwtSecurityToken(
                issuer: "https://ifcloud.com/zerotrust",
                claims: claims,
                expires: DateTime.Now.AddMinutes(1),
                signingCredentials: credentials);
            var jwtHandler = new JwtSecurityTokenHandler();
            var encodedJwt = jwtHandler.WriteToken(jwt);
            return encodedJwt;
        }

        /// <summary>
        /// 解析
        /// </summary>
        /// <param name="token"></param>
        /// <param name="securityKey"></param>
        /// <returns></returns>
        public static Tuple<bool, string> ValidateJwt(string token, string securityKey)
        {
            try
            {
                //對稱祕鑰
                SecurityKey key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(securityKey));
                //校驗token
                var validateParameter = new TokenValidationParameters()
                {
                    ValidateAudience = false,
                    ValidIssuer = "https://ifcloud.com/zerotrust",
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = key,
                    ClockSkew = TimeSpan.Zero//校驗過期時間必須加此屬性
                };
                var jwtToken = new JwtSecurityTokenHandler().ValidateToken(token, validateParameter, out _);
                var claimDic = new Dictionary<string, string>();

                foreach (var claim in jwtToken.Claims)
                {
                    claimDic.TryAdd(claim.Type, claim.Value);
                }

                var payLoad = JsonConvert.SerializeObject(claimDic);

                return new Tuple<bool, string>(true, payLoad);
            }
            catch (SecurityTokenExpiredException expired)
            {
                //token過期
                return new Tuple<bool, string>(false, expired.Message);
            }
            catch (SecurityTokenNoExpirationException noExpiration)
            {
                //token未設定過期時間
                return new Tuple<bool, string>(false, noExpiration.Message);
            }
            catch (SecurityTokenException tokenEx)
            {
                //表示token錯誤
                return new Tuple<bool, string>(false, tokenEx.Message);
            }
            catch (Exception err)
            {
                // 解析出錯
                Console.WriteLine(err.StackTrace);
                return new Tuple<bool, string>(false, err.Message);
            }
        }
    }
}

3.生成SSL證書(可跳過)

3.1 下載安裝openssl

參考文章:https://www.cnblogs.com/dingshaohua/p/12271280.html

3.2 生成證書金鑰

GrpcCommonCerts下右鍵開啟命令視窗輸入openssl

genrsa -out key.pem 2048

3.3 生成pem證書

req -new -x509 -key key.pem -out cert.pem -days 3650

3.4 pem證書轉換成pfx證書

pkcs12 -export -out cert.pfx -inkey key.pem -in cert.pem

4.搭建grpc伺服器

4.1 新建grpc服務

GrpcServer

4.2 新建資料夾

資料夾:Protos及其子資料夾Google

4.3 下載google protobuf檔案

https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-win64.zip
其他版本參考:https://github.com/protocolbuffers/protobuf/releases
下載不了的文章末尾有原始碼地址

下載解壓後將\include\google\protobuf中的所有檔案放在Protos下的Google

4.4 新建proto檔案

Protos下新建檔案example.proto

syntax = "proto3";

package example;
import "Protos/Google/struct.proto";

option csharp_namespace = "GrpcExample";

service ExampleServer {
	// Unary
	rpc UnaryCall (ExampleRequest) returns (ExampleResponse);

	// Server streaming
	rpc StreamingFromServer (ExampleRequest) returns (stream ExampleResponse);

	// Client streaming
	rpc StreamingFromClient (stream ExampleRequest) returns (ExampleResponse);

	// Bi-directional streaming
	rpc StreamingBothWays (stream ExampleRequest) returns (stream ExampleResponse);
}

message ExampleRequest {
	string securityKey = 1;
	string userId = 2;
	google.protobuf.Struct userDetail = 3;
	string token = 4;
}

message ExampleResponse {
	int32 code = 1;
	bool result = 2;
	string message = 3;
}

4.5 編譯生成Stub

GrpcServer專案右鍵編輯專案檔案新增內容

<ItemGroup>
	<Protobuf Include="Protos\example.proto" GrpcServices="Server" />
</ItemGroup>

4.6 新增ssl證書(可跳過)

修改Program.cs

builder.WebHost
    .ConfigureKestrel(serviceOpt =>
    {
        var httpPort = builder.Configuration.GetValue<int>("port:http");
        var httpsPort = builder.Configuration.GetValue<int>("port:https");
        serviceOpt.Listen(IPAddress.Any, httpPort, opt => opt.UseConnectionLogging());
        serviceOpt.Listen(IPAddress.Any, httpsPort, listenOpt =>
        {
            var enableSsl = builder.Configuration.GetValue<bool>("enableSsl");
            if (enableSsl)
            {
                listenOpt.UseHttps("Certs\\cert.pfx", "1234.com");
            }
            else
            {
                listenOpt.UseHttps();
            }

            listenOpt.UseConnectionLogging();
        });
    });

修改appsettings.json,新增設定項

  "port": {
    "http": 5000,
    "https": 7000
  },
  "enableSsl": true

4.7 新建服務類

ExampleService

using Grpc.Core;
using GrpcCommon.Helpers;
using GrpcCommon.Models;
using GrpcExampleServer;
using Newtonsoft.Json;

namespace GrpcServer.Services
{
    public class ExampleService : ExampleServer.ExampleServerBase
    {
        private readonly ILogger<ExampleService> _logger;

        public ExampleService(ILogger<ExampleService> logger)
        {
            _logger = logger;
        }

        public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context)
        {
            Console.WriteLine(request.ToString());
            _logger.LogInformation(request.ToString());
            var tokenRes = JwtHelper.ValidateJwt(request.Token, request.SecurityKey);

            // 正常響應使用者端一次
            ExampleResponse result;

            if (tokenRes.Item1)
            {
                var payLoad = JsonConvert.DeserializeObject<JwtToken>(tokenRes.Item2);
                if (payLoad == null)
                {
                    result = new ExampleResponse
                    {
                        Code = -1,
                        Result = false,
                        Message = "payLoad為空"
                    };
                }
                else
                {
                    if (!request.UserId.Equals(payLoad.UserId))
                    {
                        result = new ExampleResponse
                        {
                            Code = -1,
                            Result = false,
                            Message = "userid不匹配"
                        };
                    }
                    else
                    {
                        var userDetail = JsonConvert.DeserializeObject<UserDetails>(request.UserDetail.Fields.ToString());
                        result = new ExampleResponse
                        {
                            Code = 200,
                            Result = true,
                            Message = $"UnaryCall 單次響應: {request.UserId},{userDetail?.UserName}"
                        };
                    }
                }
            }
            else
            {
                // 正常響應使用者端一次
                result = new ExampleResponse
                {
                    Code = -1,
                    Result = false,
                    Message = tokenRes.Item2
                };
            }
            return Task.FromResult(result);
        }

        public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
        {
            // 無限響應使用者端
            while (!context.CancellationToken.IsCancellationRequested)
            {
                await responseStream.WriteAsync(new ExampleResponse
                {
                    Code = 200,
                    Result = true,
                    Message = $"StreamingFromServer 無限響應: {Guid.NewGuid()}"
                });
                await Task.Delay(TimeSpan.FromSeconds(3), context.CancellationToken);
            }
        }

        public override async Task<ExampleResponse> StreamingFromClient(IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context)
        {
            // 處理請求
            await foreach (var req in requestStream.ReadAllAsync())
            {
                Console.WriteLine(req.UserId);
            }

            // 響應使用者端
            return new ExampleResponse
            {
                Code = 200,
                Result = true,
                Message = $"StreamingFromClient 單次響應: {Guid.NewGuid()}"
            };
        }

        public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context)
        {
            // 伺服器響應使用者端一次
            // 處理請求
            //await foreach (var req in requestStream.ReadAllAsync())
            //{
            //    Console.WriteLine(req.UserName);
            //}

            // 請求處理完成之後只響應一次
            //await responseStream.WriteAsync(new ExampleResponse
            //{
            //    Code = 200,
            //    Result = true,
            //    Message = $"StreamingBothWays 單次響應: {Guid.NewGuid()}"
            //});
            //await Task.Delay(TimeSpan.FromSeconds(3), context.CancellationToken);

            // 伺服器響應使用者端多次
            // 處理請求
            var readTask = Task.Run(async () =>
            {
                await foreach (var req in requestStream.ReadAllAsync())
                {
                    Console.WriteLine(req.UserId);
                }
            });

            // 請求未處理完之前一直響應
            while (!readTask.IsCompleted)
            {
                await responseStream.WriteAsync(new ExampleResponse
                {
                    Code = 200,
                    Result = true,
                    Message = $"StreamingBothWays 請求處理完之前的響應: {Guid.NewGuid()}"
                });
                await Task.Delay(TimeSpan.FromSeconds(3), context.CancellationToken);
            }

            // 也可以無限響應使用者端
            //while (!context.CancellationToken.IsCancellationRequested)
            //{
            //    await responseStream.WriteAsync(new ExampleResponse
            //    {
            //        Code = 200,
            //        Result = true,
            //        Message = $"StreamingFromServer 無限響應: {Guid.NewGuid()}"
            //    });
            //    await Task.Delay(TimeSpan.FromSeconds(3), context.CancellationToken);
            //}
        }
    }
}

5.搭建grpc使用者端

5.1 新建控制檯程式

GrpcClient

5.2 拷貝資料夾

GrpcServer下的Protos拷貝一份到GrpcClient

5.3 安裝依賴包

Google.Protobuf 3.21.12,Grpc.Net.Client 2.51.0,Grpc.Tools 2.51.0,Newtonsoft.Json 13.0.2

5.4 編譯生成Stub

GrpcServer專案右鍵編輯專案檔案新增內容,注意這裡是Client

<ItemGroup>
	<Protobuf Include="Protos\example.proto" GrpcServices="Client" />
</ItemGroup>

5.5 新建測試類

ExampleTest.cs

using System.Security.Cryptography.X509Certificates;
using Grpc.Net.Client;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using GrpcCommon.Helpers;
using GrpcExample;

namespace GrpcClient.Test
{
    internal class ExampleTest
    {
        public static void Run()
        {
            // 常規請求響應
            UnaryCall();

            // 伺服器流響應
            StreamingFromServer();

            // 使用者端流響應
            StreamingFromClient();

            // 雙向流響應
            StreamingBothWays();
        }

        /// <summary>
        /// 建立使用者端連結
        /// </summary>
        /// <param name="enableSsl"></param>
        /// <returns></returns>
        private static ExampleServer.ExampleServerClient CreateClient(bool enableSsl = true)
        {
            GrpcChannel channel;
            if (enableSsl)
            {
                const string serverUrl = "https://localhost:7000";
                Console.WriteLine($"嘗試連結伺服器,{serverUrl}");

                var handler = new HttpClientHandler();
                // 新增證書
                handler.ClientCertificates.Add(new X509Certificate2("Certs\\cert.pfx", "1234.com"));

                // 忽略證書
                handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
                channel = GrpcChannel.ForAddress(serverUrl, new GrpcChannelOptions
                {
                    HttpClient = new HttpClient(handler)
                });
            }
            else
            {
                const string serverUrl = "http://localhost:5000";
                Console.WriteLine($"嘗試連結伺服器,{serverUrl}");
                channel = GrpcChannel.ForAddress(serverUrl);
            }

            Console.WriteLine("伺服器連結成功");
            return new ExampleServer.ExampleServerClient(channel);
        }

        private static async void UnaryCall()
        {
            var client = CreateClient();
            const string securityKey = "Dennis!@#$%^123456.com";
            var userId = Guid.NewGuid().ToString();
            var token = JwtHelper.GenerateJwt(securityKey, userId);
            var result = await client.UnaryCallAsync(new ExampleRequest
            {
                SecurityKey = securityKey,
                UserId = "Dennis",
                UserDetail = new Struct
                {
                    Fields =
                    {
                        ["userName"] = Value.ForString("Dennis"),
                        ["age"] = Value.ForString("18"),
                        ["friends"] = Value.ForList(new Value
                        {
                            ListValue = new ListValue
                            {
                                Values =
                                {
                                    new List<Value>
                                    {
                                        Value.ForString("Roger"),
                                        Value.ForString("YueBe")
                                    }
                                }
                            }
                        })
                    }
                },
                Token = token
            });
            Console.WriteLine($"Code={result.Code},Result={result.Result},Message={result.Message}");
        }

        private static async void StreamingFromServer()
        {
            var client = CreateClient();
            var result = client.StreamingFromServer(new ExampleRequest
            {
                UserId = "Dennis"
            });

            await foreach (var resp in result.ResponseStream.ReadAllAsync())
            {
                Console.WriteLine($"Code={resp.Code},Result={resp.Result},Message={resp.Message}");
            }
        }

        private static async void StreamingFromClient()
        {
            var client = CreateClient();
            var result = client.StreamingFromClient();

            // 傳送請求
            for (var i = 0; i < 5; i++)
            {
                await result.RequestStream.WriteAsync(new ExampleRequest
                {
                    UserId = $"StreamingFromClient 第{i}次請求"
                });
                await Task.Delay(TimeSpan.FromSeconds(1));
            }

            // 等待請求傳送完畢
            await result.RequestStream.CompleteAsync();

            var resp = result.ResponseAsync.Result;
            Console.WriteLine($"Code={resp.Code},Result={resp.Result},Message={resp.Message}");
        }

        private static async void StreamingBothWays()
        {
            var client = CreateClient();
            var result = client.StreamingBothWays();

            // 傳送請求
            for (var i = 0; i < 5; i++)
            {
                await result.RequestStream.WriteAsync(new ExampleRequest
                {
                    UserId = $"StreamingBothWays 第{i}次請求"
                });
                await Task.Delay(TimeSpan.FromSeconds(1));
            }

            // 處理響應
            var respTask = Task.Run(async () =>
            {
                await foreach (var resp in result.ResponseStream.ReadAllAsync())
                {
                    Console.WriteLine($"Code={resp.Code},Result={resp.Result},Message={resp.Message}");
                }
            });

            // 等待請求傳送完畢
            await result.RequestStream.CompleteAsync();

            // 等待響應處理
            await respTask;
        }
    }
}

5.6 修改程式入口

Program.cs

using GrpcClient.Test;
using Microsoft.Extensions.Hosting;

// Example測試
ExampleTest.Run();

Console.WriteLine("==================");
Console.WriteLine("按Ctrl+C停止程式");
Console.WriteLine("==================");

// 監聽Ctrl+C
await new HostBuilder().RunConsoleAsync();

6.執行專案

6.1 拷貝證書

把整個Certs資料夾分別拷貝到GrpcServerGrpcClient下的\bin\Debug\Certs

6.2 啟動程式

先執行GrpcServer在執行GrpcClient即可

6.3 偵錯

右鍵解決方案-->屬性-->啟動專案-->選擇多個啟動專案-->F5偵錯即可

7.原始碼地址

https://gitee.com/dennisdong/net-grpc