ASP.Net Core 3.1 使用gRPC入門指南
- 2020 年 12 月 3 日
- 筆記
主要參考文章微軟官方文檔: //docs.microsoft.com/zh-cn/aspnet/core/grpc/client?view=aspnetcore-3.1
此外還參考了文章 //www.cnblogs.com/stulzq/p/11581967.html並寫了一個demo: //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 證書,請選擇「是」 。
將顯示以下對話框:
如果你同意信任開發證書,請選擇「是」。
日誌顯示該服務正在偵聽 //localhost:5001
。
控制台顯示如下:
info: Microsoft.Hosting.Lifetime[0]
Now listening on: //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; }
example.proto
其中:
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); //} } } }
ExampleService
在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 //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: //go.microsoft.com/fwlink/?linkid=2086909"); }); }); } } }
Startup
就此 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("//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("//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("//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(); } } }
View Code
程式碼鏈接地址: //files.cnblogs.com/files/hudean/GrpcGreeter.zip