這篇文章將講述如何使用google的protobuf庫實現一個RPC service,就實現一個最簡單的service吧:echo. 文章對應的代碼均可以在eventrpc中找到,寫下這篇文章時的svn revision是138.linux
[cpp] view plain copy算法
package echo; message EchoRequest { required string message = 1; }; message EchoResponse { required string response = 1; }; service EchoService { rpc Echo(EchoRequest) returns (EchoResponse); };
解釋一下這個proto文件中作的事情,它定義了一個package: echo, 這個package中有service:EchoService,而這個service下只有一個服務:Echo, 它的請求由EchoRequest結構體定義,回覆由EchoResponse定義. package至關因而C++中namespace的概念,有些package中可能會提供相同名字的service,爲了解決命名衝突,就引入了package這個概念.安全
所生成的C++文件,都會在namespace echo中,就是前面提到的package概念.對於service EchoService而言,會對應的生成兩個類:EchoService類和EchoService_Stub類:服務器
[cpp] view plain copy網絡
class EchoService : public ::google::protobuf::Service { // .... EchoService_Stub(::google::protobuf::RpcChannel* channel); virtual void Echo(::google::protobuf::RpcController* controller, const ::echo::EchoRequest* request, ::echo::EchoResponse* response, ::google::protobuf::Closure* done); void CallMethod(const ::google::protobuf::MethodDescriptor* method, ::google::protobuf::RpcController* controller, const ::google::protobuf::Message* request, ::google::protobuf::Message* response, ::google::protobuf::Closure* done); }; class EchoService_Stub : public EchoService { //... void Echo(::google::protobuf::RpcController* controller, const ::echo::EchoRequest* request, ::echo::EchoResponse* response, ::google::protobuf::Closure* done); };
上面省略了一些細節,只把最關鍵的部分提取出來了. 這兩部分如何使用,後面會繼續講解app
[cpp] view plain copy異步
Dispatcher dispatcher; RpcChannel *channel = new RpcChannel("127.0.0.1", 21118, &dispatcher); if (!channel->Connect()) { printf("connect to server failed, abort\n"); exit(-1); } echo::EchoService::Stub stub(channel); echo::EchoRequest request; echo::EchoResponse response; request.set_message("hello"); stub.Echo(NULL, &request, &response, gpb::NewCallback(::echo_done, &response, channel));
能夠看到,stub類的構造函數須要一個::google::protobuf::RpcChannel指針,這個類須要咱們來實現,後面繼續說.而後就是根據協議填充請求字段,註冊回調函數,這以後就能夠調用stub類提供的Echo函數發送請求了.svn
[cpp] view plain copy函數
void EchoService_Stub::Echo(::google::protobuf::RpcController* controller, const ::echo::EchoRequest* request, ::echo::EchoResponse* response, ::google::protobuf::Closure* done) { channel_->CallMethod(descriptor()->method(0), controller, request, response, done); }
能夠看到,發送請求的背後,最後調用的實際上是RpcChannel的CallMethod函數.因此,要實現RpcChannel類,最關鍵的就是要實現這個函數,在這個函數中完成發送請求的事務.具體能夠看rpc_channel.cpp中的作法,再也不闡述,由於這裏面作的事情,和通常的網絡客戶端作的事情差很少.測試
如何識別service 前面提到過,每一個service的請求包和回覆包都是protobuf中的message結構體,在這個例子中是EchoRequest和EchoResponse message.但是,它們僅僅是包體,也就是說,即便你發送了這些消息,在服務器端還須要一個包頭來識別究竟是哪一個請求的包體. 因而在代碼中,引入了一個類Meta,其中有兩個關鍵的變量:包體長度和method id. 包體長度自沒必要說,就是緊跟着包頭的包體數據的長度. method id是用來標識哪個service的,若是不用id數字,也可使用字符串,每一個service,都有一個full name的概念,以這裏的例子而言,Echo服務的full name是echo::EchoService::Echo(再次的,又是C++中namespace的概念來表示」全路徑」以免命名衝突).可是,若是使用full name來區分,一來發送包頭就會過大,而來查找service時是一個字符串比較操做的過程,耗時間. 因此引入了method id的概念,選擇hash full name爲一個id值,通常而言,一個服務器對外提供的service,撐死有幾百個吧,而選用的id是整型數據,另外再選擇足夠好的hash算法,絕大多數狀況下是不會出現衝突的. 以上就是Meta類作的事情,封裝了包體和識別service的method id,一併做爲包頭和包體拼接發送給服務器端.
實現服務器端 接收到客戶端的請求以後,首先要作一些安全性的檢查,好比method id對應的service是否有註冊. 其次就是真正的處理過程了:
[cpp] view plain copy
int RpcMethodManager::HandleService(string *message, Meta *meta, Callback *callback) { RpcMethod *rpc_method = rpc_methods_[meta->method_id()]; const gpb::MethodDescriptor *method = rpc_method->method_; gpb::Message *request = rpc_method->request_->New(); gpb::Message *response = rpc_method->response_->New(); request->ParseFromString(*message); HandleServiceEntry *entry = new HandleServiceEntry(method, request, response, message, meta, callback); gpb::Closure *done = gpb::NewCallback( &HandleServiceDone, entry); rpc_method->service_->CallMethod(method, NULL, request, response, done); return 0; }
上面註冊了一個名爲HandleServiceDone的回調函數,當service的Echo處理完畢以後,自動就會調用這個回調函數 來看 EchoService::CallMethod的定義
[cpp] view plain copy
void EchoService::CallMethod(const ::google::protobuf::MethodDescriptor* method, ::google::protobuf::RpcController* controller, const ::google::protobuf::Message* request, ::google::protobuf::Message* response, ::google::protobuf::Closure* done) { GOOGLE_DCHECK_EQ(method->service(), EchoService_descriptor_); switch(method->index()) { case 0: Echo(controller, ::google::protobuf::down_cast<CONST ::echo::EchoRequest*>(request), ::google::protobuf::down_cast< ::echo::EchoResponse*>(response), done); break; default: GOOGLE_LOG(FATAL) << "Bad method index; this should never happen."; break; } }
能夠看到, 這個Echo服務是須要註冊的服務器端首先實現的,以echo_server.cpp中的代碼爲例,它是這樣作的:
[cpp] view plain copy
class EchoServiceImpl : public echo::EchoService { public: EchoServiceImpl() { }; virtual void Echo(::google::protobuf::RpcController* controller, const ::echo::EchoRequest* request, ::echo::EchoResponse* response, ::google::protobuf::Closure* done) { printf ("request: %s\n", request->message().c_str()); response->set_response(request->message()); if (done) { done->Run(); } } };
它作的事情就是把收到的請求打印出來,而後將請求消息做爲回覆消息傳送回去.調用done->Run()函數,其實就是調用前面註冊的回調函數HandleServiceDone函數,這時候表示服務器端已經準備好了給客戶端響應的消息,後面就是網絡傳輸層的事情了.
以上是使用google protobuf RPC實現一個service的全過程.protobuf官方並無給出這樣一個demo的例子,因此我在eventrpc項目中試圖封裝protobuf來作RPC service. 可是,當前的實現還不夠完善,存在如下的問題:
不過,就以上而言,若是想了解如何使用protobuf來實現RPC,已經足夠說明原理了,能夠對應着代碼和官方文檔看看每一個類的含義. 要編譯成功,須要protobuf庫和phread庫.以前曾經使用libevent,可是不喜歡這個東東,因而就本身作了,可是目前僅支持epoll而已,因此還只能在linux上面編譯.