gRPC 與 .NET 入門

gRPC 與 .NET 入門

從本質上來講,API 就是伺服器和用戶端之間的一個協定,指定了伺服器如何基於用戶端的要求提供特定的資料。

最後更新 2022/2/18 下午9:55
Mohamad Lawand
預計閱讀 17 分鐘
分類
.NET
標籤
.NET C# gRPC
  • 作者 | Mohamad Lawand
  • 譯者 | 張衛濱
  • 策劃 | 丁曉昀

從本質上來講,API 就是伺服器和客戶端之間的一個協定,指定了伺服器如何基於客戶端的請求提供特定的資料。

在構建 API 的時候,我們會想到不同的技術。根據需求不同,我們所選擇的開發 API 的技術也會隨之發生變化。在目前的這個時代,主要有兩種用於建立 API 的技術:

  • gRPC
  • REST

這兩種技術都使用 HTTP 作為傳輸機制。儘管使用了相同的底層傳輸機制,但是它們的實現卻是完全不同的。

我們先對比一下這兩項技術,然後再深入瞭解 gRPC。

REST

REST 是一套架構約束,而不是協定或標準。API 開發人員可以使用各種方式來實現 REST。

為了讓一個 API 被認作是 RESTful 的,我們需要遵循一些約束條件:

  • 客戶端 - 伺服器端架構:所有的請求必須使用 HTTP 作為傳輸機制;

  • 無狀態:API 應該是無狀態的,這意味著,伺服器不應該在伺服器端儲存任何關於客戶端會話的狀態。從客戶端到伺服器的每個請求都必須要包含所有必要的資訊以理解該請求。伺服器不能使用任何在伺服器端所儲存的上下文。

  • 可快取:客戶端 - 伺服器間流過的所有資料必須都是可快取的,這意味著它們可以被儲存起來,以便於後續檢索和使用。

  • 統一介面:客戶端和伺服器之間必須有一個介面,以便於資訊以標準的形式進行傳輸。

  • 分層的系統:在客戶端的請求以及伺服器端的響應之間所涉及的所有伺服器必須要按照它們的職責來進行組織,組織方式不能影響到請求或響應。

gRPC

gRPC 構建在 RPC(遠端程序呼叫,Remote Procedure Call)協定堅實的基礎之上,它也進入了 API 的領域之中。gRPC 是由谷歌開發的免費、開源的框架,它使用 HTTP/2 進行 API 通訊,為 API 的設計者隱藏了 HTTP 實現。

gRPC 有很多特徵,所以不管是在微服務還是在 web/ 行動 API 通訊方面,都使其成為下一代 web 應用的基礎模組:

  • 互操作性(Interoperability):不管當前的 HTTP 版本是什麼,無論基礎設施如何變化(比如,從 HTTP 2 升級到 HTTP 3),協定必須能夠適應和改變。

  • 分層架構:技術棧的核心面必須能夠獨立地演進和升級,而不會破壞任何使用它的應用程式。

  • 負載的中立性(Agnostic):不同的服務可能會需要不同的訊息類型和編碼,比如 Protobuf(Protocol buffer)、JSON、XML 等。gRPC 支援所有的這些格式,並且能夠透過利用可插拔的壓縮機制來壓縮負載。

  • 流:gRPC 允許將大的資料集以流的方式從伺服器中轉到客戶端,反之亦然。

  • 可插拔(Pluggable):gRPC 支援按需插入不同的功能和服務以滿足我們的需求,比如健康檢查、故障恢復和負載均衡。框架實現提供了擴充套件點,允許我們插入這些功能。

與 docker 和 kubernetes 類似,gRPC 是雲原生基金會(CNCF)的一部分。

簡而言之,gRPC 的好處包括:

  • 現代、快速

  • 開源

  • 利用 HTTP/2

  • 語言中立

  • 易於新增認證、日誌

為了使用 gRPC:

  • 我們需要使用 Protobuf(Protocol Buffer)來定義訊息和服務

  • gRPC 程式碼會自動生成,我們則需要提供具體的實現。

  • 不管是在伺服器端還是在客戶端,.proto 檔案都能支援 12 種不同的語言。

預設情況下,gRPC 會使用谷歌開源的 Protocol Buffers 機制來進行結構化資料的序列化:

  • 它是語言中立的

  • 能夠為任何現代程式語言生成程式碼

  • 資料傳輸是二進位制和高效的

  • 高度可擴充套件

  • 允許我們傳送大量的資料

  • 允許我們擴充套件和演進 API

案例學習:

在如今的技術趨勢下,比較現代的方式是構建微服務。在本例中,我們學習一下構建航空售票系統的過程:

上圖展現了一個基於微服務的航空售票系統。在這裡,有幾個與這種型別的架構相關的關鍵點,我們需要注意:

  • 微服務通常是由不同的語言構建的。那麼我們可以說,預訂管理服務可以基於.NET 構建,支付處理可以是基於 Java 的,而乘客資訊則是使用 Node.js 的。

  • 每個服務都有不同的業務功能。

假設我們現在有使用不同語言編寫的微服務,它們之間要互相進行交流。當這些微服務想要交換資訊的時候,它們需要就一些事情達成共識,比如:

  • 交換資料的 API

  • 資料格式

  • 錯誤格式

  • 訪問速度限制

REST 是最流行的構建 API 的方案。但是,這個決策取決於很多與我們的實現相關的架構考量:

  • 設計資料模型的型別;

  • 端點會是什麼樣子;

  • 錯誤該如何進行處理;

  • 一個客戶端可以進行多少次呼叫;

  • 授權是如何實現的。

考慮到這些因素,我們再來看一�� gRPC 和 REST 的差異:

gRPC

  • 契約優先的 API 開發方式:契約(服務和訊息)是在*.proto 檔案中定義的,它們是 gRPC 的核心。這是以一種語言中立的方式來定義 API。這些檔案隨後可以被其他程式語言用來生成程式碼(如強型別的客戶端和訊息類)。

  • 內容是二進位的:HTTP/2 和 Protobuf 是二進位的協定,內容是為計算機和高效能而設計的。

  • gRPC 的設計隱藏了遠端操作的複雜性。透過使用 gRPC 庫和相關的程式碼生成,我們不需要關心路由、頭資訊和序列化等問題。當需要在客戶端呼叫一個方法時,我們只需要呼叫對應的方法就可以了。

  • gRPC 支援雙向的非同步流:某個 gRPC 呼叫建立流之後,客戶端和伺服器都能在任意時間向對方傳送非同步流。伺服器流和客戶端流(在這種情況下,只有響應或請求中的某一個是流)也是支援的。

  • gRPC 是為分散式應用的高效能和高生產率而設計的。

REST API

  • 內容優先的 API 開發方式(URL、HTTP 方法、JSON):注重可讀性和格式化。

  • 內容是基於文字的(HTTP 1.1 和 JSON),所以是人類可讀的。這樣造成的結果就是,它們非常適合進行除錯,但是對效能並不友好。

  • 更加強調 HTTP。我們需要考慮低層級的一些問題,這是一件好事兒,因為我們對 HTTP 請求有很大的控制權。

  • 面向 CRUD。

  • 最廣泛的受眾:每臺計算機都能使用 HTTP/1.1 和 JSON,易於上手。

基於這些對比,我們可以看到這兩種方式各有其優點。但是,我們可以看到,gRPC 為基於微服務的場景提供了一組強大的特性。

使用 gRPC 建立一個伺服器 - 客戶端應用

在開始編碼之前,我們在自己的計算機上安裝以下軟體:

軟體安裝完成之後,我們需要建立專案結構(在本文中,我們將在終端 / 命令列中直接使用 dotnet 命令):

dotnet new grpc -n GrpcService

我們還需要設定 SSL 信任:

dotnet dev-certs https --trust

接下來,我們在 VS Code 開啟這個新專案,看一下都建立了哪些內容。我們可以看到,我們自動有了如下的內容:

  • Protos 資料夾

  • Services 資料夾

在 Protos 資料夾中,我們有一個 greet.proto 檔案。正如我們在前文中所提到的,.proto 能夠以 語言中立的方式 來定義 API。

從這個檔案中,我們可以看到,它包含一個 Greeter 服務和一個 SayHello 方法。我們可以將 Greeter 服務視為控制器,將 SayHello 方法視為一個動作。.proto 檔案的內容如下所示:

// 宣告我們可以使用的最新模式
syntax = "proto3";

// 為該 proto 定義命名空間,通常與我們的 Grpc 伺服器相同
option csharp_namespace = "GrpcService";

package greet;

// 我們可以把一個服務看做一個類
service Greeter {
  // 傳送問候語
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// 請求訊息類似於 C# 中的一個模型,其中會定義屬性
// 這裡的數字用來對屬性進行排序
message HelloRequest {
  string name = 1;
}

// 響應訊息包含了問候語
message HelloReply {
  string message = 1;
}

SayHello 方法接收一個 HelloRequest(這是一個訊息)並返回一個 HelloReply(這也是一個訊息)。

在 GreeterService 檔案中,我們可以看到有一個 GreeterService 類,它繼承自 Greeter.GreeterBase,後者是由.proto 檔案自動生成的。

在 SayHello 方法中,我們會接收一個請求(HelloRequest)並返回一個響應(HelloReply)。它們也是由.proto 檔案自動為我們生成的。

程式碼自動生成會基於.proto 檔案定義為我們生成所需的檔案。gRPC 在程式碼生成、路由和序列化方面為我們做了所有繁重的工作。我們所需要做的就是實現基類並覆蓋方法的實現。

接下來,我們嘗試執行 gRPC 服務:

dotnet run

從自動生成的端點的結果中可以看到,我們不能像使用 web 瀏覽器作為 REST 的客戶端那樣使用 gRPC。在這種情況下,我們需要建立一個 gRPC 客戶端與服務進行通訊。對於我們的客戶端來講,gRPC 也需要.proto 檔案,因為它是一個 契約優先的 RPC 框架。目前,我們的 web 瀏覽器對客戶端(我們並沒有.proto 檔案)一無所知,所以它不知道如何處理請求。

我們建立名為 customers.proto 的自定義.proto 檔案。這個檔案必須要在 Protos 資料夾中建立,它的內容如下所示:

syntax = "proto3";

option csharp_namespace = "GrpcService";

package customers;

service Customer {
    rpc GetCustomerInfo (CustomerFindModel) returns (CustomerDataModel);
}

message CustomerFindModel {
    int32 userId = 1; // bool, int32, float, double, string
}

message CustomerDataModel {
    string firstName = 1;
    string lastName = 2;
}

儲存完上述檔案之後,我們需要將它新增到.csproj 檔案中:

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

現在,我們需要構建應用:

dotnet build

下一步是新增我們的 CustomerService 類到 Services 資料夾中並更新其內容,如下所示:

public class CustomerService : Customer.CustomerBase
{
    private readonly ILogger<CustomerService> _logger;
    public CustomerService(ILogger<CustomerService> logger)
    {
        _logger = logger;
    }

    public override Task<CustomerDataModel> GetCustomerInfo(CustomerFindModel request, ServerCallContext context)
    {
       CustomerDataModel result = new CustomerDataModel();

       // 這是一個用於演示的程式碼
       // 在實際的場景中,這些資訊應該從資料庫中獲取
       // 應用中的資料不應該被硬編碼
       if(request.UserId == 1) {
           result.FirstName = "Mohamad";
           result.LastName = "Lawand";
       } else if(request.UserId == 2) {
           result.FirstName = "Richard";
           result.LastName = "Feynman";
       } else if(request.UserId == 3) {
           result.FirstName = "Bruce";
           result.LastName = "Wayne";
       } else {
           result.FirstName = "James";
           result.LastName = "Bond";
       }

        return Task.FromResult(result);
    }
}

現在,我們需要更新 Startup.cs 類,以通知我們的應用程式,我們新建立的服務有了一個新的端點。為了實現這一點,在 Configure 方法(位於 app.UserEndpoints 中)裡面,我們需要新增如下的程式碼:

endpoints.MapGrpcService<CustomerService>();

MacOS 下的注意事項:

因為 MacOS 不支援 TLS 之上的 HTTP/2,所以我們需要採用如下的方案來更新 Program.cs 檔案:

webBuilder.ConfigureKestrel(options =>
{
    // 設定無需 TLS 的 HTTP/2 端點
    options.ListenLocalhost(5000, o => o.Protocols =
        HttpProtocols.Http2);
});

下一步就是建立我們的客戶端應用:

dotnet new console -o GrpcGreeterClient

現在,我們需要新增必要的套件到客戶端控制檯應用中,使其能夠識別 gRPC。這可以透過在 GrpcGreeterClient 類中實現:

dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

因為我們需要客戶端具有和伺服器端相同的契約,所以需要將前面步驟中建立的.proto 檔案新增到客戶端應用中。為了實現這一點:

  1. 首先,我們需要新增一個名為 Protos 的資料夾到客戶端專案中。

  2. 我們需要複製 gRPC greeter 服務中 Protos 資料夾裡的內容到 gRPC 客戶端專案,即

  • greet.proto

  • customers.proto

  1. 在貼上完檔案之後,我們需要更新命名空間,使其與客戶端應用相同:

option csharp_namespace = "GrpcGreeterClient"; 4. 我們需要更新 GrpcGreeterClient.csproj 檔案,以便讓它知道我們新增加的.proto 檔案:

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

這個 Protobuf 元素是程式碼自動生成特性瞭解.proto 檔案的方式。透過上面的改動,我們在這裡表明,希望客戶端使用我們新新增的.proto 檔案。

我們需要構建客戶端並確保所有內容都能構建成功:

dotnet run

現在,我們新增一些程式碼到控制檯應用中,以便於呼叫伺服器端。在 Program.cs 檔案中,我們需要做如下的改動:

// 我們建立一個通道,它代表了客戶端到伺服器的連線
// 我們在這裡新增的 URL 是由伺服器的 Kestrel 所提供的
var channel = GrcpChannel.ForAddress("<https://localhost:5001>");

// 這個強型別的客戶端是當我們新增.proto 檔案時,由程式碼生成功能所建立的
var client = new Greeter.GreeterClient(channel);

var response = await client.SayHelloAsync(new HelloRequest
{
    Name = "Mohamad"
});

Console.WriteLine("From Server: "  + response.Message);

var customerClient = new Customer.CustomerClient(channel);

var result = await customerClient.GetCustomerInfoAsync(new CustomerFindModel()
{
    UserId = 1
});

Console.WriteLine($"First Name: {result.FirstName} - Last Name: {result.LastName}");

現在,我們為應用新增流處理的功能。

我們回到 customers.proto 檔案並在 Customer 服務中新增一個流方法:

// 我們要返回一個消費者的列表
// 但是在 gRPC 中我們不能返回列表,而是需要返回一個流
rpc GetAllCustomers (AllCustomerModel) returns (stream CustomerDataModel);

正如我們所看到的,在返回中,我們新增了 stream 關鍵字,這意味著我們正在新增由「多個」回覆所組成的 stream。

同時,我們還需要新增一個空訊息

// 在 gRPC 中,我們不能定義具有空引數的方法
// 所以,我們定義一個空訊息
message AllCustomerModel {

}

要實現這個方法,我們需要到 Services 資料夾下並新增如下的程式碼到 CustomerService 類中:

public override async Task GetAllCustomers(AllCustomerModel request, IServerStreamWriter<CustomerDataModel> responseStream, ServerCallContext context)
{
    var allCustomers = new List<CustomerDataModel>();

    var c1 = new CustomerDataModel();
    c1.Name = "Mohamad Lawand";
    c1.Email = "mohamad@mail.com";
    allCustomers.Add(c1);

    var c2 = new CustomerDataModel();
    c2.Name = "Richard Feynman";
    c2.Email = "richard@physics.com";
    allCustomers.Add(c2);

    var c3 = new CustomerDataModel();
    c3.Name = "Bruce Wayne";
    c3.Email = "bruce@gotham.com";
    allCustomers.Add(c3);

    var c4 = new CustomerDataModel();
    c4.Name = "James Bond";
    c4.Email = "007@outlook.com";
    allCustomers.Add(c4);

    foreach(var item in allCustomers)
    {
        await responseStream.WriteAsync(item);
    }
}

現在,我們需要複製伺服器端 customers.proto 檔案的變化到客戶端的 customers.proto 檔案中:

service Customer {
    rpc GetCustomerInfo (CustomerFindModel) returns (CustomerDataModel);

    // 我們要返回一個消費者的列表
    // 但是在 gRPC 中我們不能返回列表,而是需要返回一個流
    rpc GetAllCustomers (AllCustomerModel) returns (stream CustomerDataModel);
}

// 在 gRPC 中,我們不能定義具有空引數的方法
// 所以,我們定義一個空訊息
message AllCustomerModel {

}

現在,我們需要再次構建應用:

dotnet build

我們下一步需要更新 GrpcClientApp 中的 Program.cs 檔案以處理新的流方法:

var customerCall = customerClient.GetAllCustomers(new AllCustomerModel());

await foreach(var customer in customerCall.ResponseStream.ReadAllAsync())
{
    Console.WriteLine($"{customer.Name} {customer.Email}");
}

現在,我們回到 GrpcGreeter 並更新 greet.proto 檔案,為其新增流方法:

rpc SayHelloStream(HelloRequest) returns (stream HelloReply);

可以看到,在返回中我們新增了關鍵字 stream,這意味著我們正在新增由「多個」回覆所組成的 stream。要實現這個方法,我們需要到 Services 資料夾下,並在 GreeterService 中新增如下的內容:

public override async Task SayHelloStream(HelloRequest request, IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
{
  for (int i = 0; i < 10; i ++)
  {
    await responseStream.WriteAsync(new HelloReply
    {
      Message = "Hello " + request.Name + " " + i
    });

    await Task.Delay(TimeSpan.FromSeconds(1));
  }
}

現在,我們需要將 greet.proto 檔案的變更從伺服器端複製到客戶端,並對其進行構建。在客戶端應用的 greet.proto 檔案中,我們新增如下這行程式碼:

rpc SayHelloStream(HelloRequest) returns (stream HelloReply);

確保在儲存.proto 檔案後,對應用進行構建。

dotnet build

現在,我們可以開啟 Program.cs 並使用新的方法:

var call = client.SayHelloStream(new HelloRequest
{
    Name = "Mohamad"
});

await foreach(var item in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine("Result " + item.Message);
}

該範例闡述了我們如何在.NET 5 中實現 gRPC 的客戶端 - 伺服器應用。

總 結

我們可以看到 gRPC 在構建應用程式中的力量,但要發揮這種力量並不容易,因為構建 gRPC 服務需要更多的搭建時間以及客戶端與伺服器之間的協調。而使用 REST 的時候,我們幾乎不需要任何搭建過程就可以直接開始消費端點。

gRPC 不一定會取代 REST,因為這兩種技術都有其特定的應用場景。請基於你的業務場景和需求,為自己的專案選擇合適的技術。

作者簡介:

Mohamad Lawand 是一位堅定的、具有前瞻性的技術架構師,擁有 13 年以上的工作經驗,工作範圍涉及從金融機構到政府實體等眾多行業。他積極主動,適應性強,擅長跨多平臺的 SaaS 和區塊鏈技術。Mohamad 還擁有一個 Youtube 頻道,他會在那裡分享自己的知識。

原文連結:

https://www.infoq.com/articles/getting-started-grpc-dotnet/

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2026/2/7

AOT使用經驗總結

從專案建立伊始,就應養成良好的習慣,即只要添加了新功能或使用了較新的語法,就及時進行 AOT 發布測試。

繼續閱讀