日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當(dāng)前位置:首頁(yè) > 嵌入式 > 嵌入式客棧
[導(dǎo)讀]一、前言二、RPC基礎(chǔ)概念三、protobuf基本使用四、libevent五、實(shí)現(xiàn)RPC框架1.基本框架構(gòu)思2.元數(shù)據(jù)的設(shè)計(jì)3.分析:客戶端發(fā)送請(qǐng)求4.分析:服務(wù)端接收請(qǐng)求5.分析:服務(wù)端發(fā)送響應(yīng)6.分析:客戶端接收響應(yīng)六、總結(jié)1.protobuf的核心2.未解決的問(wèn)題Warni...


  • 一、前言

  • 二、RPC 基礎(chǔ)概念

  • 三、protobuf 基本使用

  • 四、libevent

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

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

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

    • 3. 分析:客戶端發(fā)送請(qǐng)求

    • 4. 分析:服務(wù)端接收請(qǐng)求

    • 5. 分析:服務(wù)端發(fā)送響應(yīng)

    • 6. 分析:客戶端接收響應(yīng)

  • 六、總結(jié)

    • 1. protobuf 的核心

    • 2. 未解決的問(wèn)題


Warning: 文章有點(diǎn)長(zhǎng),我主要是想在一篇文章中把相關(guān)的重點(diǎn)內(nèi)容都講完、講透徹,請(qǐng)見(jiàn)諒。

以后,我盡可能不寫(xiě)這么長(zhǎng)的文章。

一、前言

在嵌入式系統(tǒng)中,很少需要使用到 RPC (Remote Procedure Call)遠(yuǎn)程方法調(diào)用,因?yàn)樵诖蟛糠智闆r下,實(shí)現(xiàn)一個(gè)產(chǎn)品功能的所有進(jìn)程、線程都是運(yùn)行在同一個(gè)硬件設(shè)備中的。

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

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

因此,利用 RPC 來(lái)利用遠(yuǎn)程提供的服務(wù),相對(duì)于其他的機(jī)制來(lái)說(shuō),有更多的優(yōu)勢(shì)。

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

序列化[1]:將結(jié)構(gòu)數(shù)據(jù)或?qū)ο筠D(zhuǎn)換成能夠被存儲(chǔ)和傳輸(例如網(wǎng)絡(luò)傳輸)的格式,同時(shí)應(yīng)當(dāng)要保證這個(gè)序列化結(jié)果在之后(可能在另一個(gè)計(jì)算環(huán)境中)能夠被重建回原來(lái)的結(jié)構(gòu)數(shù)據(jù)或?qū)ο蟆?/p>
我會(huì)以 protobuf 中的一些關(guān)鍵 C 類(lèi)作為突破口,來(lái)描述從客戶端發(fā)起調(diào)用,到服務(wù)端響應(yīng),這個(gè)完整執(zhí)行序列。也就是下面這張圖:

這張圖大概畫(huà)了 2 個(gè)小時(shí)(邊看代碼,邊畫(huà)圖),我已經(jīng)盡力了,雖然看起來(lái)有點(diǎn)亂。

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

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

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

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

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

二、RPC 基礎(chǔ)概念

1. RPC 是什么?

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

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

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

int getMotionPath(float *input, int intputLen, float *output, int outputLen)
如果計(jì)算過(guò)程不復(fù)雜,可以把這個(gè)算法函數(shù)和應(yīng)用程序放在本地的同一個(gè)進(jìn)程中,以源代碼或庫(kù)的方式提供計(jì)算服務(wù),如下圖:

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

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

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

初步接觸 RPC 的朋友可能會(huì)提出:

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

這句話說(shuō)的非常對(duì),從功能上來(lái)說(shuō),這個(gè)描述過(guò)程就是 RPC 所需要做的所有事情。

不過(guò),在這個(gè)過(guò)程中,有很多問(wèn)題需要我們來(lái)手動(dòng)解決:

  1. 如何處理通信問(wèn)題?TCP or UDP or HTTP?或者利用其他的一些已有的網(wǎng)絡(luò)協(xié)議?

  2. 如何把數(shù)據(jù)進(jìn)行打包?服務(wù)端接收到打包的數(shù)據(jù)之后,如何還原數(shù)據(jù)?

  3. 對(duì)于特定領(lǐng)域的問(wèn)題,可以專(zhuān)門(mén)寫(xiě)一套實(shí)現(xiàn)來(lái)解決,但是對(duì)于通用的遠(yuǎn)程調(diào)用,怎么做到更靈活、更方便?

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

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

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

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

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

最終的目的,將會(huì)輸出一個(gè) RPC 遠(yuǎn)程調(diào)用框架的庫(kù)文件(動(dòng)態(tài)庫(kù)、靜態(tài)庫(kù))

  1. 服務(wù)器端利用這個(gè)庫(kù),在網(wǎng)絡(luò)上提供函數(shù)調(diào)用服務(wù);

  2. 客戶端利用這個(gè)庫(kù),遠(yuǎn)程調(diào)用位于服務(wù)器上的函數(shù);

2. 需要解決什么問(wèn)題?

既然我們是介紹 RPC 框架,那么需要解決的問(wèn)題就是一個(gè)典型的 RPC 框架所面對(duì)問(wèn)題,如下:

  1. 解決函數(shù)調(diào)用時(shí),數(shù)據(jù)結(jié)構(gòu)的約定問(wèn)題;
  2. 解決數(shù)據(jù)傳輸時(shí),序列化和反序列化問(wèn)題;
  3. 解決網(wǎng)絡(luò)通信問(wèn)題;
這 3 個(gè)問(wèn)題是所有的 RPC 框架都必須解決的,這是最基本的問(wèn)題,其他的考量因素就是:速度更快、成本更低、使用更靈活、易擴(kuò)展、向后兼容、占用更少的系統(tǒng)資源等等。

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

3. 有哪些開(kāi)源實(shí)現(xiàn)?

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

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

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

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

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

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

三、protobuf 基本使用

1. 基本知識(shí)

Protobuf 是 Protocol Buffers 的簡(jiǎn)稱, 它是 Google 開(kāi)發(fā)的一種跨語(yǔ)言、跨平臺(tái)、可擴(kuò)展的用于序列化數(shù)據(jù)協(xié)議,

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

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

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

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

2. 使用步驟

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

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

message EchoRequest {
string message = 1;
}

message EchoResponse {
string message = 1;
}

message AddRequest {
int32 a = 1;
int32 b = 2;
}

message AddResponse {
int32 result = 1;
}

service EchoService {
rpc Echo(EchoRequest) returns(EchoResponse);
rpc Add(AddRequest) returns(AddResponse);
}

最后的 service EchoService,是讓 protoc 生成接口類(lèi),其中包括 2 個(gè)方法 Echo 和 Add

Echo 方法:客戶端調(diào)用這個(gè)方法,請(qǐng)求的“數(shù)據(jù)結(jié)構(gòu)” EchoRequest 中包含一個(gè) string 類(lèi)型,也就是一串字符;服務(wù)端返回的“數(shù)據(jù)結(jié)構(gòu)” EchoResponse 中也是一個(gè) string 字符串;

Add 方法:客戶端調(diào)用這個(gè)方法,請(qǐng)求的“數(shù)據(jù)結(jié)構(gòu)” AddRequest 中包含 2 個(gè)整型數(shù)據(jù),服務(wù)端返回的“數(shù)據(jù)結(jié)構(gòu)” AddResponse 中包含一個(gè)整型數(shù)據(jù)(計(jì)算結(jié)果);

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

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

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

class EchoService : public ::PROTOBUF_NAMESPACE_ID::Service {
virtual void Echo(RpcController* controller,
EchoRequest* request,
EchoResponse* response,
Closure* done);

virtual void Add(RpcController* controller,
AddRequest* request,
AddResponse* response,
Closure* done);

void CallMethod(MethodDescriptor* method,
RpcController* controller,
Message* request,
Message* response,
Closure* done);
}
class EchoService_Stub : public EchoService {
public:
EchoService_Stub(RpcChannel* channel);

void Echo(RpcController* controller,
EchoRequest* request,
EchoResponse* response,
Closure* done);

void Add(RpcController* controller,
AddRequest* request,
AddResponse* response,
Closure* done);

private:
// 成員變量,比較關(guān)鍵
RpcChannel* channel_;
};
Step3:服務(wù)端程序?qū)崿F(xiàn)接口中定義的方法,提供服務(wù);客戶端調(diào)用接口函數(shù),調(diào)用遠(yuǎn)程的服務(wù)。

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

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

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

EchoService 類(lèi)中也給出了這兩個(gè)函數(shù)的默認(rèn)實(shí)現(xiàn),只不過(guò)是提示錯(cuò)誤信息:

void EchoService::Echo() {
controller->SetFailed("Method Echo() not implemented.");
done->Run();
}
void EchoService::Add() {
controller->SetFailed("Method Add() not implemented.");
done->Run();
}
圖中的 EchoServiceImpl 就是我們定義的類(lèi),其中實(shí)現(xiàn)了 Echo 和 Add 這兩個(gè)虛函數(shù):

void EchoServiceImpl::Echo(RpcController* controller,
EchoRequest* request,
EchoResponse* response,
Closure* done)
{
// 獲取請(qǐng)求消息,然后在末尾加上信息:", welcome!",返回給客戶端
response->set_message(request->message() ", welcome!");
done->Run();
}

void EchoServiceImpl::Add(RpcController* controller,
AddRequest* request,
AddResponse* response,
Closure* done)
{
// 獲取請(qǐng)求數(shù)據(jù)中的 2 個(gè)整型數(shù)據(jù)
int32_t a = request->a();
int32_t b = request->b();

// 計(jì)算結(jié)果,然后放入響應(yīng)數(shù)據(jù)中
response->set_result(a b);

done->Run();
}
(2)客戶端:EchoService_Stub

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

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

void EchoService_Stub::Echo(RpcController* controller,
EchoRequest* request,
EchoResponse* response,
Closure* done) {
channel_->CallMethod(descriptor()->method(0),
controller,
request,
response,
done);
}

void EchoService_Stub::Add(RpcController* controller,
AddRequest* request,
AddResponse* response,
Closure* done) {
channel_->CallMethod(descriptor()->method(1),
controller,
request,
response,
done);
}
看到?jīng)],每一個(gè)函數(shù)都調(diào)用了成員變量 channel_ 的 CallMethod 方法(圖中左側(cè)的步驟2),這個(gè)成員變量的類(lèi)型是 google::protobuf:RpcChannel。

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

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

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

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

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

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

我們這里選了libevent這個(gè)網(wǎng)絡(luò)庫(kù)來(lái)實(shí)現(xiàn) TCP 通信。

四、libevent

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

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

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

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

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

1. libevent 簡(jiǎn)介

Libevent 是一個(gè)用 C 語(yǔ)言編寫(xiě)的、輕量級(jí)、高性能、基于事件的網(wǎng)絡(luò)庫(kù)。

主要有以下幾個(gè)亮點(diǎn):

1. 事件驅(qū)動(dòng)( event-driven),高性能;
2. 輕量級(jí),專(zhuān)注于網(wǎng)絡(luò);源代碼相當(dāng)精煉、易讀;
3. 跨平臺(tái),支持 Windows、 Linux、*BSD 和 Mac Os;
4. 支持多種 I/O 多路復(fù)用技術(shù), epoll、 poll、 dev/poll、 select 和 kqueue 等;
5. 支持 I/O,定時(shí)器和信號(hào)等事件;注冊(cè)事件優(yōu)先級(jí)。

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

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

總之一句話:使用很方便,功能很強(qiáng)大!

2. 基本使用

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

struct event_base *m_evBase = event_base_new();
struct bufferevent *m_evBufferEvent = bufferevent_socket_new(
m_evBase, [socket Id],
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_THREADSAFE);
bufferevent_setcb(m_evBufferEvent,
[讀取數(shù)據(jù)回調(diào)函數(shù)],
NULL,
[事件回調(diào)函數(shù)],
[回調(diào)函數(shù)傳參]);

// 開(kāi)始監(jiān)聽(tīng) socket
event_base_dispatch(m_evBase);
有一個(gè)問(wèn)題需要注意:protobuf 序列化之后的數(shù)據(jù),全部是二進(jìn)制的。

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

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

從剛才的第三部分: 自動(dòng)生成的幾個(gè)類(lèi)EchoService, EchoService_Stub中,已經(jīng)能夠大概看到 RPC 框架的端倪了。這里我們?cè)僬显谝黄穑匆幌赂唧w的細(xì)節(jié)部分。

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

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

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

1. EchoService:服務(wù)端接口類(lèi),定義需要實(shí)現(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)用層需要實(shí)現(xiàn)的類(lèi),繼承自 EchoService;
2. ClientApp: 客戶端應(yīng)用程序,調(diào)用 EchoService_Stub 中的方法;

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

echo_servcie.proto 文件中,我們按照 protobuf 的語(yǔ)法規(guī)則,定義了幾個(gè) 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。

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

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

1. 消息 Id 管理;
2. 錯(cuò)誤處理;
3. 同步調(diào)用和異步調(diào)用;
4. 超時(shí)控制;

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

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

// 消息類(lèi)型
enum MessageType
{
RPC_TYPE_UNKNOWN = 0;
RPC_TYPE_REQUEST = 1;
RPC_TYPE_RESPONSE = 2;
RPC_TYPE_ERROR = 3;
}

// 錯(cuò)誤代碼
enum ErrorCode
{
RPC_ERR_OK = 0;
RPC_ERR_NO_SERVICE = 1;
RPC_ERR_NO_METHOD = 2;
RPC_ERR_INVALID_REQUEST = 3;
RPC_ERR_INVALID_RESPONSE = 4
}

message RpcMessage
{
MessageType type = 1; // 消息類(lèi)型
uint64 id = 2; // 消息id
string service = 3; // 服務(wù)名稱
string method = 4; // 方法名稱
ErrorCode error = 5; // 錯(cuò)誤代碼

bytes request = 100; // 請(qǐng)求數(shù)據(jù)
bytes response = 101; // 響應(yīng)數(shù)據(jù)
}

注意: 這里的 request 和 response,它們的類(lèi)型都是 byte

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

首先,構(gòu)造一個(gè) RpcMessage 變量,填入各種元數(shù)據(jù)(type, id, service, method, error);

然后,序列化客戶端傳入的請(qǐng)求對(duì)象(EchoRequest), 得到請(qǐng)求數(shù)據(jù)的字節(jié)碼;

再然后,把請(qǐng)求數(shù)據(jù)的字節(jié)碼插入到 RpcMessage 中的 request 字段;

最后,把 RpcMessage 變量序列化之后,通過(guò) TCP 發(fā)送出去。

如下圖:

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

首先,把接收到的 TCP 數(shù)據(jù)反序列化,得到一個(gè) RpcMessage 變量;

然后,根據(jù)其中的 type 字段,得知這是一個(gè)調(diào)用請(qǐng)求,于是根據(jù) service 和 method 字段,構(gòu)造出兩個(gè)類(lèi)實(shí)例:EchoRequest 和 EchoResponse(利用了 C 中的原型模式);

最后,從 RpcMessage 消息中的 request 字段反序列化,來(lái)填充 EchoRequest 實(shí)例;

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

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

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

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

// ip, port 是服務(wù)端網(wǎng)絡(luò)地址
RpcChannel *rpcChannel = new RpcChannelClient(ip, port);
EchoService_Stub *serviceStub = new EchoService_Stub(rpcChannel);
serviceStub->Echo(...);
上文已經(jīng)說(shuō)過(guò),EchoService_Stub 中的 Echo 方法,會(huì)調(diào)用其成員變量 channel_ 的 CallMethod 方法,因此,需要提前把實(shí)現(xiàn)好的 RpcChannelClient 實(shí)例,作為構(gòu)造函數(shù)的參數(shù),注冊(cè)到 EchoService_Stub 中。

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

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

// 創(chuàng)建 RpcMessage
RpcMessage message;

// 填充元數(shù)據(jù)
message.set_type(RPC_TYPE_REQUEST);
message.set_id(1);
message.set_service("EchoService");
message.set_method("Echo");

// 序列化請(qǐng)求變量,填充 request 字段
// (這里的 request 變量,是客戶端程序傳進(jìn)來(lái)的)
message.set_request(request->SerializeAsString());

// 把 RpcMessage 序列化
std::string message_str;
message.SerializeToString(
本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專(zhuān)欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
關(guān)閉