RPC 英文簡全稱 Remote Procedure Call(遠程過程調用)一種實現進程間通訊的協議,主要功能目標是讓構建分佈式應用時更容易,在提供強大的遠程調用能力時不損失本地調用的語義簡潔性。RPC 框架需提供一種透明調用機制讓使用者沒必要顯式的區分本地調用和遠程調用,可讓使用者能夠像調用本地服務同樣調用遠程服務。html
一個基礎的 RPC 框架 一般包含兩部分,一、傳輸協議 二、序列化協議,成熟的 RPC 的庫會封裝如「服務發現」,"負載均衡",「熔斷降級」一類面向服務的高級特性。git
經常使用的傳輸協議包含:HTTP/HTTP二、TCP/UDP。github
經常使用的序列化協議:基於文本編碼的xml、json,基於二進制的protobuf、hessian等。objective-c
HTTP協議支持鏈接池複用也能夠用 protobuf 二進制協議對數據進行序列化,可是HTTP協議傭有較多冗餘的請求、響應頭部報文。而自定義 TCP 協議則能夠有效控制報文大小提升通訊效率。json
因此大多數 RPC 框架會選擇選自定義 TCP 協議 + 基於文本的序列化協議來作爲進程通訊的通道。後端
gRPC 就是 google 版 RPC 的簡稱, 其傳輸協議使用 HTTP2, 序列化協議使用 protobuf。除此以外 gRPC 還包含成熟的 IDL 方案,接口及數據結構可編寫 IDL 文件(.proto)再經過工具生成不一樣的平臺的代碼供調用。緩存
gRPC 將 HTTP2 作爲通訊息協議,HTTP2相對HTTP1x有如下優化:安全
HTTP1.x以換行符做爲純文本的分隔符。HTTP/2將全部傳輸的信息分割爲更小的消息和幀,並對它們採用二進制格式的編碼。幀是HTTP2通訊的最小單位,每一個幀包含幀首部,首部會標識當前幀所局的消息。消息由一個或多個幀組成,例如請求消息或響應消息。bash
在HTTP1.x中,併發的請求會同時使用多個TCP鏈接而且會有數量限制。HTTP2 利用二進制分幀,同一個域名下的請求能夠在單個鏈接上完成,數據以消息的形式發送而消息又能夠由多個幀組成,多個幀之間能夠亂序發送,接收方根據幀首部標識從新組裝成消息。服務器
在 HTTP/2 中,每一個請求均可以帶一個31bit的優先值,0表示最高優先級, 數值越大優先級越低。有了這個優先值,客戶端和服務器就能夠在處理不一樣的流時採起不一樣的策略,以最優的方式發送流、消息和幀。
相對於 HTTP1.x 的單請求單次響應不一樣,HTTP/2 能夠實現單次請求屢次響應的服務端連續發送消息。
在鏈接存續期間,客戶端和服務端會各自維護一份頭部表,對於相同的頭部數據頭部信息再也不發送,頭部的鍵值對要麼被更新,要麼被追加。
HTTP2 解決了 HTTP1.x 存在的問題,在效率上接近 TCP 但又比 TCP 自定義協議更方便。TCP 自定義協議還須要本身解決併發鏈接數的控制、斷連和重連機制、網絡閃斷、宕機保護、消息緩存和重發機制等等問題。
Protocol Buffers(簡稱Protobuf) ,是Google出品的序列化協議,開發語言無關,和平臺無關,具備良好的可擴展性。Protobuf和全部的序列化框架同樣,均可以用於數據存儲、通信協議。Protobuf的序列化的結果體積要比XML、JSON小不少,XML和JSON的描述信息太多了,致使消息要大;此外Protobuf還使用了Varint 編碼,減小數據對空間的佔用。Protobuf序列化和反序列化速度比XML、JSON快不少,是直接把二進制流作位運算轉換爲完整對象,而XML和JSON還須要構建成 XML 或者 JSON 對象結構再作字段匹配。
gRPC 使用 HTTP2 傳輸協議傳輸 protobuf 序列化的二進制數據,所以有極高的效率、極低的資源佔用率。
.proto 文件用於描述服務及接口名稱以及請求/響應所用的數據結構。它充當了不一樣語言平臺、服務之間的「說明文檔」,經過特定的編繹工具 proto 文件可被翻譯爲 Go、Java、C++、Python、Objective-C等語言的接口實現代碼。 proto 語法很是簡單,先看一眼完整的 proto 文件:
// 指定 Protocol Buffer 版本
syntax = "proto3";
// 用於指定 Objective-C 類前綴
option objc_class_prefix = "RTG";
// 命名空間
package routeguide;
// 服務名稱
service RouteGuide {
// 接口名稱,接口上方的註釋編譯後會保留成 API 註釋
rpc GetFeature(Point) returns (Feature) {}
rpc ListFeatures(Rectangle) returns (stream Feature) {}
rpc RecordRoute(stream Point) returns (RouteSummary) {}
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
// 數據結構描述
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message Rectangle {
Point lo = 1;
Point hi = 2;
}
message Feature {
string name = 1;
Point location = 2;
}
message RouteNote {
Point location = 1;
string message = 2;
}
message RouteSummary {
int32 point_count = 1;
int32 feature_count = 2;
int32 distance = 3;
int32 elapsed_time = 4;
}
複製代碼
定義一個服務的基本語法以 service 爲關鍵詞,後面跟着服務名稱。一個服務在編繹後會成爲一個單獨的對象,服務內的接口會被編繹爲對象內的方法(Objective-C)。
service RouteGuide {
...
}
複製代碼
定義好服務後就能夠在服務內添加接口了,接口以 rpc 關鍵詞作爲開頭來修飾,同時包含三個要素,接口名、請求數據結構、響應數據結構
// GetFeature是接口名,Point、Feature 是相應請求、響應數據結構
rpc GetFeature(Point) returns (Feature) {}
複製代碼
接口的請求和響應均可以定義爲連續消息流,也就是說客戶端發送一個請求數據而服務端能夠返回多個響應數據。一樣的,客戶端也能夠連續發送多個請求數據,服務端接收完全部請求數據後返回一個響應數據。只須要在對應的數據結構前添加 stream 關鍵詞便可標識當前請求或者響應是不是連續的。
// 連續響應
rpc ListFeatures(Rectangle) returns (stream Feature) {}
// 連續請求
rpc RecordRoute(stream Point) returns (RouteSummary) {}
複製代碼
除了上面的請求或者響應爲之一能夠被定義爲連續消息流外,請求響應也能夠同時爲連續消息流,只須要同時在請求響應數據結構前添加 stream 標識便可。連續的請求和連續響應是相互獨立的,客戶端和服務端均可以以任意組合去處理接收到的消息。例如:服務端能夠接收完全部客戶端的消息後再返回數據給客戶端,或者服務端能夠接收到一條消息當即返回一條消息,或者其它任何的組合。而且兩端收到的消息順序是和發送的順序一致的。
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
複製代碼
數據結構名稱前要以 message 關鍵字修飾,須要在字段前指定每一個字段的類型。
每一個字段都有一個編號,此編號一般是從1開始的自增數,原則上自增沒有上限,可是單個數據結構字段編號最好不要超過16個,超過16個字段在進行二進制編碼時會佔用額外的空間。
Message 會編繹成與服務接口獨立的文件,接口服務依賴 Message 編繹後的源代碼。
message Point {
int32 latitude = 1; // 1 是字段編號
int32 longitude = 2; // 同上
}
複製代碼
message 內的字段同 json 轉換爲 model 相似支持嵌套。例如一個 Rectangle 須要用兩個座標表示,而以前定義過 Point 那麼在定義 Rectangle 內的字段時能夠把 Point 作爲座標字段的類型。
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message Rectangle {
Point lo = 1;
Point hi = 2;
}
複製代碼
在編譯爲Objective-C接口庫時能夠指定一個類前綴,這個前綴將做用於全部服務、數據結構類名。
option objc_class_prefix = "RTG";
複製代碼
生成 Objective-C 客戶端代碼能夠本身下載 protoc 及 grpc_objective_c_plugin 插件工具 經過命令行來編繹,但更爲簡單的方法是經過 cocoapods 進行編繹並集成到 iOS 項目裏。官方提供了示例 podspec 文件,替換 proto 文件路徑部分便可。
經過分析 podspec 得知,podspec文件主要作了兩件事:
1、經過兩個空的 Pod 依賴下載了 protoc 編繹器工具及 Objective-C 編繹插件。這兩個依賴不包含任何源碼僅僅只是利用 cocoapods a 工具的依賴管理下載編繹工具鏈,所以不會打包進項目裏對項目產生影響。
2、在生成 pod 工程以前經過第一步下載的編繹器執行了一段編繹命令生成了源碼文件到指定目錄並將它做爲項目的依賴集成到了項目中。這一過程與咱們用 pod 管理第三方庫是同樣的,生成的源碼和所須要的依賴能夠在 Pod 工程中直接查看。
一個完整的 gRPC 服務接口會生成兩種 Objective-C 文件,pbobjc 與 pbrpc。pbobjc 類型是 proto文件內的 Message, 也就是咱們一般所說的 Model, 而 pbrpc 則是接口 API。
API 的調用就很是簡單了,與經常使用的網絡庫接口調用相似,gRPC 提供了 delegate 和 block 兩種回調方式。 實際上 gRPC 在編繹出的 .pbrpc.h 文件內提供了兩個版本的接口,只有版本1提供了 block 的回調方式,但版本1的接口官方在註釋裏說明再也不推薦使用,因此暫時推薦使用的回調方式也只有 delegate。
delegate 調用方式
- (void)execRequest {
RTGRectangle *rectangle = [RTGRectangle message];
rectangle.lo.latitude = 405E6;
rectangle.lo.longitude = -750E6;
rectangle.hi.latitude = 410E6;
rectangle.hi.longitude = -745E6;
GRPCUnaryProtoCall *call = [_service listFeaturesWithMessage:rectangle
responseHandler:self //< 指定代理
callOptions:nil];
[call start];
}
// delegate method
// 指定代理回調執行的線程
- (dispatch_queue_t)dispatchQueue {
return dispatch_get_main_queue();
}
- (void)didReceiveProtoMessage:(GPBMessage *)message {
RTGFeature *response = (RTGFeature *)message;
if (response) {
....
}
}
- (void)didCloseWithTrailingMetadata:(NSDictionary *)trailingMetadata error:(NSError *)error {
if (error) {
.....
}
}
複製代碼
block 方試調用接口(不推薦)
RTGPoint *point = [RTGPoint message];
point.latitude = 40E7;
point.longitude = -74E7;
[service getFeatureWithRequest:point handler:^(RTGFeature *response, NSError *error) {
if (response) {
// Successful response received
} else {
// RPC error
}
}];
複製代碼
連續接收和單次請求響應的調用方式是同樣的,惟一不一樣的是連續請求的 didReceiveProtoMessage 回調方法會被調用屢次。
連續發送是調用服務對象的接口方法後返回一個對象,調用這個返回對象的 start 方法。以後再連續寫入(writeMessage)請求對象。
GRPCStreamingProtoCall *call = [_service recordRouteWithResponseHandler:self
callOptions:nil];
[call start];
for (id feature in features) {
RTGPoint *location = [RTGPoint message];
...
[call writeMessage:location];
}
[call finish];
複製代碼
最後再調用 finish 告訴服務端請求數據發送完畢。
發送和接收同時連續時的調用方式就是連續接收和連續發送的結合。
- (void)execRequest {
NSArray *notes = @[[RTGRouteNote noteWithMessage:@"First message" latitude:0 longitude:0],
[RTGRouteNote noteWithMessage:@"Second message" latitude:0]
...];
GRPCStreamingProtoCall *call = [_service routeChatWithResponseHandler:self // 代理
callOptions:nil];
[call start];
for (RTGRouteNote *note in notes) {
[call writeMessage:note]; // 連續發送
}
[call finish]; // 通知接收端發送完畢,無限發送時可不調用
}
// 接收方法會被調用屢次
- (void)didReceiveProtoMessage:(GPBMessage *)message {
RTGRouteNote *note = (RTGRouteNote *)message;
if (note) {
...
}
}
// 發生錯誤或者接收完畢會被調用
- (void)didCloseWithTrailingMetadata:(NSDictionary *)trailingMetadata error:(NSError *)error {
if (!error) {
...
} else {
...
}
}
複製代碼
不管接口創建在哪一種網絡通訊方式都不可避免的存在接口兼容的問題, gRPC 既然做爲通用的服務調用框架天然也須要解決接口兼容問題。接口兼容能夠分爲「前向兼容」、「後向兼容」。舉個栗子,若是接收方升級了本身的接口字段,發送方仍是使用的老版本接口字段,接收方還能正常解析老版本的數據則稱爲後向兼容。若是發送方升級了本身的接口字段,接收方仍是使用的老版本字段,接收方還能解析出來新版本的數據則稱爲向前兼容。
gRPC 使用 Protobuf 做爲消息編碼方式,接口兼容又主要是消息字段的兼容處理,因此接口的兼容實際上就是 Protobuf 解析字段時的兼容處理。處理向後兼容也就是接收方使用新版本時,解析接收到的老版本數據流可能出現的狀況會是新版本新增了字段或者新版本更改了字段,因爲基數據流中不存在新增或修改的字段因此解析時會生成默認值。前向兼容是接收到的數據流包含接收方不可識別的字段,這種狀況會直接丟棄該字段數據。
前向兼容和後向兼容的前提是字段名稱和字段編號在基生命週期內不可重用,也就是說當你須要更改字段名和編號時須要標記老字段爲保留(reserved)字段,以防止之後再有人使用這些用過的字段名和編號。由於對於已經使用過的字段可能會存在於某些老版本的客戶端中,若是再有其餘開發使用被用過了的字段名和編號會使老版本的客戶端收到的數據含義發生改變。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
複製代碼
另外還可對類型進行更改,只不過這種更改限定於同一個系列。例如: (int32, uint32, int64, uint64, and bool)、(sint32, sint64)括號內的類型能夠相互轉換。
gRPC 在客戶端側的功能表現幾乎就是一個網絡框架、API接口與 Model 數據的集合,gRPC 幫咱們實現了網絡請求管理、接口定義與 Model 解析,它徹底屏蔽了網絡數據的轉換,讓使用者無需關心 Model 轉換鏈接管理等細節。調用 gRPC 接口就像調用一個本地異步服務同樣,使用 gRPC 能夠作到徹底替代客戶端的網絡層。