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/

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/2/7

aot使用經驗總結

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

继续阅读