gRPC入門與實操(.NET篇)

来源:https://www.cnblogs.com/newton/archive/2023/01/10/17033789.html
-Advertisement-
Play Games

為什麼選擇 gRPC 歷史 長久以來,我們在前後端交互時使用WebApi + JSON方式,後端服務之間調用同樣如此(或者更久遠之前的WCF + XML方式)。WebApi + JSON 是優選的,很重要的一點是它們兩者都是平臺無關的三方標準,且足夠語義化,便於程式員使用,在異構(前後端、多語言後端 ...


為什麼選擇 gRPC

歷史

長久以來,我們在前後端交互時使用WebApi + JSON方式,後端服務之間調用同樣如此(或者更久遠之前的WCF + XML方式)。WebApi + JSON 是優選的,很重要的一點是它們兩者都是平臺無關的三方標準,且足夠語義化,便於程式員使用,在異構(前後端、多語言後端)交互場景下是不二選擇。然而,在後端服務體系改進特別是後來微服務興起後,我們發現,前後端交互理所當然認可的 WebApi + JSON 在後端體系內顯得有點不太合適:

  1. JSON 字元編碼方式使得傳輸數據量較大,而後端一般並不需要直接操作 JSON,都會將 JSON 轉為平臺專有類型後再處理;既然需要轉換,為什麼不選擇一個數據量更小,轉換更方便的格式呢?
  2. 調用雙方要事先約定數據結構和調用介面,稍有變動就要手動更新相關代碼(Model 類和方法簽名);是否可以將約定固化為文檔,服務提供者維護該文檔,調用方根據該文檔可以方便地生成自己需要的代碼,在文檔變化時代碼也可以自動更新?
  3. [之前] WebApi 基於的 Http[1.1] 協議已經誕生 20 多年,其定義的交互模式在今日已經捉襟見肘;業界需要一個更有效率的協議。

高效傳輸-Http2.0

我們先來說第 3 個問題,其實很多大廠內部早已開始著手處理,並誕生了一些應用廣泛的框架,如阿裡開源的Dubbo,直接拋棄了 Http 改為基於 Tcp 實現,效率得到明顯提升,不過 Dubbo 依賴 Java 環境,無法跨平臺使用,不在我們考慮範圍。

另一個大廠 Google,內部也在長期使用自研的Stubby框架,與 Dubbo 不同的是,Studdy 是跨平臺的,但是 Google 認為 Studdy 不基於任何標準,而且與其內部基礎設施緊密耦合,並不適合公開發佈。

同時 Google 也在對 Http1.1 協議進行增強,該項目是 2012 年提出的 SPDY 方案,其優化了 Http 協議層,新增的功能包括數據流的多路復用、請求優先順序以及HTTP報頭壓縮。Google 表示,引入 SPDY 協議後,在實驗室測試中頁面載入速度比原先快 64%。巨大的提升讓大家開始從正面看待和解決老版本 Http 協議的問題,這也直接加速了 Http2.0 的誕生。實際上,Http2.0 是以 SPDY 為原型進行討論和標準化的,當然也做了更多的改進和調整。

隨著 Http2.0 的出現和普及,許多與 Stubby 相同的功能已經出現在公共標準中,包括 Stubby 未提供的其他功能。很明顯,是時候重做 Stubby 以利用這種標準化,並將其適用範圍擴展到分散式計算的最後一英里,支持移動設備(如安卓)、物聯網(IOT)、和瀏覽器連接到後端服務。

2015 年 3 月,Google決定在公開場合構建下一版 Stubby,以便與業界分享經驗,併進行相關合作,也就是本文的主角gRPC

高效編碼-protobuf

回頭來看第 1 個問題,解決起來相對比較簡單,無非是將傻瓜式字元編碼轉為更有效的二進位編碼(比如數字 10000 JSON 編碼後是 5 個位元組,按整型編碼就是 4 個位元組),同時加上些事先約定的編碼演算法使得最終結果更緊湊。常見的平臺無關的編碼格式有MessagePackprotobuf等,我們以 protobuf 為例。

protobuf 採用 varint 和 處理負數的 ZigZag 兩種編碼方式使得數值欄位占用空間大大減少;同時它約定了欄位類型和標識,採用 TLV 方式,將欄位名映射為小範圍結果集中的一項(比如對於不超過 256 個欄位的數據體來說,不管欄位名本身的長度多少,每個欄位名都只要 1 個位元組就能標識),同時移除了分隔符,並且可以過濾空欄位(若欄位沒有被賦值,那麼該欄位不會出現在序列化結果中)。

高效編程-代碼生成工具

第 2 個問題呢,其實需要的就是[每個平臺]一套代碼生成工具。生成的代碼需要覆蓋類的定義、對象的序列化/反序列化、服務介面的暴露和遠程調用等等必要的模板代碼,如此,開發人員只需要負責介面文檔的維護和業務代碼的實現(很自然的面向介面編程:))。此時,採用 protobuf 的gRPC自然而然的映入眼帘,因為對於目前所有主要的編程語言和平臺,都有 gRPC 工具和庫,包括 .NET、Java、Python、Go、C++、Node.js、Swift、Dart、Ruby 以及 PHP。可以說,這些工具和庫的提供,使得 gRPC 可以跨多種語言和平臺一致地工作,成為一個全面的 RPC 解決方案。

gRPC 在 .NET 中的使用

ASP.NET Core 3.0 開始,支持gRPC作為 .NET 平臺中的“一等公民”。

服務端

在 VS 中新建ASP.NET Core gRPC 服務,會發現在項目文件中自動引入了Microsoft.NET.Sdk.Web類庫,很明顯,gRPC 服務仍然是 Web 服務,畢竟它走的是 Http 協議。同時還引入了Grpc.AspNetCore類庫,該類庫引用了幾個子類庫需要瞭解下:

  • Google.Protobuf:包含 protobuf 預定義 message 類型在 C# 中的實現;
  • Grpc.Tools:上面講到的代碼生成工具,編譯時使用,運行時不需要,因此依賴項標記為PrivateAssets="All"
  • Grpc.AspNetCore.Server:服務端專用;
  • Grpc.Net.ClientFactory:客戶端專用,如果只是提供服務的話,那麼該類庫可以移除。

定義介面文件:

syntax = "proto3";

// 指定自動生成的類所在的命名空間,如果不指定則以下麵的 package 為命名空間,這主要便於本項目內部的模塊劃分
option csharp_namespace = "Demo.Grpc";

// 對外提供服務的命名空間
package TestDemo;

// 服務
service Greeter {
  // 介面
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// 不太好的一點是就算只有一個基礎類型欄位,也要新建一個 message 進行包裝
message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

然後把它包含到項目文件中:

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

編譯一下,Grpc.Tools 將幫我們生成 GreeterBase 類及兩個模型類:

public abstract partial class GreeterBase
{
    public virtual Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        throw new RpcException(new Status(StatusCode.Unimplemented, ""));
    }
}

public class HelloRequest
{
    public string Name { get; set; }
}

public class HelloReply
{
    public string Message { get; set; }
}

這裡的 SayHello 是個空實現,我們新建一個實現類並填充業務邏輯,比如:

public class GreeterService : GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" });
    }
}

最後將服務添加到路由管道,對外暴露:

using Demo.Grpc.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddGrpc();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();

app.Run();

protobuf-net.Grpc

如果覺得寫 .proto 文件太彆扭,希望可以按傳統方式寫介面,那麼社區項目protobuf-net.Grpc值得嘗試,使用它可以它通過特性批註的 .NET 類型來定義應用的 gRPC 服務和消息。

首先我們不需要再引用 Grpc.AspNetCore,而是改為引用 protobuf-net.Grpc 庫。同樣也不需要寫 .proto 文件,而是直接寫介面類:

using ProtoBuf.Grpc;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Threading.Tasks;

namespace Demo.Grpc;

[DataContract]
public class HelloReply
{
    [DataMember(Order = 1)]
    public string Message { get; set; }
}

[DataContract]
public class HelloRequest
{
    [DataMember(Order = 1)]
    public string Name { get; set; }
}

[ServiceContract(Name = "TestDemo.GreeterService")]
public interface IGreeterService
{
    [OperationContract]
    Task<HelloReply> SayHelloAsync(HelloRequest request, CallContext context = default);
}

註意其中特性的修飾。

寫完實現類後,在 Program.cs 中註冊即可,此處不再贅述。

使用 protobuf-net.Grpc,我們不需要寫 .proto 文件,但是調用方特別是其它平臺的調用方,需要 .proto 文件來生成相應的客戶端,難道我們還要另外再寫一份嗎?別急,我們可以引入protobuf-net.Grpc.AspNetCore.Reflection,它引用的protobuf-net.Grpc.Reflection提供了根據 C# 介面生成 .proto 文件的方法;同時使用它還便於客戶端測試,同Grpc.AspNetCore.Server.Reflection的作用一樣,下文會講到。

異常處理

.Net 為 gRPC 提供了攔截器機制,可新建一個攔截器統一處理業務異常,比如:

public class GrpcGlobalExceptionInterceptor : Interceptor
{
    private readonly ILogger<GrpcGlobalExceptionInterceptor> _logger;

    public GrpcGlobalExceptionInterceptor(ILogger<GrpcGlobalExceptionInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        try
        {
            return await continuation(request, context);
        }
        catch (Exception ex)
        {
            _logger.LogError(new EventId(ex.HResult), ex, ex.Message);
            
            // do something

            // then you can choose throw the exception again
            throw ex;
        }
    }
}

上述代碼在處理完異常後重新拋出,旨在讓客戶端接收處理該異常,然而,實際上客戶端是無法接收到該異常信息的,除非服務端拋出的是RpcException;同時,為使客戶端得到正確的 HttpStatusCode(預設是 200,即使客戶端得到是 RpcException),需要顯式給HttpContext.Response.StatusCode賦值,如下:

// ...

catch(Exception ex)
{
    var httpContext = context.GetHttpContext();
    httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
             
    // 註意,RpcException 的 StatusCode 和 Http 的 StatusCode 不是一一對應的
    throw new RpcException(new Status(StatusCode.XXX, "some messages"));
}

// ...

我們可以在構造 RpcException 對象時傳遞Metadata,用於攜帶額外的數據到客戶端,如果需要傳遞複雜對象,那麼要先按約定序列化成位元組數組。

攔截器邏輯完成後,需要在服務註入時設置如下:

builder.Services.AddGrpc(options =>
{
    options.Interceptors.Add<GrpcGlobalExceptionInterceptor>();
});

測試

服務端完成後,如果要藉助Postman或者gRPCurl測試,那麼它們其實就是調用服務的客戶端,要讓它們事先知道服務約定信息,有兩種方法:

  1. 給它們提供 .proto 文件,這個很好理解,關於服務的所有信息就定義在 .proto 文件中;
  2. 服務端暴露一個可以獲取服務信息的介面。

如果要用方法 2,那麼要先引入Grpc.AspNetCore.Server.Reflection類庫,然後在 Program.cs 中註冊介面:

// ...
builder.Services.AddGrpcReflection();

var app = builder.Build();

// ...

IWebHostEnvironment env = app.Environment;

if (env.IsDevelopment())
{
    app.MapGrpcReflectionService();
}

客戶端

客戶端不需要 Grpc.AspNetCore.Server,所以我們直接引用 Google.Protobuf、Grpc.Tools、Grpc.Net.ClientFactory。

將服務端提供的 .proto 文件添加到項目中,併在項目文件中包含:

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

註意,如果只需要服務端提供的部分介面,那麼 .proto 文件中只保留必要的介面即可,真正做到按需索取:)。

我們還可以更改 .proto 文件中 message 的欄位名(只要不改動欄位類型和順序),不會影響服務的調用。這也直接反映了 protobuf 不是按欄位名而是事先定義的欄位標識編碼的。

由此,假如我們有多個 .proto 文件,使用到了相同結構的 message,無所謂欄位名是否相同,我們都可以將這些 message 抽離為單獨的一個 .proto 文件,然後其它的 .proto 文件使用import "Protos/xxx.proto";引入它。

編譯一下,然後在 Program.cs 中註冊服務客戶端:

// .proto 文件中的 package
using TestDemo;

// 這裡註入的服務是 Transient 模式
builder.Services.AddGrpcClient<Greeter.GreeterClient>(o =>
{
    o.Address = new Uri("https://localhost:5001");
});

如此,其它地方就可以愉快地使用客戶端調用遠程服務了。

同服務端一樣,我們可以給客戶端配置統一的攔截器。如果服務端返回上文提到的 RpcException,客戶端得到後是直接拋出的(就像是本地異常),我們可以新建一個專門的異常攔截器處理 RpcException 異常。

builder.Services
    .AddGrpcClient<Greeter.GreeterClient>(o =>
    {
        o.Address = new Uri("https://localhost:5001");
    })
    .AddInterceptor<ExceptionInterceptor>();  // 預設創建一次,併在 GreeterClient 實例之間共用
    //.AddInterceptor<ExceptionInterceptor>(InterceptorScope.Client); // 每個 GreeterClient 實例擁有自己的攔截器

具體的異常處理邏輯就不舉例了。提一下,通過 RpcException.Trailers 可以獲取異常的 metadata 數據。

另外,對於異常處理來說,如果項目是普通的 ASP.NET Core Web 服務,那麼使用原先的 ActionFilterAttributeIExceptionFilter等攔截器也是一樣的,因為既然運行時出現了異常,這兩者肯定也能捕獲到。

進階知識

本文未涉及的 .NET-gRPC 的進階知識諸如單元測試服務調用中止負載均衡健康監控等,以後有機會再與大家分享。其實這方面微軟官方文檔已經講解得相當全面了,但也難以覆蓋在實操過程中遇到的所有問題,所以有此文以饗讀者,還望不吝指教。

參考資料

HTTP 2.0的前世今生
.NET性能優化-是時候換個序列化協議了
MessagePack簡析
Varint編碼
Protobuf 標量數據類型


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 今天給大家介紹的是Python爬蟲批量下載相親網站圖片數據,在這裡給需要的小伙伴們代碼,並且給出一點小心得。 首先是爬取之前應該儘可能偽裝成瀏覽器而不被識別出來是爬蟲,基本的是加請求頭,但是這樣的純文本數據爬取的人會很多,所以我們需要考慮更換代理IP和隨機更換請求頭的方式來對相親網站圖片數據進 ...
  • 一、MybatisPlusMax簡介 MybatisPlusMax是MybatisPlus的增強包,秉承只拓展不修改的理念,對MybatisPlus做增強。 正如MybatisPlus是對MyBatis的增強,MybatisPlusMax是對MybatisPlus的增強,拓展理念一脈相承。 Myba ...
  • 相較於最初的 Bourne shell,現代 bash 版本的最大改進之一體現在算術方面。早期的 shell 版本沒有內建的算術功能,哪怕是給變數加1,也得調用單獨的程式來完成。 1、算術方法一: $(( )) 只要都是整數運算,就可以在 $(( )) 的算術表達式內使用所有的標準運算符。還有一個額 ...
  • 大家好,我是三友~~ 這篇文章我準備來聊一聊RocketMQ消息的一生。 不知你是否跟我一樣,在使用RocketMQ的時候也有很多的疑惑: 消息是如何發送的,隊列是如何選擇的?消息是如何存儲的,是如何保證讀寫的高性能?RocketMQ是如何實現消息的快速查找的?RocketMQ是如何實現高可用的?消 ...
  • 原文:JavaFx 頁面和控制項設置快捷鍵 - Stars-One的雜貨小窩 之前說過一篇window系統全局快捷鍵的設置,本期主要是講解JavaFx應用程式的快捷鍵設置,還是有所區別的 這裡主要是Tornadofx為例進行講解,以Kotlin語言為例,由於比較簡單,就不貼截圖了,下麵例子都是自己測試 ...
  • 2023-01-10 一、Mybatis中獲取主鍵自增數據 要獲取自增數據時,需要在映射文件中的“<insert>”中添加兩個屬性,例如獲取自增的id ①EmployeeMapper.xml中的<mapper>標簽內部 <insert id="insertEmployee" useGenerated ...
  • 在高併發下,Java程式的GC問題屬於很典型的一類問題,帶來的影響往往會被進一步放大。不管是「GC頻率過快」還是「GC耗時太長」,由於GC期間都存在Stop The World問題,因此很容易導致服務超時,引發性能問題。 ...
  • 作者:明明如月學長 鏈接:https://juejin.cn/post/7117046503616544804 一、背景 在日常開發中,通常為了方便調試、方便查問題,會列印很多 INFO 級別的日誌。 隨著訪問量越來越大,一不小心,某個日誌文件一天的 size 就大於了某個閾值(如 5G),於是,收 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...