C++极简版的RPC框架

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/li123128/article/details/102516185

  前言
  
  RPC采用客户机/服务器模式实现两个进程之间的相互通信,socket是RPC经常采用的通信手段之一。当然,除了socket,RPC还有其他的通信方法:http、管道。。。网络开源的RPC框架也比较多,一个功能比较完善的RPC框架代码比较多,如何快速的从这些代码盲海中梳理清楚主要脉络,对于初学者来说比较困难,本文介绍之前自己实现的一个C++极简版的RPC框架,代码只有100多行,希望尽量用少的代码来描述框架以减轻初学者的学习负担,同时便于大家阅读网络上复杂的RPC源码。
  
  1、经典的RPC框架echo例子里面,EchoServer_Stub类是哪里来的?
  
  2、为什么stub.Echo(&controller, &request, &response, nullptr); 调用就执行到server端的Echo函数?
  
  3、stub.Echo(&controller, &request, &response, nullptr); 最后一个参数是nullptr,调用到server端的Echo(controller, request, response, done) 函数时,done指针为什么不为空了?
  
  …
  
  让我们通过下面这个简单的RPC框架,一层一层解开上面的疑惑。
  
  echo_server.cc
  
  复制代码
  
  class EchoServerImpl : public goya::rpc::echo::EchoServer {
  
  public:
  
  EchoServerImpl() {}
  
  virtual ~EchoServerImpl() {}
  
  private:
  
  virtual void Echo(google::protobuf::RpcController* controller,
  
  const goya::rpc::echo::EchoRequest* request,
  
  goya::rpc::echo::EchoResponse* response,
  
  google::protobuf::Closure* done)
  
  {
  
  std::cout << "server received client msg: " << request->message() << std::endl;
  
  response->set_message(
  
  "server say: received msg: ***" + request->message() + std::string("***"));
  
  done->Run();
  
  }
  
  };
  
  int main(int argc, char* argv[])
  
  {
  
  RpcServer rpc_server;
  
  goya::rpc::echo::EchoServer* echo_service = new EchoServerImpl();
  
  if (!rpc_server.RegisterService(echo_service, false)) {
  
  std::cout << "register service failed" << std::endl;
  
  return -1;
  
  }
  
  std::string server_addr("0.0.0.0:12321");
  
  if (!rpc_server.Start(server_addr)) {
  
  std::cout << "start server failed" << std::endl;
  
  return -1;
  
  }
  
  return 0;
  
  }
  
  复制代码
  
  echo_client.cc
  
  复制代码
  
  int main(int argc, char* argv[])
  
  SmallHouseColleague smallHouseColleague www.feihongyul.cn= new SmallHouseColleague(www.tianyueylezc.cn);
  
  TwoHouseColleague twoHouseColleague = new TwoHouseColleague(www.xingxinyulzc.cn);
  
  ThreeHouseColleague threeHouseColleague www.haojuptzc.cn= new ThreeHouseColleague(www.haojuylpt.com );
  
  mediator.SetSmallHouse(www.shicaiyulezc.cn smallHouseColleague);
  
  mediator.SetTwoHouse(twoHouseColleague);
  
  mediator.SetThreeHouse(threeHouseColleague);
  
  echo::EchoRequest   request;
  
  echo::EchoResponse  response;
  
  request.set_message("hello tonull, from client");
  
  char* ip          = argv[1];
  
  char* port        = argv[2];
  
  std::string addr  = std::string(ip) + ":" + std::string(port);
  
  RpcChannel    rpc_channel(addr);
  
  echo::EchoServer_Stub stub(&rpc_channel);
  
  RpcController controller;
  
  stub.Echo(&controller, &request, &response, nullptr);
  
  if (controller.Failed())
  
  std::cout << "request failed: %s" << controller.ErrorText().c_str();
  
  else
  
  std::cout << "resp: " << response.message() << std::endl;
  
  return 0;
  
  }
  
  复制代码
  
  上面是一个简单的Echo实例的代码,主要功能是:server端收到client发送来的消息,然后echo返回给client,功能非常简单,但是走完了整个流程。其他特性无非基于此的一些衍生。好了,我们现在来解析下这个源码,首先来看server端。
  
  RpcServer rpc_server;
  
  goya::rpc::echo::EchoServer* echo_service = new EchoServerImpl();
  
  rpc_server.RegisterService(echo_service, false)
  
  rpc_server.Start(server_addr)
  
  最主要就上面四行代码,定义了两个对象rpc_server和echo_service,然后注册对象,启动服务。EchoServerImpl继承于EchoServer,讲到这里也许有人会问,我没有定义EchoServer这个类啊,它是从哪里来的?ok,那我们这里先跳到讲解下protobuf,讲完之后再回过头来继续。
  
  protobuf
  
  通过socket,client和server可以互相交互消息,但这种通信效率不高,一般选择在发送的时候把消息经过序列化,而在接受的时候采用反序列化解析就可以了,本文采用谷歌开源的protobuf作为消息序列化的方法,其他序列化的方法还有json和rlp。。。
  
  首先按照proto格式,定义消息传输的内容, EchoRequest为请求消息,EchoRequest为响应消息,在EchoServer里面定义了Echo方法。
  
  复制代码
  
  syntax = "proto3";
  
  package goya.rpc.echo;
  
  option cc_generic_services = true;
  
  message EchoRequest {
  
  string message = 1;
  
  }
  
  message EchoResponse {
  
  string message = 1;
  
  }
  
  service EchoServer {
  
  rpc Echo(EchoRequest) returns(EchoResponse);
  
  }
  
  复制代码
  
  把定义的proto文件用protoc工具生成对应的echo_service.pb.h和 echo_service.pb.cc文件,网上有很多介绍怎么使用proto文件生成对应的pb.h和pb.c的文档,这里就不在过多描述。具体的也可以看工程里面的 sample/echo/CMakeLists.txt 文件。
  
  service EchoService这一句会生成EchoService和EchoService_Stub两个类,分别是 server 端和 client 端需要关心的。
  
  回到server
  
  对 server 端,通过EchoService::Echo来处理请求,代码未实现,需要子类来 override。
  
  复制代码
  
  void EchoService::Echo(::google::protobuf::RpcController* controller,
  
  const ::echo::EchoRequest*,
  
  ::echo::EchoResponse*,
  
  ::google::protobuf::Closure* done) {
  
  // 代码未实现,需要server返回给client什么内容,就在这里填写
  
  controller->SetFailed("Method Echo() not implemented.");
  
  done->Run();
  
  }
  
  复制代码
  
  好了,我们现在回到上面没有讲完的server,server定义了EchoServerImpl对象,实现了Echo方法,功能也就是把client发送来的消息又返回给client。 server里面还没讲解完的是“注册”和“启动”服务两个功能,我们直接跳到代码讲解。
  
  RegisterService注册的功能非常简单,就是把我们自己定义的EchoServerImpl对象echo_service给保存在services_这个数据结构里。
  
  bool RpcServerImpl::RegisterService(google::protobuf::Service* service, bool ownership) {
  
  services_[0] = service;
  
  return true;
  
  }
  
  Start启动服务的功能也很简单,就是一个socket不断的accept远端传送过来的数据,然后进行处理。
  
  复制代码
  
  bool RpcServerImpl::Start(std::string& server_addr) {
  
  ...
  
  while (true) {
  
  auto socket = boost::make_shared<boost::asio::ip::tcp::socket>(io);
  
  acceptor.accept(*socket);
  
  std::cout << "recv from client: " << socket->remote_endpoint().address() << std::endl;
  
  int request_data_len = 256;
  
  std::vector<char> contents(request_data_len, 0);
  
  socket->receive(boost::asio::buffer(contents));
  
  ProcRpcData(std::string(&contents[0], contents.size()), socket);
  
  }
  
  }
  
  复制代码
  
  回到client
  
  RpcChannel    rpc_channel(addr);
  
  echo::EchoServer_Stub stub(&rpc_channel);
  
  RpcController controller;
  
  stub.Echo(&controller, &request, &response, nullptr);
  
  对于client 端,最主要就上面四条语句,定义了RpcChannel、EchoServer_Stub、RpcController三个不同的对象,通过EchoService_Stub来发送数据,EchoService_Stub::Echo调用了::google::protobuf::Channel::CallMethod方法,但是Channel是一个纯虚类,需要 RPC 框架在子类里实现需要的功能。
  
  复制代码
  
  class EchoService_Stub : public EchoService {
  
  ...
  
  void Echo(::google::protobuf::RpcController* controller,
  
  const ::echo::EchoRequest* request,
  
  ::echo::EchoResponse* response,
  
  ::google::protobuf::Closure* done);
  
  private:
  
  ::google::protobuf::RpcChannel* channel_;
  
  };
  
  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);
  
  }
  
  复制代码
  
  也就是说,执行stub.Echo(&controller, &request, &response, nullptr); 这条语句实际是执行到了
  
  复制代码
  
  void RpcChannelImpl::CallMethod(const ::google::protobuf::MethodDescriptor* method,
  
  ::google::protobuf::RpcController* controller,
  
  const ::google::protobuf::Message* request,
  
  ::google::protobuf::Message* response,
  
  ::google::protobuf::Closure* done) {
  
  std::string request_data = request->SerializeAsString();
  
  socket_->send(boost::asio::buffer(request_data));
  
  int  resp_data_len = 256;
  
  std::vector<char> resp_data(resp_data_len, 0);
  
  socket_->receive(boost::asio::buffer(resp_data));
  
  response->ParseFromString(std::string(&resp_data[0], resp_data.size()));
  
  }
  
  复制代码
  
  RpcChannelImpl::CallMethod主要做了什么呢?主要两件事情:1、把request消息通过socket发送给远端;2、同时接受来自远端的reponse消息。
  
  讲到这里基本流程就梳理的差不多了,文章开头的几个问题也基本在讲解的过程中回答了,对于后面两个问题,这里再划重点讲解下,stub.Echo(&controller, &request, &response, nullptr); 最后一个参数是nullptr,这里你填啥都没啥卵用,因为在RpcChannelImpl::CallMethod中根本就没使用到,而为什么又要加这个参数呢?这纯属是为了给人一种错觉:client端执行stub.Echo(&controller, &request, &response, nullptr);就是调用到了server端的EchoServerImpl::Echo(*controller, *request, *response, *done),使远程调用看起来像本地调用一样(至少参数类型及个数是一致的)。而其实这也是最令初学者疑惑的地方。
  
  而本质上,server端的EchoServerImpl::Echo(*controller, *request, *response, *done)函数其实是在接受到数据后,从这里调用过来的,具体见下面代码:
  
  复制代码
  
  void RpcServerImpl::ProcRpcData(const std::string& serialzied_data,
  
  const boost::shared_ptr<boost::asio::ip::tcp::socket>& socket) {
  
  auto service      = services_[0];
  
  auto m_descriptor = service->GetDescriptor()->method(0);
  
  auto recv_msg = service->GetRequestPrototype(m_descriptor).New();
  
  auto resp_msg = service->GetResponsePrototype(m_descriptor).New();
  
  recv_msg->ParseFromString(serialzied_data);
  
  // 构建NewCallback对象
  
  auto done = google::protobuf::NewCallback(
  
  this, &RpcServerImpl::OnCallbackDone, resp_msg, socket);
  
  RpcController controller;
  
  service->CallMethod(m_descriptor, &controller, recv_msg, resp_msg, done);
  
  }
  
  复制代码
  
  service->CallMethod(m_descriptor, &controller, recv_msg, resp_msg, done); 会调用到EchoServer::CallMethod,protobuf会根据method->index()找到对应的执行函数,EchoServerImpl实现了Echo函数,所以上面的service->CallMethod(m_descriptor, &controller, recv_msg, resp_msg, done); 会执行到EchoServerImpl::Echo,这进一步说明了 EchoServerImpl::Echo 跟stub.Echo()调用没有鸡毛关系,唯一有的关系,确实发起动作是stub.Echo(); 中间经过了无数次解析最后确实是调到了EchoServerImpl::Echo。
  
  复制代码
  
  void EchoServer::CallMethod(const ::PROTOBUF_NAMESPACE_ID::MethodDescriptor* method,
  
  ::PROTOBUF_NAMESPACE_ID::RpcController* controller,
  
  const ::PROTOBUF_NAMESPACE_ID::Message* request,
  
  ::PROTOBUF_NAMESPACE_ID::Message* response,
  
  ::google::protobuf::Closure* done) {
  
  GOOGLE_DCHECK_EQ(method->service(), file_level_service_descriptors_echo_5fservice_2eproto[0]);
  
  switch(method->index()) {
  
  case 0:
  
  Echo(controller,
  
  ::PROTOBUF_NAMESPACE_ID::internal::DownCast<const ::goya::rpc::echo::EchoRequest*>(
  
  request),
  
  ::PROTOBUF_NAMESPACE_ID::internal::DownCast<::goya::rpc::echo::EchoResponse*>(
  
  response),
  
  done);
  
  break;
  
  default:
  
  GOOGLE_LOG(FATAL) << "Bad method index; this should never happen.";

猜你喜欢

转载自blog.csdn.net/li123128/article/details/102516185