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.cs
和UserDetails.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 生成證書金鑰
在GrpcCommon
的Certs
下右鍵開啟命令視窗輸入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
資料夾分別複製到GrpcServer
和GrpcClient
下的\bin\Debug\Certs
6.2 啟動程式
先執行GrpcServer
在執行GrpcClient
即可
6.3 除錯
右鍵解決方案-->屬性-->啟動專案-->選擇多個啟動專案-->F5除錯即可