ASP.Net Core 3.1 使用gRPC入門指南

青春似雨後霓虹發表於2020-12-03

主要參考文章微軟官方文件: 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 服務”,然後選擇“下一步” :

    Visual Studio 中的“建立新專案”對話方塊

  • 將專案命名為 GrpcGreeter。 將專案命名為“GrpcGreeter”非常重要,這樣在複製和貼上程式碼時名稱空間就會匹配。

  • 選擇“建立”。

  • 在“建立新 gRPC 服務”對話方塊中:

    • 選擇“gRPC 服務”模板。
    • 選擇“建立”。

執行服務

  • 按 Ctrl+F5 以在不使用除錯程式的情況下執行。

    Visual Studio 會顯示以下對話方塊:

    此專案配置為使用 SSL。

    如果信任 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檔案內容如下

ASP.Net Core 3.1 使用gRPC入門指南
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類,程式碼如下:

ASP.Net Core 3.1 使用gRPC入門指南
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>();   

ASP.Net Core 3.1 使用gRPC入門指南
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");
                });
            });
        }
    }
}
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> 元素的項組:

XML
<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客戶端寫呼叫服務端程式碼,程式碼如下:

ASP.Net Core 3.1 使用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();
        }
    }
}
View Code

 

程式碼連結地址: https://files.cnblogs.com/files/hudean/GrpcGreeter.zip

 

相關文章