自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何利用Google的protobuf,來思考、設(shè)計、實現(xiàn)自己的RPC框架

開發(fā)
這篇文章我們就來聊一聊 RPC 的相關(guān)內(nèi)容,來看一下如何利用 Google 的開源序列化工具protobuf,來實現(xiàn)一個我們自己的 RPC 框架。

[[394559]]

一、前言

在嵌入式系統(tǒng)中,很少需要使用到 RPC (Remote Procedure Call)遠程方法調(diào)用,因為在大部分情況下,實現(xiàn)一個產(chǎn)品功能的所有進程、線程都是運行在同一個硬件設(shè)備中的。

但是在一些特殊的場景中,RPC 調(diào)用還是很有市場的,比如:

  • 在計算密集型產(chǎn)品中,需要調(diào)用算力更強的中央服務(wù)器提供的算法函數(shù);

因此,利用 RPC 來利用遠程提供的服務(wù),相對于其他的機制來說,有更多的優(yōu)勢。

這篇文章我們就來聊一聊 RPC 的相關(guān)內(nèi)容,來看一下如何利用 Google 的開源序列化工具protobuf,來實現(xiàn)一個我們自己的 RPC 框架。

  • 序列化[1]:將結(jié)構(gòu)數(shù)據(jù)或?qū)ο筠D(zhuǎn)換成能夠被存儲和傳輸(例如網(wǎng)絡(luò)傳輸)的格式,同時應(yīng)當(dāng)要保證這個序列化結(jié)果在之后(可能在另一個計算環(huán)境中)能夠被重建回原來的結(jié)構(gòu)數(shù)據(jù)或?qū)ο蟆?/li>

我會以 protobuf 中的一些關(guān)鍵 C++ 類作為突破口,來描述從客戶端發(fā)起調(diào)用,到服務(wù)端響應(yīng),這個完整執(zhí)行序列。也就是下面這張圖:

這張圖大概畫了 2 個小時(邊看代碼,邊畫圖),我已經(jīng)盡力了,雖然看起來有點亂。

在下面的描述中,我會根據(jù)每一部分的主題,把這張圖拆成不同的模塊,從空間(文件和類的結(jié)構(gòu))和時間(函數(shù)的調(diào)用順序、數(shù)據(jù)流向)這兩個角度,來描述圖中的每一個元素,我相信聰明的你一定會看明白的!

希望你看了這篇文章之后,對 RPC 框架的設(shè)計過程有一個基本的認識和理解,應(yīng)對面試官的時候,關(guān)于 RPC 框架設(shè)計的問題應(yīng)該綽綽有余了。

如果在項目中恰好選擇了 protobuf,那么根據(jù)這張圖中的模塊結(jié)構(gòu)和函數(shù)調(diào)用流程分析,可以協(xié)助你更好的完成每一個模塊的開發(fā)。

注意:這篇文章不會聊什么內(nèi)容:

  1. protfobuf 的源碼實現(xiàn);
  2. protfobuf 的編碼算法;

二、RPC 基礎(chǔ)概念

1. RPC 是什么?

RPC (Remote Procedure Call)從字面上理解,就是調(diào)用一個方法,但是這個方法不是運行在本地,而是運行在遠端的服務(wù)器上。也就是說,客戶端應(yīng)用可以像調(diào)用本地函數(shù)一樣,直接調(diào)用運行在遠端服務(wù)器上的方法。

下面這張圖描述了 RPC 調(diào)用的基本流程:

假如,我們的應(yīng)用程序需要調(diào)用一個算法函數(shù)來獲取運動軌跡:

  1. int getMotionPath(float *input, int intputLen, float *outputint outputLen) 

如果計算過程不復(fù)雜,可以把這個算法函數(shù)和應(yīng)用程序放在本地的同一個進程中,以源代碼或庫的方式提供計算服務(wù),如下圖:

但是,如果這個計算過程比較復(fù)雜,需要耗費一定的資源(時間和空間),本地的 CPU 計算能力根本無法支撐,那么就可以把這個函數(shù)放在 CPU 能力更強的服務(wù)器上。

此時,調(diào)用過程如下圖這樣:

從功能上來看,應(yīng)用程序仍然是調(diào)用遠程服務(wù)器上的一個方法,也就是虛線部分。但是由于他們運行在不同的實體設(shè)備上,更不是在同一個進程中,因此,如果想調(diào)用成功就一定需要利用網(wǎng)絡(luò)來傳輸數(shù)據(jù)。

初步接觸 RPC 的朋友可能會提出:

那我可以在應(yīng)用程序中把算法需要的輸入數(shù)據(jù)打包好,通過網(wǎng)絡(luò)發(fā)送給算法服務(wù)器;服務(wù)器計算出結(jié)果后,再打包好返回給應(yīng)用程序就可以了。

這句話說的非常對,從功能上來說,這個描述過程就是 RPC 所需要做的所有事情。

不過,在這個過程中,有很多問題需要我們來手動解決:

  1. 如何處理通信問題?TCP or UDP or HTTP?或者利用其他的一些已有的網(wǎng)絡(luò)協(xié)議?
  2. 如何把數(shù)據(jù)進行打包?服務(wù)端接收到打包的數(shù)據(jù)之后,如何還原數(shù)據(jù)?
  3. 對于特定領(lǐng)域的問題,可以專門寫一套實現(xiàn)來解決,但是對于通用的遠程調(diào)用,怎么做到更靈活、更方便?

為了解決以上這幾個問題,于是 RPC 遠程調(diào)用框架就誕生了!

圖中的綠色背景部分,就是 RPC 框架需要做的事情。

對于應(yīng)用程序來說,Client 端代理就相當(dāng)于是算法服務(wù)的“本地代理人”,至于這個代理人是怎么來處理剛才提到的那幾個問題、然后從真正的算法服務(wù)器上得到結(jié)果,這就不需要應(yīng)用程序來關(guān)心了。

結(jié)合文章的第一張圖中,從應(yīng)用程序的角度看,它只是執(zhí)行了一個函數(shù)調(diào)用(步驟1),然后就立刻得到了結(jié)果(步驟10),這中間的所有步驟(2-9),全部是 RPC 框架來處理,而且能夠靈活的處理各種不同的請求、響應(yīng)數(shù)據(jù)。

鋪墊到這里,我就可以更明確的再次重復(fù)一下了:這篇文章的目的,就是介紹如何利用 protobuf 來實現(xiàn)圖中的綠色部分的功能。

最終的目的,將會輸出一個 RPC 遠程調(diào)用框架的庫文件(動態(tài)庫、靜態(tài)庫):

  1. 服務(wù)器端利用這個庫,在網(wǎng)絡(luò)上提供函數(shù)調(diào)用服務(wù);
  2. 客戶端利用這個庫,遠程調(diào)用位于服務(wù)器上的函數(shù);

2. 需要解決什么問題?

既然我們是介紹 RPC 框架,那么需要解決的問題就是一個典型的 RPC 框架所面對問題,如下:

  1. 解決函數(shù)調(diào)用時,數(shù)據(jù)結(jié)構(gòu)的約定問題;
  2. 解決數(shù)據(jù)傳輸時,序列化和反序列化問題;
  3. 解決網(wǎng)絡(luò)通信問題;

這 3 個問題是所有的 RPC 框架都必須解決的,這是最基本的問題,其他的考量因素就是:速度更快、成本更低、使用更靈活、易擴展、向后兼容、占用更少的系統(tǒng)資源等等。

另外還有一個考量因素:跨語言。比如:客戶端可以用 C 語言實現(xiàn),服務(wù)端可以用 C/C++、Java或其他語言來實現(xiàn),在技術(shù)選型時這也是非常重要的考慮因素。

3. 有哪些開源實現(xiàn)?

從上面的介紹中可以看出來,RPC 的最大優(yōu)勢就是降低了客戶端的函數(shù)調(diào)用難度,調(diào)用遠程的服務(wù)就好像在調(diào)用本地的一個函數(shù)一樣。

因此,各種大廠都開發(fā)了自己的 RPC 框架,例如:

  • Google 的 gRPC;
  • Facebook 的 thrift;
  • 騰訊的 Tars;
  • 百度的 BRPC;

另外,還有很多小廠以及個人,也會發(fā)布一些 RPC 遠程調(diào)用框架(tinyRPC,forestRPC,EasyRPC等等)。每一家 RPC 的特點,感興趣的小伙伴可以自行去搜索比對,這里對 gRPC 多說幾句,

我們剛才主要聊了 protobuf,其實它只是解決了序列化的問題,對于一個完整的 RPC 框架,還缺少網(wǎng)絡(luò)通信這個步驟。

gRPC 就是利用了 protobuf,來實現(xiàn)了一個完整的 RPC 遠程調(diào)用框架,其中的通信部分,使用的是 HTTP 協(xié)議。

三、protobuf 基本使用

1. 基本知識

Protobuf 是 Protocol Buffers 的簡稱, 它是 Google 開發(fā)的一種跨語言、跨平臺、可擴展的用于序列化數(shù)據(jù)協(xié)議,

Protobuf 可以用于結(jié)構(gòu)化數(shù)據(jù)序列化(串行化),它序列化出來的數(shù)據(jù)量少,再加上以 K-V 的方式來存儲數(shù)據(jù),非常適用于在網(wǎng)絡(luò)通訊中的數(shù)據(jù)載體。

只要遵守一些簡單的使用規(guī)則,可以做到非常好的兼容性和擴展性,可用于通訊協(xié)議、數(shù)據(jù)存儲等領(lǐng)域的語言無關(guān)、平臺無關(guān)、可擴展的序列化結(jié)構(gòu)數(shù)據(jù)格式。

Protobuf 中最基本的數(shù)據(jù)單元是 message ,并且在 message 中可以多層嵌套 message 或其它的基礎(chǔ)數(shù)據(jù)類型的成員。

Protobuf 是一種靈活,高效,自動化機制的結(jié)構(gòu)數(shù)據(jù)序列化方法,可類比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更簡單,而且它支持 Java、C++、Python 等多種語言。

2. 使用步驟

Step1:創(chuàng)建 .proto 文件,定義數(shù)據(jù)結(jié)構(gòu)

例如,定義文件 echo_service.proto, 其中的內(nèi)容為:

  1. message EchoRequest { 
  2.     string message = 1; 
  3.  
  4. message EchoResponse { 
  5.     string message = 1; 
  6.  
  7. message AddRequest { 
  8.     int32 a = 1; 
  9.     int32 b = 2; 
  10.  
  11. message AddResponse { 
  12.     int32 result = 1; 
  13.  
  14. service EchoService { 
  15.     rpc Echo(EchoRequest) returns(EchoResponse); 
  16.     rpc Add(AddRequest) returns(AddResponse); 

最后的 service EchoService,是讓 protoc 生成接口類,其中包括 2 個方法 Echo 和 Add:

  • Echo 方法:客戶端調(diào)用這個方法,請求的“數(shù)據(jù)結(jié)構(gòu)” EchoRequest 中包含一個 string 類型,也就是一串字符;服務(wù)端返回的“數(shù)據(jù)結(jié)構(gòu)” EchoResponse 中也是一個 string 字符串;
  • Add 方法:客戶端調(diào)用這個方法,請求的“數(shù)據(jù)結(jié)構(gòu)” AddRequest 中包含 2 個整型數(shù)據(jù),服務(wù)端返回的“數(shù)據(jù)結(jié)構(gòu)” AddResponse 中包含一個整型數(shù)據(jù)(計算結(jié)果);

Step2: 使用 protoc 工具,來編譯 .proto 文件,生成接口(類以及相應(yīng)的方法)

  1. protoc echo_service.proto -I./ --cpp_out=./ 

執(zhí)行以上命令,即可生成兩個文件:echo_service.pb.h, echo_service.pb.c,在這 2 個文件中,定義了 2 個重要的類,也就是下圖中綠色部分:

EchoService 和 EchoService_Stub 這 2 個類就是接下來要介紹的重點。我把其中比較重要的內(nèi)容摘抄如下(為減少干擾,把命名空間字符都去掉了):

  1. class EchoService : public ::PROTOBUF_NAMESPACE_ID::Service { 
  2.     virtual void Echo(RpcController* controller,  
  3.                     EchoRequest* request, 
  4.                     EchoResponse* response, 
  5.                     Closure* done); 
  6.                      
  7.    virtual void Add(RpcController* controller, 
  8.                     AddRequest* request, 
  9.                     AddResponse* response, 
  10.                     Closure* done); 
  11.                      
  12.     void CallMethod(MethodDescriptor* method, 
  13.                   RpcController* controller, 
  14.                   Message* request, 
  15.                   Message* response, 
  16.                   Closure* done); 

  1. class EchoService_Stub : public EchoService { 
  2.  public
  3.   EchoService_Stub(RpcChannel* channel); 
  4.  
  5.   void Echo(RpcController* controller, 
  6.             EchoRequest* request, 
  7.             EchoResponse* response, 
  8.             Closure* done); 
  9.              
  10.   void Add(RpcController* controller, 
  11.             AddRequest* request, 
  12.             AddResponse* response, 
  13.             Closure* done); 
  14.              
  15. private: 
  16.     // 成員變量,比較關(guān)鍵 
  17.     RpcChannel* channel_; 
  18. }; 

Step3:服務(wù)端程序?qū)崿F(xiàn)接口中定義的方法,提供服務(wù);客戶端調(diào)用接口函數(shù),調(diào)用遠程的服務(wù)。

請關(guān)注上圖中的綠色部分。

(1)服務(wù)端:EchoService

EchoService 類中的兩個方法 Echo 和 Add 都是虛函數(shù),我們需要繼承這個類,定義一個業(yè)務(wù)層的服務(wù)類 EchoServiceImpl,然后實現(xiàn)這兩個方法,以此來提供遠程調(diào)用服務(wù)。

  • EchoService 類中也給出了這兩個函數(shù)的默認實現(xiàn),只不過是提示錯誤信息:
  1. void EchoService::Echo() { 
  2.   controller->SetFailed("Method Echo() not implemented."); 
  3.   done->Run(); 

  1. void EchoService::Add() { 
  2.   controller->SetFailed("Method Add() not implemented."); 
  3.   done->Run(); 

圖中的 EchoServiceImpl 就是我們定義的類,其中實現(xiàn)了 Echo 和 Add 這兩個虛函數(shù):

  1. void EchoServiceImpl::Echo(RpcController* controller, 
  2.                    EchoRequest* request, 
  3.                    EchoResponse* response, 
  4.                    Closure* done) 
  5.     // 獲取請求消息,然后在末尾加上信息:", welcome!",返回給客戶端 
  6.     response->set_message(request->message() + ", welcome!"); 
  7.     done->Run(); 
  8.  
  9. void EchoServiceImpl::Add(RpcController* controller, 
  10.                    AddRequest* request, 
  11.                    AddResponse* response, 
  12.                    Closure* done) 
  13.     // 獲取請求數(shù)據(jù)中的 2 個整型數(shù)據(jù) 
  14.     int32_t a = request->a(); 
  15.     int32_t b = request->b(); 
  16.  
  17.     // 計算結(jié)果,然后放入響應(yīng)數(shù)據(jù)中 
  18.     response->set_result(a + b); 
  19.  
  20.     done->Run(); 

(2)客戶端:EchoService_Stub

EchoService_Stub 就相當(dāng)于是客戶端的代理,應(yīng)用程序只要把它"當(dāng)做"遠程服務(wù)的替身,直接調(diào)用其中的函數(shù)就可以了(圖中左側(cè)的步驟1)。

因此,EchoService_Stub 這個類中肯定要實現(xiàn) Echo 和 Add 這 2 個方法,看一下 protobuf 自動生成的實現(xiàn)代碼:

  1. void EchoService_Stub::Echo(RpcController* controller, 
  2.                             EchoRequest* request, 
  3.                             EchoResponse* response, 
  4.                             Closure* done) { 
  5.   channel_->CallMethod(descriptor()->method(0), 
  6.                        controller,  
  7.                        request,  
  8.                        response,  
  9.                        done); 
  10.  
  11. void EchoService_Stub::Add(RpcController* controller, 
  12.                             AddRequest* request, 
  13.                             AddResponse* response, 
  14.                             Closure* done) { 
  15.   channel_->CallMethod(descriptor()->method(1), 
  16.                        controller,  
  17.                        request,  
  18.                        response,  
  19.                        done); 

看到?jīng)],每一個函數(shù)都調(diào)用了成員變量 channel_ 的 CallMethod 方法(圖中左側(cè)的步驟2),這個成員變量的類型是 google::protobuf:RpcChannel。

從字面上理解:channel 就像一個通道,是用來解決數(shù)據(jù)傳輸問題的。也就是說 channel_->CallMethod 方法會把所有的數(shù)據(jù)結(jié)構(gòu)序列化之后,通過網(wǎng)絡(luò)發(fā)送給服務(wù)器。

既然 RpcChannel 是用來解決網(wǎng)絡(luò)通信問題的,因此客戶端和服務(wù)端都需要它們來提供數(shù)據(jù)的接收和發(fā)送。

圖中的RpcChannelClient是客戶端使用的 Channel, RpcChannelServer是服務(wù)端使用的 Channel,它倆都是繼承自 protobuf 提供的 RpcChannel。

注意:這里的 RpcChannel,只是提供了網(wǎng)絡(luò)通信的策略,至于通信的機制是什么(TCP? UDP? HTTP?),protobuf 并不關(guān)心,這需要由 RPC 框架來決定和實現(xiàn)。

protobuf 提供了一個基類 RpcChannel,其中定義了CallMethod方法。我們的 RPC 框架中,客戶端和服務(wù)端實現(xiàn)的 Channel 必須繼承 protobuf 中的 RpcChannel,然后重載 CallMethod這個方法。

CallMethod 方法的幾個參數(shù)特別重要,我們通過這些參數(shù),來利用 protobuf 實現(xiàn)序列化、控制函數(shù)調(diào)用等操作,也就是說這些參數(shù)就是一個紐帶,把我們寫的代碼與 protobuf 提供的功能,連接在一起。

我們這里選了libevent這個網(wǎng)絡(luò)庫來實現(xiàn) TCP 通信。

四、libevent

實現(xiàn) RPC 框架,需要解決 2 個問題:通信和序列化。protobuf 解決了序列化問題,那么還需要解決通信問題。

有下面幾種通信方式備選:

  1. TCP 通信;
  2. UDP 通信;
  3. HTTP 通信;

如何選擇,那就是見仁見智的事情了,比如 gRPC 選擇的就是 HTTP,也工作的很好,更多的實現(xiàn)選擇的是 TCP 通信。

下面就是要決定:是從 socket 層次開始自己寫?還是利用已有的一些開源網(wǎng)絡(luò)庫來實現(xiàn)通信?

既然標題已經(jīng)是 libevent 了,那肯定選擇的就是它!當(dāng)然還有很多其他優(yōu)秀的網(wǎng)絡(luò)庫可以利用,比如:libev, libuv 等等。

1. libevent 簡介

Libevent 是一個用 C 語言編寫的、輕量級、高性能、基于事件的網(wǎng)絡(luò)庫。

主要有以下幾個亮點:

1. 事件驅(qū)動( event-driven),高性能;

2. 輕量級,專注于網(wǎng)絡(luò);源代碼相當(dāng)精煉、易讀;

3. 跨平臺,支持 Windows、 Linux、*BSD 和 Mac Os;

4. 支持多種 I/O 多路復(fù)用技術(shù), epoll、 poll、 dev/poll、 select 和 kqueue 等;

5. 支持 I/O,定時器和信號等事件;注冊事件優(yōu)先級。

從我們使用者的角度來看,libevent 庫提供了以下功能:當(dāng)一個文件描述符的特定事件(如可讀,可寫或出錯)發(fā)生了,或一個定時事件發(fā)生了, libevent 就會自動執(zhí)行用戶注冊的回調(diào)函數(shù),來接收數(shù)據(jù)或者處理事件。

此外,libevent 還把 fd 讀寫、信號、DNS、定時器甚至idle(空閑) 都抽象化成了event(事件)。

總之一句話:使用很方便,功能很強大!

2. 基本使用

libevent 是基于事件的回調(diào)函數(shù)機制,因此在啟動監(jiān)聽 socket 之前,只要設(shè)置好相應(yīng)的回調(diào)函數(shù),當(dāng)有事件或者網(wǎng)絡(luò)數(shù)據(jù)到來時,libevent 就會自動調(diào)用回調(diào)函數(shù)。

  1. struct event_base  *m_evBase = event_base_new(); 
  2. struct bufferevent *m_evBufferEvent =  bufferevent_socket_new( 
  3.         m_evBase, [socket Id],  
  4.         BEV_OPT_CLOSE_ON_FREE | BEV_OPT_THREADSAFE);   
  5. bufferevent_setcb(m_evBufferEvent,  
  6.         [讀取數(shù)據(jù)回調(diào)函數(shù)],  
  7.         NULL,  
  8.         [事件回調(diào)函數(shù)],  
  9.         [回調(diào)函數(shù)傳參]);   
  10.  
  11. // 開始監(jiān)聽 socket 
  12. event_base_dispatch(m_evBase); 

有一個問題需要注意:protobuf 序列化之后的數(shù)據(jù),全部是二進制的。

libevent 只是一個網(wǎng)絡(luò)通信的機制,如何處理接收到的二進制數(shù)據(jù)(粘包、分包的問題),是我們需要解決的問題。

五、實現(xiàn) RPC 框架

從剛才的第三部分: 自動生成的幾個類EchoService, EchoService_Stub中,已經(jīng)能夠大概看到 RPC 框架的端倪了。這里我們再整合在一起,看一下更具體的細節(jié)部分。

1. 基本框架構(gòu)思

我把圖中的干擾細節(jié)全部去掉,得到下面這張圖:

其中的綠色部分就是我們的 RPC 框架需要實現(xiàn)的部分,功能簡述如下:

1. EchoService:服務(wù)端接口類,定義需要實現(xiàn)哪些方法;

2. EchoService_Stub: 繼承自 EchoService,是客戶端的本地代理;

3. RpcChannelClient: 用戶處理客戶端網(wǎng)絡(luò)通信,繼承自 RpcChannel;

4. RpcChannelServer: 用戶處理服務(wù)端網(wǎng)絡(luò)通信,繼承自 RpcChannel;

應(yīng)用程序:

1. EchoServiceImpl:服務(wù)端應(yīng)用層需要實現(xiàn)的類,繼承自 EchoService;

2. ClientApp: 客戶端應(yīng)用程序,調(diào)用 EchoService_Stub 中的方法;

2. 元數(shù)據(jù)的設(shè)計

在 echo_servcie.proto 文件中,我們按照 protobuf 的語法規(guī)則,定義了幾個 Message,可以看作是“數(shù)據(jù)結(jié)構(gòu)”:

1. Echo 方法相關(guān)的“數(shù)據(jù)結(jié)構(gòu)”:EchoRequest, EchoResponse。

2. Add 方法相關(guān)的“數(shù)據(jù)結(jié)構(gòu)”:AddRequest, AddResponse。

這幾個數(shù)據(jù)結(jié)構(gòu)是直接與業(yè)務(wù)層相關(guān)的,是我們的客戶端和服務(wù)端來處理請求和響應(yīng)數(shù)據(jù)的一種約定。

為了實現(xiàn)一個基本完善的數(shù)據(jù) RPC 框架,我們還需要其他的一些“數(shù)據(jù)結(jié)構(gòu)”來完成必要的功能,例如:

1. 消息 Id 管理;

2. 錯誤處理;

3. 同步調(diào)用和異步調(diào)用;

4. 超時控制;

另外,在調(diào)用函數(shù)時,請求和響應(yīng)的“數(shù)據(jù)結(jié)構(gòu)”是不同的數(shù)據(jù)類型。為了便于統(tǒng)一處理,我們把請求數(shù)據(jù)和響應(yīng)數(shù)據(jù)都包裝在一個統(tǒng)一的 RPC “數(shù)據(jù)結(jié)構(gòu)”中,并用一個類型字段(type)來區(qū)分:某個 RPC 消息是請求數(shù)據(jù),還是響應(yīng)數(shù)據(jù)。

根據(jù)以上這些想法,我們設(shè)計出下面這樣的元數(shù)據(jù):

  1. // 消息類型 
  2. enum MessageType 
  3.     RPC_TYPE_UNKNOWN = 0; 
  4.     RPC_TYPE_REQUEST = 1; 
  5.     RPC_TYPE_RESPONSE = 2; 
  6.     RPC_TYPE_ERROR = 3; 
  7.  
  8. // 錯誤代碼 
  9. enum ErrorCode 
  10.     RPC_ERR_OK = 0; 
  11.     RPC_ERR_NO_SERVICE = 1; 
  12.     RPC_ERR_NO_METHOD = 2; 
  13.     RPC_ERR_INVALID_REQUEST = 3; 
  14.     RPC_ERR_INVALID_RESPONSE = 4 
  15.  
  16. message RpcMessage 
  17.     MessageType type = 1;       // 消息類型 
  18.     uint64      id   = 2;       // 消息id 
  19.     string service   = 3;       // 服務(wù)名稱 
  20.     string method    = 4;       // 方法名稱 
  21.     ErrorCode error  = 5;       // 錯誤代碼 
  22.  
  23.     bytes request    = 100;     // 請求數(shù)據(jù) 
  24.     bytes response   = 101;     // 響應(yīng)數(shù)據(jù) 

注意: 這里的 request 和 response,它們的類型都是 byte。

客戶端在發(fā)送數(shù)據(jù)時:

  • 首先,構(gòu)造一個 RpcMessage 變量,填入各種元數(shù)據(jù)(type, id, service, method, error);
  • 然后,序列化客戶端傳入的請求對象(EchoRequest), 得到請求數(shù)據(jù)的字節(jié)碼;
  • 再然后,把請求數(shù)據(jù)的字節(jié)碼插入到 RpcMessage 中的 request 字段;
  • 最后,把 RpcMessage 變量序列化之后,通過 TCP 發(fā)送出去。

如下圖:

服務(wù)端在接收到 TCP 數(shù)據(jù)時,執(zhí)行相反的操作:

  • 首先,把接收到的 TCP 數(shù)據(jù)反序列化,得到一個 RpcMessage 變量;
  • 然后,根據(jù)其中的 type 字段,得知這是一個調(diào)用請求,于是根據(jù) service 和 method 字段,構(gòu)造出兩個類實例:EchoRequest 和 EchoResponse(利用了 C++ 中的原型模式);
  • 最后,從 RpcMessage 消息中的 request 字段反序列化,來填充 EchoRequest 實例;

這樣就得到了這次調(diào)用請求的所有數(shù)據(jù)。如下圖:

3. 客戶端發(fā)送請求數(shù)據(jù)

這部分主要描述下圖中綠色部分的內(nèi)容:

Step1: 業(yè)務(wù)層客戶端調(diào)用 Echo() 函數(shù)

  1. // ip, port 是服務(wù)端網(wǎng)絡(luò)地址 
  2. RpcChannel *rpcChannel = new RpcChannelClient(ip, port); 
  3. EchoService_Stub *serviceStub = new EchoService_Stub(rpcChannel); 
  4. serviceStub->Echo(...); 

上文已經(jīng)說過,EchoService_Stub 中的 Echo 方法,會調(diào)用其成員變量 channel_ 的 CallMethod方法,因此,需要提前把實現(xiàn)好的 RpcChannelClient 實例,作為構(gòu)造函數(shù)的參數(shù),注冊到 EchoService_Stub 中。

Step2: EchoService_Stub 調(diào)用 channel_.CallMethod() 方法

這個方法在 RpcChannelClient (繼承自 protobuf 中的 RpcChannel 類)中實現(xiàn),它主要的任務(wù)就是:把 EchoRequest 請求數(shù)據(jù),包裝在 RPC 元數(shù)據(jù)中,然后序列化得到二進制數(shù)據(jù)。

  1. // 創(chuàng)建 RpcMessage 
  2. RpcMessage message; 
  3.  
  4. // 填充元數(shù)據(jù) 
  5. message.set_type(RPC_TYPE_REQUEST); 
  6. message.set_id(1); 
  7. message.set_service("EchoService"); 
  8. message.set_method("Echo"); 
  9.  
  10. // 序列化請求變量,填充 request 字段 
  11. // (這里的 request 變量,是客戶端程序傳進來的) 
  12. message.set_request(request->SerializeAsString()); 
  13.  
  14. // 把 RpcMessage 序列化 
  15. std::string message_str; 
  16. message.SerializeToString(&message_str); 

Step3: 通過 libevent 接口函數(shù)發(fā)送 TCP 數(shù)據(jù)

  1. bufferevent_write(m_evBufferEvent, [二進制數(shù)據(jù)]); 

4. 服務(wù)端接收請求數(shù)據(jù)

這部分主要描述下圖中綠色部分的內(nèi)容:

Step4: 第一次反序列化數(shù)據(jù)

RpcChannelServer 是負責(zé)處理服務(wù)端的網(wǎng)絡(luò)數(shù)據(jù),當(dāng)它接收到 TCP 數(shù)據(jù)之后,首先進行第一次反序列化,得到 RpcMessage 變量,這樣就獲得了 RPC 元數(shù)據(jù),包括:消息類型(請求RPC_TYPE_REQUEST)、消息 Id、Service 名稱("EchoServcie")、Method 名稱("Echo")。

  1. RpcMessage rpcMsg; 
  2.  
  3. // 第一次反序列化 
  4. rpcMsg.ParseFromString(tcpData);  
  5.  
  6. // 創(chuàng)建請求和響應(yīng)實例 
  7. auto *serviceDesc = service->GetDescriptor(); 
  8. auto *methodDesc = serviceDesc->FindMethodByName(rpcMsg.method()); 

從請求數(shù)據(jù)中獲取到請求服務(wù)的 Service 名稱(serviceDesc)之后,就可以查找到服務(wù)對象EchoService 了,因為我們也拿到了請求方法的名稱(methodDesc),此時利用 C++ 中的原型模式,構(gòu)造出這個方法所需要的請求對象和響應(yīng)對象,如下:

  1. // 構(gòu)造 request & response 對象 
  2. auto *echoRequest = service->GetRequestPrototype(methodDesc).New(); 
  3. auto *echoResponse = service->GetResponsePrototype(methodDesc).New(); 

構(gòu)造出請求對象 echoRequest 之后,就可以用 TCP 數(shù)據(jù)中的請求字段(即: rpcMsg.request)來第二次反序列化了,此時就還原出了這次方法調(diào)用中的 參數(shù),如下:

  1. // 第二次反序列化: 
  2. request->ParseFromString(rpcMsg.request()); 

這里有一個內(nèi)容需要補充一下: EchoService 服務(wù)是如何被查找到的?

在服務(wù)端可能同時運行了 很多個 Service 以提供不同的服務(wù),我們的 EchoService 只是其中的服務(wù)之一。那么這就需要解決一個問題:在從請求數(shù)據(jù)中提取出 Service 和 Method 的名稱之后,如何找到 EchoService 實例?

一般的做法是:在服務(wù)端有一個 Service 服務(wù)對象池,當(dāng) RpcChannelServer 接收到調(diào)用請求后,到這個池子中 查找相應(yīng)的 Service 對象,對于我們的示例來說,就是要查找 EchoServcie 對象,例如:

  1. std::map<std::string, google::protobuf::Service *> m_spServiceMap; 
  2.  
  3. // 在服務(wù)端啟動的時候,把一個 EchoServcie 實例注冊到池子中 
  4. EchoService *echoService = new EchoServiceImpl(); 
  5. m_spServiceMap->insert("EchoService", echoService); 

由于EchoService示例已經(jīng)提前創(chuàng)建好,并 注冊到 Service 對象池中(以 名稱字符串作為關(guān)鍵字),因此當(dāng)需要的時候,就可以通過 服務(wù)名稱來查找相應(yīng)的服務(wù)對象了。

Step5: 調(diào)用 EchoServiceImpl 中的 Echo() 方法

查找到EchoService服務(wù)對象之后,就可以調(diào)用其中的 Echo() 這個方法了,但 不是直接調(diào)用,而是用一個中間函數(shù)CallMethod來進行過渡。

  1. // 查找到 EchoService 對象 
  2. service->CallMethod(...) 

在 echo_servcie.pb.cc 中,這個 CallMethod() 方法的實現(xiàn)為:

  1. void EchoService::CallMethod(...) 
  2.     switch(method->index()) 
  3.     { 
  4.         case 0:  
  5.             Echo(...); 
  6.             break; 
  7.              
  8.         case 1: 
  9.             Add(...); 
  10.             break; 
  11.     } 

可以看到:protobuf 是利用固定(寫死)的 索引,來定位一個 Service 服務(wù)中所有的 method 的,也就是說 順序很重要!

Step6: 調(diào)用 EchoServiceImpl 中的 Echo 方法

EchoServiceImpl 類繼承自 EchoService,并實現(xiàn)了其中的虛函數(shù) Echo 和 Add,因此 Step5 中在調(diào)用 Echo 方法時,根據(jù) C++ 的多態(tài),就進入了業(yè)務(wù)層中實現(xiàn)的 Echo 方法。

  • 再補充另一個知識點:我們這里的示例代碼中,客戶端是預(yù)先知道服務(wù)端的 IP 地址和端口號的,所以就直接建立到服務(wù)器的 TCP 連接了。在一些分步式應(yīng)用場景中,可能會有一個服務(wù)發(fā)現(xiàn)流程。也就是說:每一個服務(wù)都注冊到“服務(wù)發(fā)現(xiàn)服務(wù)器”上,然后客戶端在調(diào)用遠程服務(wù)的之前,并不知道服務(wù)提供者在什么位置??蛻舳耸紫鹊椒?wù)發(fā)現(xiàn)服務(wù)器中查詢,拿到了某個服務(wù)提供者的網(wǎng)絡(luò)地址之后,再向該服務(wù)提供者發(fā)送遠程調(diào)用請求。

當(dāng)查找到 EchoServcie 服務(wù)對象之后,就可以調(diào)用其中的指定方法了。

5. 服務(wù)端發(fā)送響應(yīng)數(shù)據(jù)

這部分主要描述下圖中 綠色部分的內(nèi)容:

Step7: 業(yè)務(wù)層處理完畢,回調(diào) RpcChannelServer 中的回調(diào)對象

在上面的 Step4 中,我們通過原型模式構(gòu)造了 2 個對象:請求對象(echoRequest)和響應(yīng)對象(echoResponse),代碼重貼一下:

  1. // 構(gòu)造 request & response 對象 
  2. auto *echoRequest = service->GetRequestPrototype(methodDesc).New(); 
  3. auto *echoResponse = service->GetResponsePrototype(methodDesc).New(); 

構(gòu)造 echoRequest 對象比較好理解,因為我們要從 TCP 二進制數(shù)據(jù)中反序列化,得到 Echo 方法的請求參數(shù)。

那么 echoResponse 這個對象為什么需要構(gòu)造出來?這個對象的目的肯定是為了存放處理結(jié)果。

在 Step5 中,調(diào)用 service->CallMethod(...) 的時候,傳遞參數(shù)如下:

  1. service->CallMethod([參數(shù)1:先不管], [參數(shù)2:先不管], echoRequest, echoResponse, respDone); 
  2.  
  3. // this position 

按照一般的函數(shù)調(diào)用流程,在CallMethod中調(diào)用 Echo() 函數(shù),業(yè)務(wù)層處理完之后,會回到上面 this position 這個位置。然后再把 echoResponse 響應(yīng)數(shù)據(jù)序列化,最后通過 TCP 發(fā)送出去。

但是 protobuf 的設(shè)計并不是如此,這里利用了 C++ 中的閉包的可調(diào)用特性,構(gòu)造了 respDone 這個變量,這個變量會一直作為參數(shù)傳遞到業(yè)務(wù)層的 Echo() 方法中。

這個respDone對象是這樣創(chuàng)建出來的:

  1. auto respDone = google::protobuf::NewCallback(this,  
  2.   &RpcChannelServer::onResponseDoneCB, echoResponse);   

這里的 NewCallback,是由 protobuf 提供的,在 protobuf 源碼中,有這么一段:

  1. template <typename Class, typename Arg1> 
  2. inline Closure* NewPermanentCallback(Class* object,  
  3.                 void (Class::*method)(Arg1), 
  4.                 Arg1 arg1) { 
  5.   return new internal::MethodClosure1<Class, Arg1>(object, method, false, arg1); 
  6.  
  7.  
  8. // 只貼出關(guān)鍵代碼 
  9. class MethodClosure1 : public Closure 
  10.     void Run() override  
  11.     {  
  12.         (object_->*method_)(arg1_); 
  13.     } 

因此,通過 NewCallBack 這個模板方法,就可以創(chuàng)建一個可調(diào)用對象 respDone,并且這個對象中保存了傳入的參數(shù):一個函數(shù),這個函數(shù)接收的參數(shù)。

當(dāng)在以后某個時候,調(diào)用 respDone 這個對象的 Run 方法時,這個方法就會調(diào)用它保存的那個函數(shù),并且傳入保存的參數(shù)。

有了這部分知識,再來看一下業(yè)務(wù)層的 Echo() 代碼:

  1. void EchoServiceImpl::Echo(protobuf::RpcController* controller, 
  2.                    EchoRequest* request, 
  3.                    EchoResponse* response, 
  4.                    protobuf::Closure* done) 
  5.     response->set_message(request->message() + ", welcome!"); 
  6.     done->Run(); 

可以看到,在 Echo() 方法處理完畢之后,只調(diào)用了 done->Run() 方法,這個方法會調(diào)用之前作為參數(shù)注冊進去的 RpcChannelServer::onResponseDoneCB 方法,并且把響應(yīng)對象echoResponse作為參數(shù)傳遞進去。

這這里就比較好理解了,可以預(yù)見到:RpcChannelServer::onResponseDoneCB 方法中一定是進行了 2 個操作:

  • 反序列化數(shù)據(jù);
  • 發(fā)送 TCP 數(shù)據(jù);

Step8: 序列化得到二進制字節(jié)碼,發(fā)送 TCP 數(shù)據(jù)

首先,構(gòu)造 RPC 元數(shù)據(jù),把響應(yīng)對象序列化之后,設(shè)置到 response 字段。

  1. void RpcChannelImpl::onResponseDoneCB(Message *response) 
  2.     // 構(gòu)造外層的 RPC 元數(shù)據(jù) 
  3.     RpcMessage rpcMsg; 
  4.     rpcMsg.set_type(RPC_TYPE_RESPONSE); 
  5.     rpcMsg.set_id([消息 Id]]); 
  6.     rpcMsg.set_error(RPC_ERR_SUCCESS); 
  7.      
  8.     // 把響應(yīng)對象序列化,設(shè)置到 response 字段。 
  9.     rpcMsg.set_response(response->SerializeAsString()); 

然后,序列化數(shù)據(jù),通過 libevent 發(fā)送 TCP 數(shù)據(jù)。

  1. std::string message_str; 
  2. rpcMsg.SerializeToString(&message_str); 
  3. bufferevent_write(m_evBufferEvent, message_str.c_str(), message_str.size()); 

 6. 客戶端接收響應(yīng)數(shù)據(jù)

這部分主要描述下圖中綠色部分的內(nèi)容:

Step9: 反序列化接收到的 TCP 數(shù)據(jù)

RpcChannelClient 是負責(zé)客戶端的網(wǎng)絡(luò)通信,因此當(dāng)它接收到 TCP 數(shù)據(jù)之后,首先進行第一次反序列化,構(gòu)造出 RpcMessage 變量,其中的 response 字段就存放著服務(wù)端的函數(shù)處理結(jié)果,只不過此時它是二進制數(shù)據(jù)。

  1. RpcMessage rpcMsg; 
  2. rpcMsg.ParseFromString(tcpData); 
  3.  
  4. // 此時,rpcMsg.reponse 中存儲的就是 Echo() 函數(shù)處理結(jié)果的二進制數(shù)據(jù)。 

Step10: 調(diào)用業(yè)務(wù)層客戶端的函數(shù)來處理 RPC 結(jié)果

那么應(yīng)該把這個二進制響應(yīng)數(shù)據(jù)序列化到哪一個 response 對象上呢?

在前面的主題【客戶端發(fā)送請求數(shù)據(jù)】,也就是 Step1 中,業(yè)務(wù)層客戶端在調(diào)用 serviceStub->Echo(...) 方法的時候,我沒有列出傳遞的參數(shù),這里把它補全:

  1. // 定義請求對象 
  2. EchoRequest request; 
  3. request.set_message("hello, I am client"); 
  4.  
  5. // 定義響應(yīng)對象 
  6. EchoResponse *response = new EchoResponse; 
  7.  
  8.  
  9. auto doneClosure = protobuf::NewCallback( 
  10.         &doneEchoResponseCB,  
  11.         response); 
  12.  
  13. // 第一個參數(shù)先不用關(guān)心 
  14. serviceStub->Echo(rpcController, &request, response, doneClosure); 

可以看到,這里同樣利用了 protobuf 提供的 NewCallback 模板方法,來創(chuàng)建一個可調(diào)用對象(閉包doneClosure),并且讓這個閉包保存了 2 個參數(shù):一個回調(diào)函數(shù)(doneEchoResponseCB)和 response 對象(應(yīng)該說是指針更準確)。

當(dāng)回調(diào)函數(shù) doneEchoResponseCB 被調(diào)用的時候,會自動把 response 對象作為參數(shù)傳遞進去。

這個可調(diào)用對象(doneClosure閉包) 和 response 對象,被作為參數(shù) 一路傳遞到 EchoService_Stub --> RpcChannelClient,如下圖所示:

因此當(dāng) RpcChannelClient 接收到 RPC 遠程調(diào)用結(jié)果時,就把二進制的 TCP 數(shù)據(jù),反序列化到 response 對象上,然后再調(diào)用 doneClosure->Run() 方法,Run() 方法中執(zhí)行 (object_->*method_)(arg1_),就調(diào)用了業(yè)務(wù)層中的回調(diào)函數(shù),也把參數(shù)傳遞進去了。

業(yè)務(wù)層的回調(diào)函數(shù) doneEchoResponseCB() 函數(shù)的代碼如下:

  1. void doneEchoResponseCB(EchoResponse *response) 
  2.     cout << "response.message = " << response->message() << endl; 
  3.     delete response; 

至此,整個 RPC 調(diào)用流程結(jié)束。

七、總結(jié)

1. protobuf 的核心

通過以上的分析,可以看出 protobuf 主要是為我們解決了序列化和反序列化的問題。

然后又通過 RpcChannel 這個類,來完成業(yè)務(wù)層的用戶代碼與 protobuf 代碼的整合問題。

利用這兩個神器,我們來實現(xiàn)自己的 RPC 框架,思路就非常的清晰了。

2. 未解決的問題

這篇文章僅僅是分析了利用 protobuf 工具,來實現(xiàn)一個 RPC 遠程調(diào)用框架中的幾個關(guān)鍵的類,以及函數(shù)的調(diào)用順序。

按照文中的描述,可以實現(xiàn)出一個滿足基本功能的 RPC 框架,但是還不足以在產(chǎn)品中使用,因為還有下面幾個問題需要解決:

  1. 同步調(diào)用和異步調(diào)用問題;
  2. 并發(fā)問題(多個客戶端的并發(fā)連接,同一個客戶端的并發(fā)調(diào)用);
  3. 調(diào)用超時控制;

以后有機會的話,再和大家一起繼續(xù)深入的討論這些話題,祝您好運!

責(zé)任編輯:姜華 來源: IOT物聯(lián)網(wǎng)小鎮(zhèn)
相關(guān)推薦

2022-01-07 06:12:08

RPC框架限流

2012-10-10 09:14:50

PHPRPCPHP框架

2024-01-02 12:17:44

Go傳統(tǒng)遠程

2014-08-26 10:05:37

框架開發(fā)AngularJS

2016-10-27 08:39:35

大數(shù)據(jù)設(shè)計定量

2022-05-06 10:11:21

開源游戲桌游

2013-05-22 16:46:58

2021-08-25 23:03:58

區(qū)塊鏈數(shù)據(jù)安全

2023-01-05 08:01:35

SpringRPC框架

2018-07-16 08:29:54

redis集群限流

2023-12-07 07:02:00

大倉權(quán)限設(shè)計

2009-06-01 10:23:31

asp.net mvcasp.net mvc.net mvc框架

2022-03-01 11:38:51

RPC框架后端

2022-09-05 08:12:28

Google二進制Protobuf

2021-08-31 16:15:53

安全運營業(yè)務(wù)現(xiàn)代化網(wǎng)絡(luò)安全

2017-07-17 06:46:06

2013-10-30 09:37:19

LinuxLinux命令

2024-11-14 09:40:06

RPC框架NettyJava

2014-06-25 09:14:23

2023-03-26 23:53:49

TLSgRPC通信
點贊
收藏

51CTO技術(shù)棧公眾號