主要參考文章微軟官方文件: https://docs.microsoft.com/zh-cn/aspnet/core/grpc/client?view=aspnetcore-3.1
此外還參考了文章 https://www.cnblogs.com/stulzq/p/11581967.html並寫了一個demo: https://files.cnblogs.com/files/hudean/GrpcDemo.zip
一、簡介
gRPC 是一種與語言無關的高效能遠端過程呼叫 (RPC) 框架。
gRPC 的主要優點是:
- 現代高效能輕量級 RPC 框架。
- 協定優先 API 開發,預設使用協議緩衝區,允許與語言無關的實現。
- 可用於多種語言的工具,以生成強型別伺服器和客戶端。
- 支援客戶端、伺服器和雙向流式處理呼叫。
- 使用 Protobuf 二進位制序列化減少對網路的使用。
這些優點使 gRPC 適用於:
- 效率至關重要的輕量級微服務。
- 需要多種語言用於開發的 Polyglot 系統。
- 需要處理流式處理請求或響應的點對點實時服務。
二、建立 gRPC 服務
-
啟動 Visual Studio 並選擇“建立新專案”。 或者,從 Visual Studio“檔案”選單中選擇“新建” > “專案” 。
-
在“建立新專案”對話方塊中,選擇“gRPC 服務”,然後選擇“下一步” :
-
將專案命名為 GrpcGreeter。 將專案命名為“GrpcGreeter”非常重要,這樣在複製和貼上程式碼時名稱空間就會匹配。
-
選擇“建立”。
-
在“建立新 gRPC 服務”對話方塊中:
- 選擇“gRPC 服務”模板。
- 選擇“建立”。
執行服務
-
按 Ctrl+F5 以在不使用除錯程式的情況下執行。
Visual Studio 會顯示以下對話方塊:
如果信任 IIS Express SSL 證書,請選擇“是” 。
將顯示以下對話方塊:
如果你同意信任開發證書,請選擇“是”。
日誌顯示該服務正在偵聽 https://localhost:5001
。
控制檯顯示如下:
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
備註
gRPC 模板配置為使用傳輸層安全性 (TLS)。 gRPC 客戶端需要使用 HTTPS 呼叫伺服器。
macOS 不支援 ASP.NET Core gRPC 及 TLS。 在 macOS 上成功執行 gRPC 服務需要其他配置。
檢查專案檔案
GrpcGreeter 專案檔案:
- greet.proto : Protos/greet.proto 檔案定義
Greeter
gRPC,且用於生成 gRPC 伺服器資產。 - Services 資料夾:包含
Greeter
服務的實現。 - appSettings.json :包含配置資料,例如 Kestrel 使用的協議。
- Program.cs:包含 gRPC 服務的入口點。
Startup.cs :包含配置應用行為的程式碼。
上述準備工作完成,開始寫gRPC服務端程式碼!
example.proto檔案內容如下
syntax = "proto3"; option csharp_namespace = "GrpcGreeter"; package example; service exampler { // Unarys 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 { int32 id = 1; string name = 2; } message ExampleResponse { string msg = 1; }
其中:
syntax = "proto3";是使用 proto3 語法,protocol buffer 編譯器預設使用的是 proto2 。 這必須是檔案的非空、非註釋的第一行。
對於 C#語言,編譯器會為每一個.proto 檔案建立一個.cs 檔案,為每一個訊息型別都建立一個類來操作。
option csharp_namespace = "GrpcGreeter";是c#程式碼的名稱空間
package example;包的名稱空間
service exampler 是服務的名字
rpc UnaryCall (ExampleRequest) returns (ExampleResponse); 意思是rpc呼叫方法 UnaryCall 方法引數是ExampleRequest型別 返回值是ExampleResponse 型別
message ExampleRequest { int32 id = 1; string name = 2; }
指定欄位型別 在上面的例子中,所有欄位都是標量型別:一個整型(id),一個string型別(name)。當然,你也可以為欄位指定其他的合成型別,包括列舉(enumerations)或其他訊息型別。 分配標識號 我們可以看到在上面定義的訊息中,給每個欄位都定義了唯一的數字值。這些數字是用來在訊息的二進位制格式中識別各個欄位的,一旦開始使用就不能夠再改變。注:[1,15]之內的標識號在編碼的時候會佔用一個位元組。[16,2047]之內的標識號則佔用2個位元組。所以應該為那些頻繁出現的訊息元素保留
[1,15]之內的標識號。切記:要為將來有可能新增的、頻繁出現的標識號預留一些標識號。 最小的標識號可以從1開始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的標識號, Protobuf協議實現中對這些進行了預留。如果非要在.proto檔案中使用這些預留標識號,編譯時就會報警。類似地,你不能使用之前保留的任何識別符號。 指定欄位規則 訊息的欄位可以是一下情況之一: singular(預設):一個格式良好的訊息可以包含該段可以出現 0 或 1 次(不能大於 1 次)。 repeated:在一個格式良好的訊息中,這種欄位可以重複任意多次(包括0次)。重複的值的順序會被保留。 預設情況下,標量數值型別的repeated欄位使用packed的編碼方式。
在GrpcGreeter.csproj檔案新增:
<ItemGroup>
<Protobuf Include="Protos\example.proto" GrpcServices="Server" />
</ItemGroup>
點選儲存
在Services資料夾下新增ExampleService類,程式碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Grpc.Core; namespace GrpcGreeter { public class ExampleService :exampler.examplerBase { /// <summary> /// 一元方法以引數的形式獲取請求訊息,並返回響應。 返回響應時,一元呼叫完成。 /// </summary> /// <param name="request"></param> /// <param name="context"></param> /// <returns></returns> public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context) { // return base.UnaryCall(request, context); return Task.FromResult(new ExampleResponse { Msg = "id :" + request.Id + "name : " + request.Name + " hello" }) ; } /// <summary> /// 伺服器流式處理方法 /// 伺服器流式處理方法以引數的形式獲取請求訊息。 由於可以將多個訊息流式傳輸回撥用方,因此可使用 responseStream.WriteAsync 傳送響應 /// 訊息。 當方法返回時,伺服器流式處理呼叫完成。 /// </summary> /// <param name="request"></param> /// <param name="responseStream"></param> /// <param name="context"></param> /// <returns></returns> public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { /** * 伺服器流式處理方法啟動後,客戶端無法傳送其他訊息或資料。 某些流式處理方法設計為永久執行。 對於連續流式處理方法,客戶端可以在*再需要呼叫時將其取消。 當發生取消時,客戶端會將訊號傳送到伺服器,並引發 ServerCallContext.CancellationToken。 應在伺服器上通過非同步方法使用 CancellationToken 標記,以實現以下目的: 所有非同步工作都與流式處理呼叫一起取消。 該方法快速退出。 **/ //return base.StreamingFromServer(request, responseStream, context); //for (int i = 0; i < 5; i++) //{ // await responseStream.WriteAsync(new ExampleResponse { Msg = "我是服務端流for:" + i }); // await Task.Delay(TimeSpan.FromSeconds(1)); //} int index = 0; while (!context.CancellationToken.IsCancellationRequested) { index++; await responseStream.WriteAsync(new ExampleResponse { Msg = "我是服務端流while" + index+" "+request.Id+" "+request.Name }); await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken); } } /// <summary> /// 客戶端流式處理方法 /// 客戶端流式處理方法在該方法沒有接收訊息的情況下啟動。 requestStream 引數用於從客戶端讀取訊息。 返回響應訊息時,客戶端流式處理呼叫 /// 完成: /// </summary> /// <param name="requestStream"></param> /// <param name="context"></param> /// <returns></returns> public override async Task<ExampleResponse> StreamingFromClient(IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context) { // return base.StreamingFromClient(requestStream, context); List<string> list = new List<string>(); while (await requestStream.MoveNext()) { //var message = requestStream.Current; var id = requestStream.Current.Id; var name = requestStream.Current.Name; list.Add($"{id}-{name}"); // ... } return new ExampleResponse() { Msg = "我是客戶端流while"+string.Join(',',list) }; //await foreach (var message in requestStream.ReadAllAsync()) //{ // // ... //} // return new ExampleResponse() { Msg= "我是客戶端流foreach" }; } /// <summary> /// 雙向流式處理方法 /// 雙向流式處理方法在該方法沒有接收到訊息的情況下啟動。 requestStream 引數用於從客戶端讀取訊息。 /// 該方法可選擇使用 responseStream.WriteAsync 傳送訊息。 當方法返回時,雙向流式處理呼叫完成: /// </summary> /// <param name="requestStream"></param> /// <param name="responseStream"></param> /// <param name="context"></param> /// <returns></returns> public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { //return base.StreamingBothWays(requestStream, responseStream, context); await foreach (var message in requestStream.ReadAllAsync()) { string str= message.Id + " " + message.Name; await responseStream.WriteAsync(new ExampleResponse() { Msg="我是雙向流:"+ str }); } //// Read requests in a background task. //var readTask = Task.Run(async () => //{ // await foreach (var message in requestStream.ReadAllAsync()) // { // // Process request. // string str = message.Id + " " + message.Name; // } //}); //// Send responses until the client signals that it is complete. //while (!readTask.IsCompleted) //{ // await responseStream.WriteAsync(new ExampleResponse()); // await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken); //} } } }
在Startup類裡Configure中加入一個這個 endpoints.MapGrpcService<ExampleService>();
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.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace GrpcGreeter { public class Startup { // 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.AddGrpc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>(); endpoints.MapGrpcService<ExampleService>(); endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); }); }); } } }
就此 gRPC服務端程式碼完成了
在 .NET 控制檯應用中建立 gRPC 客戶端
- 開啟 Visual Studio 的第二個例項並選擇“建立新專案”。
- 在“建立新專案”對話方塊中,選擇“控制檯應用(.NET Core)”,然後選擇“下一步” 。
- 在“專案名稱”文字框中,輸入“GrpcGreeterClient”,然後選擇“建立” 。
新增所需的包
gRPC 客戶端專案需要以下包:
- Grpc.Net.Client,其中包含 .NET Core 客戶端。
- Google.Protobuf 包含適用於 C# 的 Protobuf 訊息。
- Grpc.Tools 包含適用於 Protobuf 檔案的 C# 工具支援。 執行時不需要工具包,因此依賴項標記為
PrivateAssets="All"
。
通過包管理器控制檯 (PMC) 或管理 NuGet 包來安裝包。
用於安裝包的 PMC 選項
-
從 Visual Studio 中,依次選擇“工具” > “NuGet 包管理器” > “包管理器控制檯”
-
從“包管理器控制檯”視窗中,執行
cd GrpcGreeterClient
以將目錄更改為包含 GrpcGreeterClient.csproj 檔案的資料夾。
執行以下命令:
PowerShell
Install-Package Grpc.Net.Client Install-Package Google.Protobuf Install-Package Grpc.Tools
管理 NuGet 包選項以安裝包
- 右鍵單擊“解決方案資源管理器” > “管理 NuGet 包”中的專案 。
- 選擇“瀏覽”選項卡。
- 在搜尋框中輸入 Grpc.Net.Client。
- 從“瀏覽”選項卡中選擇“Grpc.Net.Client”包,然後選擇“安裝” 。
- 為
Google.Protobuf
和Grpc.Tools
重複這些步驟。
新增 greet.proto
-
在 gRPC 客戶端專案中建立 Protos 資料夾。
-
從 gRPC Greeter 服務將 Protos\greet.proto 檔案複製到 gRPC 客戶端專案。
-
將
greet.proto
檔案中的名稱空間更新為專案的名稱空間:option csharp_namespace = "GrpcGreeterClient";
-
編輯 GrpcGreeterClient.csproj 專案檔案:
右鍵單擊專案,並選擇“編輯專案檔案”。
新增具有引用 greet.proto 檔案的 <Protobuf>
元素的項組:
<ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Client" /> </ItemGroup>
建立 Greeter 客戶端
構建客戶端專案,以在 GrpcGreeter
名稱空間中建立型別。 GrpcGreeter
型別是由生成程式自動生成的。
使用以下程式碼更新 gRPC 客戶端的 Program.cs 檔案:
using System; using System.Net.Http; using System.Threading.Tasks; using Grpc.Net.Client; namespace GrpcGreeterClient { class Program { static async Task Main(string[] args) { // The port number(5001) must match the port of the gRPC server. using var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new Greeter.GreeterClient(channel); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Console.WriteLine("Greeting: " + reply.Message); Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } } }
新增內容如下:
<ItemGroup> <Protobuf Include="Protos\example.proto" GrpcServices="Server" /> <Protobuf Include="Protos\greet.proto" GrpcServices="Server" /> </ItemGroup> <ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Client" /> </ItemGroup> <ItemGroup> <Protobuf Include="Protos\example.proto" GrpcServices="Client" /> </ItemGroup>
在gRPC客戶端寫呼叫服務端程式碼,程式碼如下:
using Grpc.Core; using Grpc.Net.Client; using GrpcGreeter; using System; using System.Threading; using System.Threading.Tasks; namespace GrpcGreeterClient { class Program { //static void Main(string[] args) //{ // Console.WriteLine("Hello World!"); //} //static async Task Main(string[] args) //{ // // The port number(5001) must match the port of the gRPC server. // using var channel = GrpcChannel.ForAddress("https://localhost:5001"); // var client = new Greeter.GreeterClient(channel); // var reply = await client.SayHelloAsync( // new HelloRequest { Name = "GreeterClient" }); // Console.WriteLine("Greeting: " + reply.Message); // Console.WriteLine("Press any key to exit..."); // Console.ReadKey(); //} static async Task Main(string[] args) { // The port number(5001) must match the port of the gRPC server. using var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new exampler.examplerClient(channel); #region 一元呼叫 //var reply = await client.UnaryCallAsync(new ExampleRequest { Id = 1, Name = "hda" }); //Console.WriteLine("Greeting: " + reply.Msg); #endregion 一元呼叫 #region 伺服器流式處理呼叫 //using var call = client.StreamingFromServer(new ExampleRequest { Id = 1, Name = "hda" }); //while (await call.ResponseStream.MoveNext(CancellationToken.None)) //{ // Console.WriteLine("Greeting: " + call.ResponseStream.Current.Msg); //} //如果使用 C# 8 或更高版本,則可使用 await foreach 語法來讀取訊息。 IAsyncStreamReader<T>.ReadAllAsync() 擴充套件方法讀取響應資料流中的所有訊息: //await foreach (var response in call.ResponseStream.ReadAllAsync()) //{ // Console.WriteLine("Greeting: " + response.Msg); // // "Greeting: Hello World" is written multiple times //} #endregion 伺服器流式處理呼叫 #region 客戶端流式處理呼叫 //using var call = client.StreamingFromClient(); //for (int i = 0; i < 5; i++) //{ // await call.RequestStream.WriteAsync(new ExampleRequest { Id = i, Name = "hda" + i }); //} //await call.RequestStream.CompleteAsync(); //var response = await call; //Console.WriteLine($"Count: {response.Msg}"); #endregion 客戶端流式處理呼叫 #region 雙向流式處理呼叫 //通過呼叫 EchoClient.Echo 啟動新的雙向流式呼叫。 //使用 ResponseStream.ReadAllAsync() 建立用於從服務中讀取訊息的後臺任務。 //使用 RequestStream.WriteAsync 將訊息傳送到伺服器。 //使用 RequestStream.CompleteAsync() 通知伺服器它已傳送訊息。 //等待直到後臺任務已讀取所有傳入訊息。 //雙向流式處理呼叫期間,客戶端和服務可在任何時間互相傳送訊息。 與雙向呼叫互動的最佳客戶端邏輯因服務邏輯而異。 using var call = client.StreamingBothWays(); Console.WriteLine("Starting background task to receive messages"); var readTask = Task.Run(async () => { await foreach (var response in call.ResponseStream.ReadAllAsync()) { Console.WriteLine(response.Msg); // Echo messages sent to the service } }); Console.WriteLine("Starting to send messages"); Console.WriteLine("Type a message to echo then press enter."); while (true) { var result = Console.ReadLine(); if (string.IsNullOrEmpty(result)) { break; } await call.RequestStream.WriteAsync(new ExampleRequest { Id=1,Name= result }); } Console.WriteLine("Disconnecting"); await call.RequestStream.CompleteAsync(); await readTask; #endregion 雙向流式處理呼叫 Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } } }
程式碼連結地址: https://files.cnblogs.com/files/hudean/GrpcGreeter.zip