google的protobuf庫

這篇文章將講述如何使用google的protobuf庫實現一個RPC service,就實現一個最簡單的service吧:echo. 文章對應的代碼均可以在eventrpc中找到,寫下這篇文章時的svn revision是138.linux

  1. 定義協議 首先須要爲這個service定義proto文件, 以下:

[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這個概念.安全

  1. 對應的C++文件 使用protobuf自帶的編譯proto文件編譯器,能夠生成對應的pb.h和pb.cc文件.具體細節能夠參考protobuf關於這部分的參考文檔.

所生成的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

  1. 實現客戶端 首先來看如何實現客戶端. 客戶端都經過上面提到的對應service的stub類來發送請求,以sample/echo_client.cpp中的代碼來解釋:

[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

  1. 實現RpcChannel類 如今能夠講解RpcChannel類和stub類的關係了,看看在調用stub::Echo函數,也就是發送請求時發生了什麼事情:

[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中的作法,再也不闡述,由於這裏面作的事情,和通常的網絡客戶端作的事情差很少.測試

  1. 如何識別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,一併做爲包頭和包體拼接發送給服務器端.

  2. 實現服務器端 接收到客戶端的請求以後,首先要作一些安全性的檢查,好比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. 可是,當前的實現還不夠完善,存在如下的問題:

  1. 效率不高
  2. 沒有實現客戶端能夠選擇異步或者同步方式來響應服務器端的消息
  3. 安全性檢查不夠完善,目前僅適用method id來檢查
  4. 沒有把dispatcher抽出來獨立到一個線程中,只有這樣才能實現2)
  5. 沒有爲每一個函數寫測試用例. .... N) 其餘尚未想到的....等着您給建議

不過,就以上而言,若是想了解如何使用protobuf來實現RPC,已經足夠說明原理了,能夠對應着代碼和官方文檔看看每一個類的含義. 要編譯成功,須要protobuf庫和phread庫.以前曾經使用libevent,可是不喜歡這個東東,因而就本身作了,可是目前僅支持epoll而已,因此還只能在linux上面編譯.

相關文章
相關標籤/搜索