前言

RPC采用客户机/服务器模式实现两个进程之间的相互通信,socket是RPC经常采用的通信手段之一。当然,除了socket,RPC还有其他的通信方法:http、管道。。。网络开源的RPC框架也比较多,一个功能比较完善的RPC框架代码比较多,如何快速的从这些代码盲海中梳理清楚主要脉络,对于初学者来说比较困难,本文介绍之前自己实现的一个C++极简版的RPC框架(https://github.com/goyas/goya-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[])
{ 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();elsestd::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和EchoServer,然后注册对象,启动服务。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 EchoServer这一句会生成EchoServer和EchoServer_Stub两个类,分别是 server 端和 client 端需要关心的。

回到server

对 server 端,通过EchoServer::Echo来处理请求,代码未实现,需要子类来 override。

void EchoServer::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_这个数据结构里。(RpcServer类中有一个RpcServerImpl*的成员,所有操作实际上都是转调该成员的相应函数)

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三个不同的对象,通过EchoServer_Stub来发送数据,EchoServer_Stub::Echo调用了::google::protobuf::Channel::CallMethod方法,但是Channel是一个纯虚类,需要 RPC 框架在子类里实现需要的功能。

// EchoServer、EchoServer_Stub是proto自动生成的类
class EchoServer_Stub : public EchoServer
{...
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()方法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.";break;}
}

一个基于protobuf的极简RPC相关推荐

  1. 从零开始仿写一个抖音App——基于FFmpeg的极简视频播放器

    本文首发于微信公众号--世界上有意思的事,搬运转载请注明出处,否则将追究版权责任.微信号:a1018998632,交流qq群:859640274 1.从零开始仿写一个抖音app--开始 4.从零开始仿 ...

  2. 基于Protobuf的分布式高性能RPC框架——Navi-Pbrpc

    基于Protobuf的分布式高性能RPC框架--Navi-Pbrpc 二月 8, 2016 1 简介 Navi-pbrpc框架是一个高性能的远程调用RPC框架,使用netty4技术提供非阻塞.异步.全 ...

  3. 基于python的个人博客_一款基于 Django 的极简主义个人博客系统

    shadow_blog 介绍 shadow_blog 是一款基于 Django 的极简主义个人博客,已应用在苍茫误此生博客 前端基于 Boundless-UI,风格简约.支持响应式布局, 已适配主流的 ...

  4. 基于SpringBoot+Bootstrap极简运维监控系统

    真正的大师,永远都怀着一颗学徒的心! 一.项目简介 基于SpringBoot+Bootstrap极简运维监控系统 二.实现功能 支持cpu使用率 支持cpu温度 支持内存使用率 支持磁盘容量 支持磁盘 ...

  5. 基于Vue的极简生成器 — Vuepress

    为什么要使用Vuepress VuePress由两部分组成:一个极简的静态站点生成器,带有一个vue支持的主题系统和Plugin API,以及一个为编写技术文档而优化的默认主题.创建它是为了支持Vue ...

  6. cli3解决 ie11语法错误 vue_vue-admin-template基于vue2的极简后台管理系统

    简介 vue-admin-template一款极简的 vue admin 管理后台. 它只包含了 Element UI & axios & iconfont & permiss ...

  7. 以小见大——那些基于 protobuf 的五花八门的 RPC(5 完)

    赖勇浩(http://laiyonghao.com) 快刀斩乱麻,祭上最后两个 rpc 分析,再整上我自己的设计,这个系列就完结了. protobuf-socket-rpc 好,废话不多说,看看这个 ...

  8. 以小见大——那些基于 protobuf 的五花八门的 RPC(2)

    赖勇浩(http://laiyonghao.com ) 多看了三五个 rpc 实现之后,这个事儿就变得很有趣了,今儿来看 fepss-rpc 和 casocklib,分别基于 java 和 C++ 开 ...

  9. 以小见大——那些基于 protobuf 的五花八门的 RPC(4)

    赖勇浩(http://laiyonghao.com ) protobuf-rpc-pro 不知道你还记不记得 protobuf-rpc,这货在后面加了个 pro,就真的重量级了许多.照例先看看简介:A ...

  10. 以小见大——那些基于 protobuf 的五花八门的 RPC(3)

    赖勇浩(http://laiyonghao.com ) protobuf-remote 嘎~再来一枚 C++ 系的 RPC,它的简介是 RPC implementation for C# and C+ ...

最新文章

  1. 电子商务思维导图精品荟萃:电子商务思维导图大全[多图精品收藏]
  2. 【ARM】Tiny4412裸板编程之LED(二)
  3. GitHub 新出的 Actions 是什么? 用他做自动测试?
  4. mysql字段重命名_MySQL中使用SQL语句对字段进行重命名
  5. 强化学习《基于策略价值 - Pathwise Derivative Policy Grident》
  6. 51ak带你看MYSQL5.7源码2:编译现有的代码
  7. ecshop 订单-》订单状态 2
  8. linux java amr转mp3_部署ffmpeg及amr转mp3方法
  9. RAX,eax,ax,ah,al 关系
  10. c语言鼠标游戏代码,自己用C写的一个简单的打地鼠游戏代码出了个问题(鼠标和循环不能...
  11. 方格取数问题(网络流24题之一)
  12. python毕业设计论文-基于Python的网络爬虫的设计与实现.doc
  13. rust货轮什么时候出现_中国最早的汉字出现于什么时候?
  14. 电子邮件的地址格式是怎样的?请说明各部分的意思。
  15. 佛说爱情二:前生500次的回眸才换得今生的一次擦肩而过
  16. numpy学习笔记1—ravel() 和 flatten()
  17. java界面添加按钮_java界面添加按钮
  18. spring boot网上购物系统毕业设计源码311236
  19. 无障碍出行,他让盲人“看得见”
  20. Apache Pulsar和Apache BookKeeper

热门文章

  1. 员工收“老板”QQ消息转账98万 警方挽回被骗资金
  2. QT开发(三十四)——QT多线程编程
  3. linux 下 maven 安装配置
  4. Cognos知识点总结
  5. linux 异步信号的同步处理方式
  6. Office 2007,在编辑Word时,文件经常无法保存会丢失
  7. 2.GitLab 项目管理
  8. 15.Linux 高性能服务器编程 --- 进程池和线程池
  9. 52. yii create webapp
  10. 2.ubuntu 配置目录