.NET Core使用gRPC打造服務間通訊基礎設施
一、什麼是RPC
rpc(遠端過程呼叫)是一個古老而新穎的名詞,他幾乎與http協議同時或更早誕生,也是網際網路資料傳輸過程中非常重要的傳輸機制。
利用這種傳輸機制,不同程序(或服務)間像呼叫本地程序中的方法一般進行互動,而無需關心實現細節。
rpc的主要實現流程為:
1、客戶端本地方法呼叫客戶端stub(方法存根)。這個呼叫發生在客戶端本地,並把呼叫引數推送到棧中。
2、客戶端stub (方法存根)將這些引數打包,通過系統呼叫傳送到伺服器機器。打包的過程通常可以採用xml、json、二進位制編碼。打包的過程被稱為marshalling。
3、客戶端本地作業系統傳送資訊到目標伺服器(可以通過自定義tcp協議或Http協議傳輸)。
4、伺服器系統將資訊傳送到服務端stub(方法存根)
5、服務端stub (服務端方法存根) 解析資訊。解析資訊的過程可以稱為 unmarshalling。
6、伺服器stub (服務端方法存根) 呼叫程式,並通過類似的方式返回客戶端。
為了讓不同的客戶端均能訪問伺服器,許多標準化的rpc元件往往會使用介面描述語言的形式,以便方便跨平臺、跨語言的遠端過程呼叫的實現。
圖1:RPC 呼叫流程
參考維基百科:https://zh.wikipedia.org/wiki/%E9%81%A0%E7%A8%8B%E9%81%8E%E7%A8%8B%E8%AA%BF%E7%94%A8
二、什麼時候使用RPC?
HTTP和RPC是現代微服務架構中普遍採用的兩種資料傳輸方式,在某種場合幾乎都是可以完全替換的,但又具有各自不同的特點。
1、HTTP協議是一種規範、開放、通用性非常強、標準的傳輸協議,幾乎所有的語言都支援,如果要確保各類平臺都能無縫的訪問資料,可以考慮使用HTTP協議。例如目前常用的RestFul規約,定義好請求方法、資料格式並以Json的形式返回引數,能夠讓前後端之間的對接非常便捷;之前的開發者或許用wsdl、soap的形式比較多,也都是HTTP協議的一種應用。
2、RPC協議不僅僅是一種服務間傳輸的協議,也能使用於程序間的資料傳輸,它能極大的降低微服務間的通訊成本,遮蔽通訊細節,讓呼叫者能夠像呼叫本地方法一般呼叫遠端方法。 相對而言,RPC可能無法在網頁端提供支援,也並非所有的語言都實現了這種介面描述語言,讓開發過程會相對繁瑣,因此它的使用範圍相對較小。雖然gRPC目前已經提供了web版的gRPC,但由於瀏覽器的相容性等問題,也限制了他的應用。
三、什麼是gRPC
gRPC可以通俗的理解為google對於RPC的一種實現形式。
參見gRPC官網的解釋:
gRPC是可以在任何環境中執行的現代開源高效能RPC框架。它可以通過可插拔的支援來有效地連線資料中心內和跨資料中心的服務,以實現負載平衡,跟蹤,執行狀況檢查和身份驗證。它也適用於分散式計算的最後一英里,以將裝置,移動應用程式和瀏覽器連線到後端服務。
它包括四個主要特點:
- 簡單的服務定義:gRPC基於Protobuf協議構建,該協議提供了一個強大的二進位制序列化工具集和語言定義服務。
- 跨語言和平臺工作:可自動為多語言或平臺生成符合相應習慣的客戶端和服務端存根
- 快速啟動並擴充套件:只需一行程式碼即可安裝執行時環境和生成環境、並通過該框架可擴充套件到數百萬rpc請求。
- 雙向流和整合身份驗證:基於http/2的傳輸機制以及雙向流傳輸和完全整合的可插入式身份驗證機制。
gRPC目前廣泛應用於各大網際網路公司的微服務架構中,也是CNCF基金會孵化的開源基礎設施元件。其官網為https://grpc.io/;開源專案地址為https://github.com/grpc/grpc。
官網提供了詳細的文件說明,幾乎可以開箱即用,只需簡單配置就能滿足你的應用需求。在開源專案中也提供了完善的各種語言實現的sample示例程式碼,能極大的方便開發者的使用。
在gRPC中,使用的傳輸協議為HTTP/2,使用的資料傳輸的格式為Protobuf協議。
四、什麼是Protobuf
Protobuf全稱為Protocal Buffers,是一種序列化協議實現,與只類似的還有thrift。這是一種與語言中立、與實現無關、可擴充套件的序列化資料格式,不僅僅可以用於通訊協議傳輸過程,也同樣適用於資料儲存過程。它靈活高效、效能優良、更加快速和簡單。在使用Protobuf的實踐中,只需定義要處理資料的資料結構,就能利用Protobuf生成相關的程式碼。只需使用Protobuf對資料結構進行描述(IDL),即可在各種不同的語言或不同的資料流中對結構化資料進行輕鬆讀寫。
在上面的圖1 RPC呼叫流程中,使用紅色字型標註的(1)中,在客戶端套接字和服務端套接字之間進行資料交換的資料傳輸機制就可以使用Protobuf。
Protocol Buffers最早是有谷歌發明用於解決索引伺服器之間request/response協議的。通過慢慢發展發展和演進,目前已經具有了更多的特性:
- 自動生成的序列化和反序列化程式碼避免了手動解析的需要。(官方提供自動生成程式碼工具,各個語言平臺的基本都有)
- 除了用於 RPC(遠端過程呼叫)請求之外,人們開始將 protocol buffers 用作持久儲存資料的便捷自描述格式(例如,在Bigtable中)。
- 伺服器的 RPC 介面可以先宣告為協議的一部分,然後用 protocol compiler 生成基類,使用者可以使用伺服器介面的實際實現來覆蓋它們。
由於protocal buffers誕生之初主要是為了解決伺服器新舊協議之間相容性問題,所以命名為"協議緩衝區",不過目前顯然已經超出了緩衝含義的範圍。而Protobuf中的術語,則使用"message"來指代在協議傳輸過程中定義的抽象化物件,也顯然不再僅僅只是原始含義的訊息所能囊括的。
五、Proto3協議簡述
當我們使用Visual Studio 2019建立一個.NET Core下的gRPC專案時,可以看到,專案會自帶一個Protos\Greet.proto檔案,這便是gRPC使用的Protobuf的介面描述檔案,通過定義這個描述檔案,可以為生成對應的服務端、客戶端方法存根,讓方法呼叫過程更加簡單。
1、基本的資料型別對應關係
目前最新版本的Protobuf協議為proto3協議,在這個新版的協議中,提供了以下資料型別,可以方便的對應到我們日常使用的資料型別。
2、關鍵字
1)分配欄位編號
在proto協議中,每個訊息定義中的欄位都有唯一的編號,用來表示訊息二進位制格式中的欄位,且使用訊息型別後不應更改。可以使用的最小編號為1,最大編號為2^29^-1 或 536,870,911,但不包括 19000 到 19999(FieldDescriptor :: kFirstReservedNumber 到 FieldDescriptor :: kLastReservedNumber),因為它們是為 Protocol Buffers實現保留的。
2)重複欄位(repeated)
在訊息中定義重複欄位(repeated 關鍵字),允許一個message 欄位中重複數值,可以理解為陣列物件。
3)保留欄位(reserved)
Protobuf中提供了保留欄位(reserved 關鍵字),如果在老版本的proto檔案中定義了一些欄位,而在新版本的協議中移除了這些欄位,有可能出現協議檔案不匹配的問題,則可以使用reserved關鍵字。這樣當協議資料不匹配時,編譯器會提示錯誤。
圖2 使用保留欄位時,會提示錯誤
3、列舉
允許在訊息中定義列舉型別。也可以將列舉型別巢狀在message中。當使用列舉型別時,需要注意:
- 列舉為 0 的是作為零值,當不賦值的時候,就會是零值。
為了和 proto2 相容。在 proto2 中,零值必須是第一個值。
4、訊息巢狀
在proto協議中,允許巢狀組合為更加複雜的訊息。
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
5、定義服務(Services)
在proto中,如果需要對外提供介面方法,則需要使用Services。定義好services之後,protocol buffer編譯器將使用所選語言生成服務介面程式碼和客戶端與服務端方法存根。例如,
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
}
這裡就定義了一個SayHello的方法存根。該方法將返回一個名稱為 HelloReply 的訊息。
如果需要定義無引數方法,或返回值為 void 的方法,需要使用* google.protobuf.Empty** 物件,並在頭部的名稱空間中,引用預設的協議檔案 google/protobuf/empty.proto. *例如:
option csharp_namespace = "TestGRPC_Client";
import "google/protobuf/empty.proto";
package Greet;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
rpc Listen (google.protobuf.Empty) returns (google.protobuf.Empty);//啟動監聽
}
同樣,也可以使用 import 引用其他協議檔案。
參考《https://github.com/halfrost/Halfrost-Field/blob/master/contents/Protocol/Protocol-buffers-encode.md》
六、什麼是HTTP/2
gRPC 的客戶端與服務端之間的通訊機制,並沒有採用TCP造輪子,而是重用了HTTP/2的傳輸協議。HTTP2.0是超文字傳輸協議HTTP的下一代協議,也是在傳統開發者最為熟悉的HTTP/1.1 協議格式基礎上進行的升級。 與1.1相比,他提供了新的二進位制格式、多路複用機制、Header壓縮、服務端推送等特色,讓協議請求過程能夠達到更好的效能提升。限於篇幅,這裡就不再贅述了。
七、服務端開發
我們將引入一個範例,以HelloWorld作為專案名稱,在這個專案中,簡單介紹在NetCore中如何使用gRPC的過程、如何使用gRPC進行簡單身份驗證的過程。
服務端:
- 並提供一個登入 login 的方法、以及其配套的使用者請求引數、並返回對應的響應值。
一個 logout 的方法,該方法返回值為void空物件。
1、建立專案
在Visual Studio 2019中建立一個基於gRPC的空專案。這個專案命名為HelloWorld,放置在預設目錄下。如果有需要可以開啟容器支援。
2、專案的組成結構
當我們檢視這個專案時,可以看到這是一個Asp.NET Core的專案,預設的專案模板中已經集成了Asp.NET Core和gRPC.AspNetCore的元件包。
- Protos資料夾
字尾名為proto的是基於proto3的協議檔案。
- Services資料夾
專案模板建立的預設請求檔案,實現了在proto檔案中定義的SayHello方法,並以非同步的形式返回了物件HelloReply。
- 其他檔案
Dockerfile:模板自動建立的Dockerfile檔案,後期可以基於這個檔案進行docker容器的構建。
Program: 程式執行的入口。
Startup: 程式啟動項,定義AspNET Core專案啟動所需的各種配置資訊。 在UseRouting和UseEndPoints中間,加入UseAuthentication()和UseAuthorization()程式碼,以便為後期身份認證和授權。。
3、建立Proto檔案
在Protos資料夾右鍵單擊,建立一個空的記事本檔案(快捷鍵為Ctrl+Shift+A),命名為helloworld.proto。然後再裡面鍵入以下內容:
syntax = "proto3"
import "google/protobuf/empty.proto"; //需要使用空引數和空返回值時,需要使用這個預設的協議檔案
option csharp_namespace="HelloWorld";
package Account;
service Account{
rpc Login (LoginModel) returns (UserModel);
rpc Logout (google.protobuf.Empty) returns (google.protobuf.Empty);
}
message LoginModel{
string userName=1;
string userPsw=2;
}
message UserModel{
string NickName=1;
string Token=2;
Date LoginDate=3;
}
message Date{
int32 Year=1;
int32 Month=2;
int32 Day=3;
int32 Hour=4;
int32 Minute=5;
int32 Second=6;
int32 FFF=7;
}
4、建立 AccountService檔案
選擇 Services 資料夾,並建立一個檔名為 AccountService的CSharp程式碼檔案。並分別過載 Login 和 Logout 方法。
public class AccountService : account.accountBase
{
public override Task<UserModel> Login(LoginModel request, ServerCallContext context)
{
return base.Login(request, context);
}
public override Task<Empty> Logout(Empty request, ServerCallContext context)
{
return base.Logout(request, context);
}
}
然後再進行程式碼的編寫。這裡我們將登入後,返回一個假的 UserModel 資料。除此之外,我們還返回了錯誤情況下的返回模型 BadRequest 。
public class AccountService : account.accountBase
{
public override Task<StringData> Login(LoginModel request, ServerCallContext context)
{
if (request.UserName == "1234" && request.UserPsd == "1234")
{
var userModel = new UserModel
{
NickName = "測試使用者",
Token = Guid.NewGuid().ToString(),
};
return Task.FromResult(new StringData() { Data = Newtonsoft.Json.JsonConvert.SerializeObject(userModel) });
}
else
{
var BadRequest = new BadRequest { ErrorCode = 1, ErrorDescription = "使用者名稱或密碼錯誤" };
return Task.FromResult(new StringData() { Data = Newtonsoft.Json.JsonConvert.SerializeObject(BadRequest) });
}
}
public override Task<Empty> Logout(Empty request, ServerCallContext context)
{
return Task.FromResult(new Empty());
}
}
public class BadRequest
{
public int ErrorCode { get; set; }
public string ErrorDescription { get; set; }
}
八、客戶端開發
客戶端,是一個基於 .NET Core 的控制檯程式。在這個控制檯中,我們可以實現下面功能:
- 通過輸入命令 1 呼叫登入方法;
輸入命令 2 呼叫登出方法。
1、建立專案、引用依賴包
建立一個基於.NET Core的一個控制檯程式,並使用 Nuget 安裝元件包
2、建立協議檔案
將在服務端開發中建立的 Protos 資料夾拷貝到客戶端程式中。
並使用記事本對專案檔案【HelloWorld.Client.csproj】進行編輯, 將Protobuf 檔案的GrpcServices屬性設定為 “Client”。
完成這些操作,編譯完成,即可自動生成客戶端與服務端連線的客戶端方法存根。
3、編寫客戶端方法
建立一個單獨的類檔案,用來編寫客戶端呼叫方法。這個類檔名稱為 AccountClientImpl。 程式碼如下:
using Grpc.Net.Client;
using System;
using System.Collections.Generic;
using System.Text;
using static HelloWorld.Greeter;
using System.Threading.Tasks;
namespace HelloWorld.Client
{
public class AccountClientImpl
{
private readonly GrpcChannel _grpcChannel;
private readonly Account.AccountClient _accountClient;
public AccountClientImpl(GrpcChannel grpcChannel, Account.AccountClient accountClient)
{
_grpcChannel = grpcChannel;
_accountClient = accountClient;
}
public void Login()
{
var result = _accountClient.Login(new LoginModel() { UserName = "1234", UserPsd = "1234" });
Console.WriteLine(result.Data);
}
public void Logout()
{
var empty = new Google.Protobuf.WellKnownTypes.Empty();
_accountClient.Logout(empty);
}
}
}
然後再修改 Program.cs 檔案,用來呼叫上述方法。在這個方法中,如果輸入1,則執行登入方法;輸入2,則執行退出方法。
class Program
{
static void Main(string[] args)
{
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Account.AccountClient(channel);
AccountClientImpl accountClientImpl = new AccountClientImpl(channel, client);
if (Console.ReadLine() == "1")
{
accountClientImpl.Login();
}
else if (Console.ReadLine() == "2")
{
accountClientImpl.Logout();
}
Console.ReadKey();
}
}
這樣就完成了我們的程式碼編寫。
將客戶端與服務端執行起來,然後在客戶端程式碼中輸入數字 1 ;即可獲得我們想要的結果。
九、協議與專案分離
在傳統的開發過程中,由於客戶端和服務端需要維護兩套內容完全相同的 proto 協議檔案,略顯臃腫,因此我們可以通過相應的手段,將對應的檔案進行分離,便於後期的維護。
1、移動檔案
將服務端中的Protos檔案移動到上一級目錄。
2、修改專案檔案中的Proto檔案
服務端修改為:
<ItemGroup>
<Protobuf Include="..\Protos\*.proto" GrpcServices="Server" />
<Content Include="@(Protobuf)" LinkBase="" />
</ItemGroup>
客戶端修改為:
<ItemGroup>
<Protobuf Include="..\Protos\*.proto" GrpcServices="Client" />
<Content Include="@(Protobuf)" LinkBase="" />
</ItemGroup>
如果覺得這樣的展示效果不太美觀,也可以將proto檔案移動到Protos目錄下。
3、重新編譯
完成協議檔案分離,即可對專案進行編譯。
總結
在這個教程中,我們從PRC開始講起,簡單介紹了與gRPC相關的技術棧,練習了使用 gRPC 進行服務端和客戶端程式開發的全過程,希望大家能獲得收穫。
第一次嘗試編寫入門級教程,如有不足之處還請匹配指正