前言
系統分布式已經成為程序員的家常,將大型單體划分為相對簡單的小模塊,分散系統能力,提升系統擴展性、功能模塊復用性等;各功能模塊之間肯定會有很多數據共享和交互的應用場景,那就避免不了各模塊之間的通信;目前用的比較多的方式是HTTP(Restful API)接口、消息隊列等,而HTTP(Restful API)接口應該是目前應用比較廣泛的,相對之前的webservice和WCF都顯得比較輕量級,而且實用;
隨着微服務的盛行,對服務間的通信要求也越來越高,比如傳輸方式、傳輸速率、傳輸內容大小等,而HTTP(Restful API)方式有較重的頭信息、無狀態、不能復用連接等缺點,所以優化是早晚的事;那么gRPC的出現就顯得情有可原啦(gRPC不局限於服務間通信,只要符合Server/Client場景就可以),所以接下來咱們一起來探究學習一下。
正文
1. 認識一下gRPC
gRPC 由 google 開發,最前面的g就代表google。進入gRPC官網,一句話描述了gRPC的主要特色,先上張圖:

A high performance, open source universal RPC framework
翻譯:一個高性能、開源的通用RPC框架
- 高性能:gRPC遵循HTTP/2協議,解決並優化了HTTP1.1的一些缺陷;默認使用谷歌開源的 protocol buffers(類似於XML、JSON的數據序列化結構協議),傳輸速率、解析速度都很快、壓縮率高,性能整體都比XML和JSON好(后續專門寫個程序來比較比較);
- 開源:源碼地址請進傳送門;
- 通用:各種流行語言(C++、C#、Java、Go、Python等)都能用,輕松實現跨語言通信;本身不限於任何平台。
- RPC:遠程過程調用(Remote Procedure Call),通俗一點理解,就是分布式中各服務之間調用的一種技術。
總而言之,gRPC是一個現代的開源高性能遠程過程調用(RPC)框架,可以在任何環境中運行。它可以有效地連接數據中心內和跨數據中心的服務,並支持可插拔的負載均衡、跟蹤、健康檢查和身份認證(后續會一一舉例演示)。
算啦,估計到這有些小伙伴還是有點懵圈(語言組織能力還有待提高),那就先將其理解為一個類似於WebAPI的調用框架,只是性能更高,使用更簡單,就像調用本地方法一樣;它使客戶端和服務器應用程序能夠透明地通信,隱藏了遠程調用的細節,大概過程如下:

上圖簡析:各語言之間可以互相調用,只要客戶端按照約定(Protocol Buffer)傳遞對應的請求參數,調用服務端對應的方法,最后就會返回約定好(Protocol Buffer)的響應數據。
不說那么多啦,直接開干吧,一邊擼碼一邊說理論。
2. 初體驗 gRPC
2.1 從0開始寫服務端
-
創建一個空的Web項目(基於.NetCore3.1),引入包Grpc.AspNetCore

-
編寫proto文件(重點),因為gRPC是使用protocol Buffer作為接口定義語言,內容包含以下兩部分:
傳遞的消息:請求和響應時的數據信息,類似於現在用的DTO類。
gRPC服務的定義:定義gRPC服務方法,可以理解為現在寫的Restful 接口。
這里模擬用戶維護的場景,包含增、刪、改、查方法,這里新建protos目錄專門用來存放proto文件,user.proto內容如下:
// 使用的是proto3版本 syntax = "proto3"; // 定義命名空間,后續生成代碼時就會生成對應的命名空間 option csharp_namespace = "gRPC.Demo.Server.protos"; /* 每一句需要用分號結尾 message 用來定義請求和返回數據格式 tag message后面的值數字代表是字段的標識(tag),不是賦值, */ // 新增用戶時需要傳遞數據消息, 可理解為一個類 message AddUserReuqest{ string name=1; int32 age=2; bool isBoy=3; } // 新增時返回的消息格式 message ResultResponse { int32 code=1; string msg =2; } //傳遞的查詢條件信息格式,可理解為平時傳入的查詢條件對象 message QueryUserReuqest{ string name=1; } //查詢返回的用戶信息格式,可理解為返回的類 message UserInfoResponse { string name=1; int32 age=2; string gender=3; } // service 用標識定義服務的,里面寫對應的方法 service UserService{ // 新增用戶 rpc AddUser(AddUserReuqest) returns (ResultResponse); // 查詢用戶 rpc GetAllUser(QueryUserReuqest) returns (UserInfoResponse); }proto文件,各種編程語言是通用的,都可以根據定義好的proto文件生成對應編程語言的代碼,重寫業務即可。對於自動生成代碼,其他編程語言需要借助protoc 工具(點這里去下載),而.Net微軟已經封裝好了,只要引入了Grpc相關的包,直接編譯即可。先簡單設置一下proto文件屬性,大概步驟如下:

注:如果不想按照上面步驟設置文件屬性,也可以直接編輯項目文件也行,如下增加紅框部分:

最后編譯程序就可以自動生成對應的服務端代碼了,如下路徑:

-
重寫生成的方法,增加業務邏輯
proto文件生成的代碼只是一個約定,不包含業務邏輯,關於具體的業務需要自己處理,如下:

-
在Startup.cs文件中開啟gRPC的功能,如下:

到這服務端就完成了,現在就去寫一個客戶端連接一下;
2.2 搞一個客戶端訪問服務
-
創建一個控制台程序
引入Google.Protobuf、Grpc.Net.Client、Grpc.Tools三個包,然后將服務端寫好的proto文件拷貝到項目中,按照上面說的步驟設置一下proto文件屬性為Client only,編譯即可,項目結構如下:

如果不想通過設置屬性的方式,也可以直接修改項目文件,然后編譯就可自動生成代碼:

-
現在可以寫業務啦
新增用戶調用遠程服務邏輯:

查詢用戶調用遠程服務邏輯:

從上面代碼來看,使用是不是比較簡單,只需要知道遠程地址即可,遠程調用就像調用本地方法一樣,也不用判斷狀態碼,不用自己去解析內容,而是按照約定直接返回結果。
-
先運行服務端,再運行客戶端,效果如下:

調試時注意:
由於gRPC使用SSL/TLS保護服務,在調試的時候盡管指定為https連接也會報錯,這是因為系統沒有信任微軟開發證書,執行以下命令即可:
dotnet dev-certs https --trust然后就可以正常調試了;當然在生產環境中,還是需要配置真實證書的,如果不想用證書也是可以滴,這個后續單獨會說到。
2.3 使用.NetCore模板快速生成服務端
.NetCore已經將gRPC服務端封裝成一個模板了,可以通過模板快速創建一個gRPC項目,如下:

創建出來的服務端項目基本和剛剛創建的一樣,編譯運行就能啟動;小伙伴后續可以直接用模板創建,在里面添加對應的業務代碼即可。
本來打算把gRPC的四種模式接着演示的,但考慮到proto文件的編寫是一個重點,所以總結了一些常用的寫法,先熟悉熟悉proto文件的編寫,后續再通過案例演示加深印象就更好啦。
3. Protocol Buffer 是重點
通過上面示例演示,proto文件用於約定服務接口,各編程語言用其可以生成對應的代碼,然后進行業務邏輯的編寫,可見proto文件扮演了很重要的角色。在性能方便是以二進制格式進行解析,內容小,傳輸效率高,適合傳遞大量數據的場景。
proto文件中每一個message代表了一類結構化的數據,message 里面定義了每一個屬性的類型和名字,並指定一個Tag(每個屬性后面的數字)。在gRPC傳輸過程中是通過Tag這個數字進行標識屬性的,不是用屬性名。
3.1 數據類型
由於protocol Buffer 不限於編程語言,所以在編寫proto文件時,指定的類型和不同編程語言的類型是一一對應的,這樣在根據proto文件生成對應代碼時,約束力就比較強,大概的類型對照表如下(只整理了比較常用的語言,詳細請進官網):

每一種類型在沒有指定值時都對應有默認值:
- string: 空字符串
- bytes:空byte數組
- bool:false
- 數值型:0
- 枚舉enum: 默認第一個枚舉值,第一個值必須是0
默認值其實基本上和對應的編程語言默認值差不多一樣。
3.2 常用編寫方式
-
一般形式
通過message定義一類結構化數據,如下:

使用message定義一類數據時,其實就可以理解為在編寫一個類,里面的字段就是類里面的屬性。需要注意的是message中指定字段的后面數值不是字段值,而是字段名稱對應的一個標識(Tag)。
-
嵌套自定義類型
經常會遇到這種嵌套自定義類型的場景,比如一個班級有多個學生,班級類型就需要嵌套學生,如下:

這里用到repeated來表示一個班級有多個學生,可以理解為List.
-
使用枚舉
枚舉在編程過程中肯定少不了,這里把上面的性別改為枚舉,如下:

枚舉類型需要注意的是第一項映射的常量值必須為0,方便設置其作為默認項,同時也是為了兼容proto2。
如果想要一個數字對應多個枚舉項,可以開啟允許別名的方式進行設置,如下:

-
文件引入
在實際開發場景中,定義的數據類型很多,放在一個文件肯定不太合適,可讀性差,所以都會將其放在多個proto文件,然后通過引用多個文件即可;接下來把學生單獨出來作為一個proto文件,如下:

注:在創建proto文件時,按照2.1那個步驟設置文件屬性,否則不會自動生成類,如果有文件相互引用,還會編譯報錯。
-
引用第三方proto
在平時開發的時候,有些機構、社區或是大佬將常用的類進行封裝,然后打包好, 我們只需要下載引用就可以愉快的使用啦;同樣,proto也可以這樣,如下在Student中加一個入學時間,如下:

其他類型進傳送門,需要哪個,就照着上面方式使用即可。
3.3 參數字段變更注意
需求變動對於開發來說是家常便飯,針對接口增加字段或修改字段都是常有的事,如果打算在原有方法修改字段,那就需要注意啦; 在通過protocol Buffer進行序列化時,是通過類似於字典的形式約定好的,其中字段后面的數字Tag至關重要,用其進行屬性字段標識,學生舉例如下:
// 在解析時,都是用后面的數字tag進行約定解析的
message Student{
string name=1; // 1 就代表 姓名
int32 age=2; // 2 就代表 年齡
Gender gender=3; // 3 就代表 性別
string addr=4; // 4 就代表 地址
google.protobuf.Timestamp enrollmentDate=5; //5 就代表 入學時間
}
只要已經上線互相調用,如果有參數字段變動,最好是通過新增的方式,因為這樣會避免雙方調用因為沒有同時更改參數字段造成對業務的影響。比如上面學生字段要變動,需求是:新增一個分數字段,刪除年齡字段,通常會有以下方式:
-
方式1:將年齡字段直接改成分數字段即可

如果服務端將proto文件改成這樣,客戶端還沒來的及改,就會造成如下情況:
2這個tag標識在服務端代表分數,在傳遞數據的時候就將分數賦值給該字段;但對於客戶端來說,還是認為2這個tag標識為年齡,接收的時候還是以年齡字段進行處理,最終就會影響原有解析邏輯,並影響到業務。
-
方式2:刪除年齡字段並將其對應tag標識設置為保留字段,然后新增一個分數字段

通過reserved將原有參數字段和對應的tag標識設為保留值,這樣不允許用作其他業務,就會避免業務邏輯處理異常的情況,最多是就接受方沒有收到值,按默認值處理即可。
關於字段的更新注意先暫時說這么多,類型之間互相兼容的細節小伙伴去官網好好看看。 這里記住一個重點:tag 標識不能隨便進行重用,不然proto文件沒有及時同步就有可能導致業務邏輯處理有問題。
總結
.NetCore對於gRPC已經是封裝得比較方便啦,引入對應的包,編譯自動根據.proto文件生成對應代碼,然后直接寫業務即可;下一篇說說關於gRPC四種模式及gRPC服務的認證、授權。
一個被程序搞丑的帥小伙,關注"Code綜藝圈",和我一起學~~~
