gRPC 基础概念详解
作者:jasonzxpan,騰訊 IEG 運營開發工程師
gRPC (gRPC Remote Procedure Calls) 是 Google 發起的一個開源遠程過程調用系統,該系統基于 HTTP/2 協議傳輸,本文介紹 gRPC 的基礎概念,首先通過關系圖直觀展示這些基礎概念之間關聯,介紹異步 gRPC 的 Server 和 Client 的邏輯;然后介紹 RPC 的類型,閱讀和抓包分析 gRPC 的通信過程協議,gRPC 上下文;最后分析 grpc.pb.h 文件的內容,包括 Stub 的能力、Service 的種類以及與核心庫的關系。
之所以謂之基礎,是這些內容基本不涉及 gRPC Core 的內容。
一、基本概念概覽
grpc-base-concept上圖中列出了 gRPC 基礎概念及其關系圖。其中包括:Service(定義)、RPC、API、Client、Stub、Channel、Server、Service(實現)、ServiceBuilder 等。
接下來,以官方提供的 example/helloworld 為例進行說明。
.proto 文件定義了服務 Greeter 和 API SayHello:
// helloworld.proto // The greeting service definition. service Greeter {// Sends a greetingrpc SayHello (HelloRequest) returns (HelloReply) {} }class GreeterClient 是 Client,是對 Stub 封裝;通過 Stub 可以真正的調用 RPC 請求。
class?GreeterClient?{public:GreeterClient(std::shared_ptr<Channel>?channel):?stub_(Greeter::NewStub(channel))?{}std::string?SayHello(const?std::string&?user)?{ ... private:std::unique_ptr<Greeter::Stub>?stub_; };Channel 提供一個與特定 gRPC server 的主機和端口建立的連接。
Stub 就是在 Channel 的基礎上創建而成的。
target_str?=?"localhost:50051"; auto?channel?=grpc::CreateChannel(target_str,?grpc::InsecureChannelCredentials()); GreeterClient?greeter(channel); std::string?user("world"); std::string?reply?=?greeter.SayHello(user);Server 端需要實現對應的 RPC,所有的 RPC 組成了 Service:
class?GreeterServiceImpl?final?:?public?Greeter::Service?{Status?SayHello(ServerContext*?context,?const?HelloRequest*?request,HelloReply*?reply)?override?{std::string?prefix("Hello?");reply->set_message(prefix?+?request->name());return?Status::OK;} };Server 的創建需要一個 Builder,添加上監聽的地址和端口,注冊上該端口上綁定的服務,最后構建出 Server 并啟動:
ServerBuilder?builder; builder.AddListeningPort(server_address,?grpc::InsecureServerCredentials()); builder.RegisterService(&service); std::unique_ptr<Server>?server(builder.BuildAndStart());RPC 和 API 的區別:RPC (Remote Procedure Call) 是一次遠程過程調用的整個動作,而 API (Application Programming Interface) 是不同語言在實現 RPC 中的具體接口。一個 RPC 可能對應多種 API,比如同步的、異步的、回調的。一次 RPC 是對某個 API 的一次調用,比如:
std::unique_ptr<ClientAsyncResponseReader<HelloReply>?>?rpc(stub_->PrepareAsyncSayHello(&context,?request,?&cq));不管是哪種類型 RPC,都是由 Client 發起請求。
二、異步相關概念
不管是 Client 還是 Server,異步 gRPC 都是利用 CompletionQueue API 進行異步操作。基本的流程:
綁定一個 CompletionQueue 到一個 RPC 調用
利用唯一的 void* Tag 進行讀寫
調用 CompletionQueue::Next() 等待操作完成,完成后通過唯一的 Tag 來判斷對應什么請求/返回進行后續操作
官方文檔 Asynchronous-API tutorial 中有上邊的介紹,并介紹了異步 client 和 server 的解釋,對應這 greeter_async_client.cc 和 greeter_async_server.cc 兩個文件。
Client 看文檔可以理解,但 Server 的代碼復雜,文檔和注釋中的解釋并不是很好理解,接下來會多做一些解釋。
1. 異步 Client
greeter_async_client.cc 中是異步 Client 的 Demo,其中只有一次請求,邏輯簡單。
創建 CompletionQueue
創建 RPC (既 ClientAsyncResponseReader<HelloReply>),這里有兩種方式:
stub_->PrepareAsyncSayHello() + rpc->StartCall()
stub_->AsyncSayHello()
調用 rpc->Finish() 設置請求消息 reply 和唯一的 tag 關聯,將請求發送出去
使用 cq.Next() 等待 Completion Queue 返回響應消息體,通過 tag 關聯對應的請求
[TODO] ClientAsyncResponseReader 在 Finish() 完之后就沒有用了?
2. 異步 Server
RequestSayHello() 這個函數沒有任何的說明。只說是:"we request that the system start processing SayHello requests." 也沒有說跟 cq_->Next(&tag, &ok); 的關系。我這里通過加上一些日志打印,來更清晰的展示 Server 的邏輯:
async-server-log上邊綠色的部分為創建的第一個 CallData 對象地址,橙色的為第二個 CallData 的地址。
創建一個 CallData,初始構造列表中將狀態設置為 CREATE
構造函數中,調用 Process()成員函數,調用 service_->RequestSayHello()后,狀態變更為 PROCESS:
傳入 ServerContext ctx_
傳入 HelloRequest request_
傳入 ServerAsyncResponseWriter<HelloReply> responder_
傳入 ServerCompletionQueue* cq_
將對象自身的地址作為 tag 傳入
該動作,能將事件加入事件循環,可以在 CompletionQueue 中等待
收到請求,cq->Next()的阻塞結束并返回,得到 tag,既上次傳入的 CallData 對象地址
調用 tag 對應 CallData 對象的 Proceed(),此時狀態為 Process
創建新的 CallData 對象以接收新請求
處理消息體并設置 reply_
將狀態設置為 FINISH
調用 responder_.Finish() 將返回發送給客戶端
該動作,能將事件加入到事件循環,可以在 CompletionQueue 中等待
發送完畢,cq->Next()的阻塞結束并返回,得到 tag。現實中,如果發送有異常應當有其他相關的處理
調用 tag 對應 CallData 對象的 Proceed(),此時狀態為 FINISH,delete this 清理自己,一條消息處理完成
3. 關系圖
將上邊的異步 Client 和異步 Server 的邏輯通過關系圖進行展示。右側 RPC 為創建的對象中的內存容,左側使用相同顏色的小塊進行代替。
async-client以下 CallData 并非 gRPC 中的概念,而是異步 Server 在實現過程中為了方便進行的封裝,其中的 Status 也是在異步調用過程中自定義的、用于轉移的狀態。
async-server4. 異步 Client 2
在 example/cpp/helloworld 中還有另外一個異步 Client,對應文件名為 greeter_async_client2.cc。這個例子中使用了兩個線程去分別進行發送請求和處理返回,一個線程批量發出 100 個 SayHello 的請求,另外一個不斷的通過 cq_.Next() 來等待返回。
無論是 Client 還是 Server,在以異步方式進行處理時,都要預先分配好一定的內存/對象,以存儲異步的請求或返回。
5. 回調方式的異步調用
在 example/cpp/helloworld 中,還提供了 callback 相關的 Client 和 Server。
使用回調方式簡介明了,結構上與同步方式相差不多,但是并發有本質的區別。可以通過文件對比,來查看其中的差異。
cd?examples/cpp/helloworld/ vimdiff?greeter_callback_client.cc?greeter_client.cc vimdiff?greeter_callback_server.cc?greeter_server.cc其實,回調方式的異步調用屬于實驗性質的,不建議直接在生產環境使用,這里也只做簡單的介紹:
Notice: This API is EXPERIMENTAL and may be changed or removed at any time.
5.1 回調 Client
發送單個請求,在調用 SayHello 時,除了傳入 Request、 Reply 的地址之外,還需要傳入一個接收 Status 的回調函數。
例子中只有一個請求,因此在 SayHello 之后,就直接通過 condition_variable 的 wait 函數等待回調結束,然后進行后續處理。這樣其實不能進行并發,跟同步請求差別不大。如果要進行大規模的并發,還是需要使用額外的對象進行封裝一下。
stub_->async()->SayHello(&context,?&request,?&reply,[&mu,?&cv,?&done,?&status](Status?s)?{status?=?std::move(s);std::lock_guard<std::mutex>?lock(mu);done?=?true;cv.notify_one();});上邊函數調用函數聲明如下,很明顯這是實驗性(experimental)的接口:
void?Greeter::Stub::experimental_async::SayHello(::grpc::ClientContext*?context,?const?::helloworld::HelloRequest*?request,::helloworld::HelloReply*?response,?std::function<void(::grpc::Status)>?f);5.2 回調 Server
與同步 Server 不同的是:
服務的實現是繼承 Greeter::CallbackService
SayHello 返回的不是狀態,而是 ServerUnaryReactor 指針
通過 CallbackServerContext 獲得 reactor
調用 reactor 的 Finish 函數處理返回狀態
三、流相關概念
可以按照 Client 和 Server 一次發送/返回的是單個消息還是多個消息,將 gRPC 分為:
Unary RPC
Server streaming RPC
Client streaming RPC
Bidirectional streaming RPC
1. Server 對 RPC 的實現
Server 需要實現 proto 中定義的 RPC,每種 RPC 的實現都需要將 ServerContext 作為參數輸入。
如果是一元 (Unary) RPC 調用,則像調用普通函數一樣。將 Request 和 Reply 的對象地址作為參數傳入,函數中將根據 Request 的內容,在 Reply 的地址上寫上對應的返回內容。
//?rpc?GetFeature(Point)?returns?(Feature)?{} Status?GetFeature(ServerContext*?context,?const?Point*?point,?Feature*?feature);如果涉及到流,則會用 Reader 或/和 Writer 作為參數,讀取流內容。如 ServerStream 模式下,只有 Server 端產生流,這時對應的 Server 返回內容,需要使用作為參數傳入的 ServerWriter。這類似于以 'w' 打開一個文件,持續的往里寫內容,直到沒有內容可寫關閉。
//?rpc?ListFeatures(Rectangle)?returns?(stream?Feature)?{} Status?ListFeatures(ServerContext*?context,const?routeguide::Rectangle*?rectangle,ServerWriter<Feature>*?writer);另一方面,Client 來的流,Server 需要使用一個 ServerReader 來接收。這類似于打開一個文件,讀其中的內容,直到讀到 EOF 為止類似。
//?rpc?RecordRoute(stream?Point)?returns?(RouteSummary)?{} Status?RecordRoute(ServerContext*?context,?ServerReader<Point>*?reader,RouteSummary*?summary);如果 Client 和 Server 都使用流,也就是 Bidirectional-Stream 模式下,輸入參數除了 ServerContext 之外,只有一個 ServerReaderWriter 指針。通過該指針,既能讀 Client 來的流,又能寫 Server 產生的流。
例子中,Server 不斷地從 stream 中讀,讀到了就將對應的寫過寫到 stream 中,直到客戶端告知結束;Server 處理完所有數據之后,直接返回狀態碼即可。
//?rpc?RouteChat(stream?RouteNote)?returns?(stream?RouteNote)?{} Status?RouteChat(ServerContext*?context,ServerReaderWriter<RouteNote,?RouteNote>*?stream);2. Client 對 RPC 的調用
Client 在調用一元 (Unary) RPC 時,像調用普通函數一樣,除了傳入 ClientContext 之外,將 Request 和 Response 的地址,返回的是 RPC 狀態:
//?rpc?GetFeature(Point)?returns?(Feature)?{} Status?GetFeature(ClientContext*?context,?const?Point&?request,Feature*?response);Client 在調用 ServerStream RPC 時,不會得到狀態,而是返回一個 ClientReader 的指針:
//?rpc?ListFeatures(Rectangle)?returns?(stream?Feature)?{} unique_ptr<ClientReader<Feature>>?ListFeatures(ClientContext*?context,const?Rectangle&?request);Reader 通過不斷的 Read(),來不斷的讀取流,結束時 Read() 會返回 false;通過調用 Finish() 來讀取返回狀態。
調用 ClientStream RPC 時,則會返回一個 ClientWriter 指針:
//?rpc?RecordRoute(stream?Point)?returns?(RouteSummary)?{} unique_ptr<ClientWriter<Point>>?RecordRoute(ClientContext*?context,Route?Summary*?response);Writer 會不斷的調用 Write() 函數將流中的消息發出;發送完成后調用 WriteDone() 來說明發送完畢;調用 Finish() 來等待對端發送狀態。
而雙向流的 RPC 時,會返回 ClientReaderWriter,:
//?rpc?RouteChat(stream?RouteNote)?returns?(stream?RouteNote)?{} unique_ptr<ClientReaderWriter<RouteNote,?RouteNote>>?RouteChat(ClientContext*?context);前面說明了 Reader 和 Writer 讀取和發送完成的函數調用。因為 RPC 都是 Client 請求而后 Server 響應,雙向流也是要 Client 先發送完自己流,才有 Server 才可能結束 RPC。所以對于雙向流的結束過程是:
stream->WriteDone()
stream->Finish()
示例中創建了單獨的一個線程去發送請求流,在主線程中讀返回流,實現了一定程度上的并發。
3. 流是會結束的
并不似長連接,建立上之后就一直保持,有消息的時候發送。(是否有通過建立一個流 RPC 建立推送機制?)
Client 發送流,是通過 Writer->WritesDone() 函數結束流
Server 發送流,是通過結束 RPC 函數并返回狀態碼的方式來結束流
流接受者,都是通過 Reader->Read() 返回的 bool 型狀態,來判斷流是否結束
Server 并沒有像 Client 一樣調用 WriteDone(),而是在消息之后,將 status code、可選的 status message、可選的 trailing metadata 追加進行發送,這就意味著流結束了。
四、通信協議
本節通過介紹 gRPC 協議文檔描述和對 helloworld 的抓包,來說明 gRPC 到底是如何傳輸的。
官方文檔《gRPC over HTTP2》中有描述 gRPC 基于 HTTP2 的具體實現,主要介紹的就是協議,也就是 gRPC 的請求和返回是如何基于 HTTP 協議構造的。如果不熟悉 HTTP2 可以閱讀一下 RFC 7540。
1. ABNF 語法
ABNF 語法是一種描述協議的標準,gRPC 協議也是使用 ABNF 語法描述,幾種常見的運算符在第三節中有介紹:
3.??Operators3.1.??Concatenation:??Rule1?Rule23.2.??Alternatives:??Rule1?/?Rule23.3.??Incremental?Alternatives:?Rule1?=/?Rule23.4.??Value?Range?Alternatives:??%c##-##3.5.??Sequence?Group:??(Rule1?Rule2)3.6.??Variable?Repetition:??*Rule3.7.??Specific?Repetition:??nRule3.8.??Optional?Sequence:??[RULE]3.9.??Comment:??;?Comment3.10.?Operator?Precedence2. 請求協議
*<element> 表示 element 會重復多次(最少 0 次)。知道這個就能理解概況里的描述了:
Request?→?Request-Headers?*Length-Prefixed-Message?EOS Request-Headers?→?Call-Definition?*Custom-Metadata這表示 Request 是由 3 部分組成,首先是 Request-Headers,接下來是可能多次出現的 Length-Prefixed-Message,最后以一個 EOS 結尾(EOS 表示 End-Of-Stream)。
2.1 Request-Headers
根據上邊的協議描述, Request-Headers 是由一個 Call-Definition 和若干 Custom-Metadata 組成。
[] 表示最多出現一次,比如 Call-Definition 有很多組成部分,其中 Message-Type 等是選填的:
Call-Definition?→?Method?Scheme?Path?TE?[Authority]?[Timeout]?Content-Type?[Message-Type]?[Message-Encoding]?[Message-Accept-Encoding]?[User-Agent]通過 Wireshark 抓包可以看到請求的 Call-Definition 中共有所有要求的 Header,還有額外可選的,比如 user-agent:
wireshark-request-protocol因為 helloworld 的示例比較簡單,請求中沒有填寫自定義的元數據(Custom-Metadata)
2.2 Length-Prefixed-Message
傳輸的 Length-Prefixed-Message 也分為三部分:
Length-Prefixed-Message?→?Compressed-Flag?Message-Length?Message同樣的,Wireshark 抓到的請求中也有這部分信息,并且設置 .proto 文件的搜索路徑之后可以自動解析 PB:
wireshark-length-prefixed-message其中第一個紅框(Compressed-Flag)表示不進行壓縮,第二個紅框(Message-Length)表示消息長度為 7,藍色反選部分則是 Protobuf 序列化的二進制內容,也就是 Message。
在 gRPC 的核心概念介紹時提到,gRPC 默認使用 Protobuf 作為接口定義語言(IDL),也可以使用其他的 IDL 替代 Protobuf:
By default, gRPC uses protocol buffers as the Interface Definition Language (IDL) for describing both the service interface and the structure of the payload messages. It is possible to use other alternatives if desired.
這里 Length-Prefixed-Message 中傳輸的可以是 PB 也可以是 JSON,須通過 Content-Type 頭中描述告知。
2.3 EOS
End-Of-Stream 并沒有單獨的數據去描述,而是通過 HTTP2 的數據幀上帶一個 END_STREAM 的 flag 來標識的。比如 helloworld 中請求的數據幀,也攜帶了 END_STREAM 的標簽:
wireshark-eos3. 返回協議
() 表示括號中的內容作為單個元素對待,/ 表示前后兩個元素可選其一。Response 的定義說明,可以有兩種返回形式,一種是消息頭、消息體、Trailer,另外一種是只帶 Trailer:
Response?→?(Response-Headers?*Length-Prefixed-Message?Trailers)?/?Trailers-Only這里需要區分 gRPC 的 Status 和 HTTP 的 Status 兩種狀態。
Response-Headers?→?HTTP-Status?[Message-Encoding]?[Message-Accept-Encoding]?Content-Type?*Custom-Metadata Trailers-Only?→?HTTP-Status?Content-Type?Trailers Trailers?→?Status?[Status-Message]?*Custom-Metadata不管是哪種形式,最后一部分都是Trailers,其中包含了 gRPC 的狀態碼、狀態信息和額外的自定義元數據。
同樣地,使用 END_STREAM 的 flag 標識最后 Trailer 的結束。
wireshark-response-protocol4. 與 HTTP/2 的關系
The libraries in this repository provide a concrete implemnetation of the gRPC protocol, layered over HTTP/2.
五、上下文
gRPC 支持上下文的傳遞,其主要用途有:
添加自定義的 metadata,能夠通過 gRPC 調用傳遞
控制調用配置,如壓縮、鑒權、超時
從對端獲取 metadata
用于性能測量,比如使用 opencensus 等
客戶端添加自定義的 metadata key-value 對沒有特別的區分,而服務端添加的,則有 inital 和 trailing 兩種 metadata 的區分。這也分別對應這 ClientContext 只有一個添加 Metadata 的函數:
void?AddMetadata?(const?std::string?&meta_key,?const?std::string?&meta_value)而 ServerContext 則有兩個:
void?AddInitialMetadata?(const?std::string?&key,?const?std::string?&value) void?AddTrailingMetadata?(const?std::string?&key,?const?std::string?&value)還有一種 Callback Server 對應的上下文叫做 CallbackServerContext,它與 ServerContext 繼承自同一個基類,功能基本上相同。區別在于:
ServerContext 被 Sync Server 和基于 CQ 的 Async Server 所使用,后者需要用到 AsyncNotifyWhenDone
CallbackServerContext 因為在 CallOnDone 的時候,需要釋放 context,因此需要知道 context_allocator,因此對應設置和獲取 context_allocator 的兩個函數
六、Generated Code
通過 protoc 生成 gRPC 相關的文件,除了用于消息體定義的 xxx.pb.h 和 xxx.pb.cc 文件之外,就是定義 RPC 過程的 xxx.grpc.pb.h 和 xxx.grpc.pb.cc。本節以 helloworld.proto 生成的文件為例,看看 .grpc.pb 相關文件具體定義了些什么。
helloworld.grpc.pb.h 文件中有命名空間 helloworld,其中就僅包含一個類 Greeter,所有的 RPC 相關定義都在 Greeter 當中,這其中又主要分為兩部分:
Client 用于調用 RPC 的媒介 Stub 相關類
Server 端用于實現不同服務的 Service 相關類和類模板
1. Stub
.proto 中的一個 service 只有一個 Stub,該類中會提供對應每個 RPC 所有的同步、異步、回調等方式的函數都包含在該類中,而該類繼承自接口類 StubInterface。
為什么需要一個 StubInterface 來讓 Stub 繼承,而不是直接產生 Stub?別的復雜的 proto 會有多個 Stub 繼承同一個 StubInterface 的情況?不會,因為每個 RPC 對應的函數名是不同。
Greeter 中唯一一個函數是用于創建 Stub 的靜態函數 NewStub:
static?std::unique_ptr<Stub>?NewStub(...)Stub 中同步、異步方式的函數是直接作為 Stub 的成員函數提供,比如針對一元調用:
SayHello
AsyncSayHello
PrepareAsyncSayHello
[TODO] 為什么同步函數SayHello的實現是放在源代碼中,而異步函數AsyncSayHello的實現是放在頭文件中(兩者都是直接 return 的)?
return?::grpc::internal::BlockingUnaryCall<?::helloworld::HelloRequest,?::helloworld::HelloReply,?::grpc::protobuf::MessageLite,?::grpc::protobuf::MessageLite>(channel_.get(),?rpcmethod_SayHello_,?context,?request,?response); return?std::unique_ptr<?::grpc::ClientAsyncResponseReader<?::helloworld::HelloReply>>(AsyncSayHelloRaw(context,?request,?cq));回調方式的 RPC 調用是通過一個 experimental_async 的類進行了封裝(有個 async_stub_ 的成員變量),所以回調 Client 中提到,回調的調用方式用法是 stub_->async()->SayHello(...)。
experimental_async 類定義中將 Stub 類作為自己的友元,自己的成員可以被 Stub 直接訪問,而在 StubInterface 中也對應有一個 experimental_async_interface 的接口類,規定了要實現哪些接口。
2. Service
有幾個概念都叫 Service:proto 文件中 RPC 的集合、proto 文件中 service 產生源文件中的 Greeter::Service 類、gRPC 框架中的 ::grpc::Service 類。本小節說的 Service 就是 helloworld.grpc.pb.h 中的 Greeter::Service。
2.1 Service 是如何定義的
helloworld.grpc.pb.h 文件中共定義了 7 種 Service,拿出最常用的 Service 和 AsyncService 兩個定義來說明下 Service 的定義過程:通過類模板鏈式繼承。
Service 跟其他幾種 Service 不同,直接繼承自 grpc::Service,而其他的 Service 都是由類模板構造出來的,而且使用類模板進行嵌套,最基礎的類就是這里的 Service。
Service 有以下特點:
構造函數利用其父類 grpc::Service 的 AddMethod() 函數,將 .proto 文件中定義的 RPC API,添加到成員變量 methods_ 中(methods_ 是個向量)
AddMethod() 時會創建 RpcServiceMethod 對象,而該對象有一個屬性叫做 api_type_,構造時默認填的 ApiType::SYNC
SayHello 函數不直接聲明為純虛函數,而是以返回 UNIMPLEMENTED 狀態,因為這個類可能被多次、多級繼承
所以 Service 類中的所有 RPC API 都是同步的。
再看 AsyncService 的具體定義:
template?<class?BaseClass>class?WithAsyncMethod_SayHello?:?public?BaseClass?{?...?};typedef?WithAsyncMethod_SayHello<Service?>?AsyncService;所以 AsyncService 的含義就是繼承自 Service,加上了 WithAsyncMethod_SayHello 的新功能:
構造時,將 SayHello (RPC) 對應的 api_type_ 設置為 ApiType::ASYNC
將 SayHello 函數直接禁用掉, abort() + 返回 UNIMPLEMENTED 狀態碼
添加 RequestSayHello() 函數, 異步 Server 小節中有介紹過這個函數用法
通過 gRPC 提供的 route_guide.proto 例子能更明顯的理解這點:
typedef?WithAsyncMethod_GetFeature<?\WithAsyncMethod_ListFeatures<?\WithAsyncMethod_RecordRoute<?\WithAsyncMethod_RouteChat<Service>?>?>?>AsyncService;這里 RouteGuide 服務中有 4 個 RPC,GetFeature、ListFeatures、RecordRoute、RouteChat,通過 4 個WithAsyncMethod_{RPC_name} 的類模板嵌套,能將 4 個 API 都設置成 ApiType::ASYNC、添加上對應的 RequestXXX() 函數、禁用同步函數。
[TODO] 通過類模板嵌套繼承的方式,有什么好處? 為什么不直接實現 AsyncService 這個類呢?
2.2 Service 的種類
helloworld.grpc.pb.h 文件中 7 種 Service 中,有 3 對 Service 的真正含義都相同(出于什么目的使用不同的名稱?),實際只剩下 4 種 Service。前三種在前邊的同步、異步、回調 Server 的介紹中都有涉及。
Service
AsyncService
CallbackService
ExperimentalCallbackService -- 等價于 CallbackService
StreamedUnaryService
SplitStreamedService -- 等價于 Service
StreamedService -- 等價于 StreamedUnaryService
其實這些不同類型的 Service 是跟前邊提到的 api_type_ 有關。使用不同的 ::grpc::Service::MarkMethodXXX 設置不同的 ApiType 會產生不同的 API 模板類,所有 API 模板類級聯起來,就得到了不同的 Service。這三者的關系簡單列舉如下:
image-20210707145111294另外還有兩種模板是通過設置其他屬性產生的,這里暫時不做介紹:
[TODO] 頭文件中沒有用到的類模板在什么場景中會用到?
3. 與 ::grpc 核心庫的關系
Stub 類中主要是用到 gRPC Channel 和不同類型 RPC 對應的方法實現:
Service 類則繼承自 ::grpc::Service,具備其父類的能力,需要自己實現一些 RPC 方法具體的處理邏輯。其它 Service 涉及到 gRPC 核心庫的聯系有:
AsyncService::RequestSayHello() 調用 ::grpc::Service::RequestAsyncUnary。
CallbackService::SayHello() 函數返回的是 ::grpc::ServerUnaryReactor 指針。
CallbackService::SetMessageAllocatorFor_SayHello() 函數中調用 ::grpc::internal::CallbackUnaryHandler::SetMessageAllocator() 函數設置 RPC 方法的回調的消息分配器。
[TODO] SetMessageAllocatorFor_SayHello() 函數并沒有被調用到,默認該分配器指針初始值為空,表示用戶預先自己分配好而無需回調時分配?
參考資料
https://grpc.io/docs/what-is-grpc/core-concepts/
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
https://grpc.io/blog/wireshark/
最近其他文章:
深入淺出 Linux 驚群:現象、原因和解決方案
不吹不擂,一文揭秘鴻蒙操作系統
Nginx 最全操作總結
騰訊程序員最新視頻
十個微信實用技巧
總結
以上是生活随笔為你收集整理的gRPC 基础概念详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 不吹不擂,一文揭秘鸿蒙操作系统
- 下一篇: 带你快速了解 Docker 和 Kube