前言

RPC采用客户机/服务器模式实现两个进程之间的相互通信,socket是RPC经常采用的通信手段之一。当然,除了socket,RPC还有其他的通信方法:RDMA、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 -;
} std::string server_addr("0.0.0.0:12321");
if (!rpc_server.Start(server_addr)) {
std::cout << "start server failed" << std::endl;
return -;
} return ;
}

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[];
char* port = argv[];
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 ;
}

上面是一个简单的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 = ;
}
message EchoResponse {
string message = ;
}
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_[] = 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 = ;
std::vector<char> contents(request_data_len, );
socket->receive(boost::asio::buffer(contents)); ProcRpcData(std::string(&contents[], 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(), 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 = ;
std::vector<char> resp_data(resp_data_len, );
socket_->receive(boost::asio::buffer(resp_data)); response->ParseFromString(std::string(&resp_data[], 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_[];
auto m_descriptor = service->GetDescriptor()->method();
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[]);
switch(method->index()) {
case :
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. 基于Protobuf的分布式高性能RPC框架——Navi-Pbrpc

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

  2. 基于七牛云对象存储,搭建一个自己专属的极简Web图床应用(手摸手的注释讲解核心部分的实现原理)

    一个极简的Web图床应用,支持复制粘贴与拖拽上传图片 1.开发缘由 日常使用Vs Code编写markdown笔记与博客文章时,在文章中插入图片时发现非常不便 使用本地文件编写相对路径---没法直接复 ...

  3. 我的第一个 Rails 站点:极简优雅的笔记工具-Raysnote

    出于公司开发需求,这个暑假我開始搞Ruby on Rails.在业余时间捣鼓了一个在线笔记应用:http://raysnote.com.这是一个极简而优雅的笔记站点(至少我个人这么觉得的). 笔记支持 ...

  4. 手写一个Web服务器,极简版Tomcat

    网络传输是通过遵守HTTP协议的数据格式来传输的. HTTP协议是由标准化组织W3C(World Wide Web Consortium,万维网联盟)和IETF(Internet Engineerin ...

  5. (数据科学学习手札136)Python中基于joblib实现极简并行计算加速

    本文示例代码及文件已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 我们在日常使用Python进行各种数据计算 ...

  6. 【转】手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)

    前言 做这个 vueAdmin-template 的主要原因是: vue-element-admin 这个项目的初衷是一个vue的管理后台集成方案,把平时用到的一些组件或者经验分享给大家,同时它也在不 ...

  7. 基于 Node.js 平台,快速、开放、极简的 web 开发框架。

    资料地址:http://www.expressjs.com.cn/ Express 基于 Node.js 平台,快速.开放.极简的 web 开发框架. $ npm install express -- ...

  8. 基于Linux-3.9.4的mykernel实验环境的极简内核分析

    382 + 原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ 一.实验环境 win10 -> VMware -> Ubuntu1 ...

  9. [开发技巧]·Python极简实现滑动平均滤波(基于Numpy.convolve)

    [开发技巧]·Python极简实现滑动平均滤波(基于Numpy.convolve) ​ 1.滑动平均概念 滑动平均滤波法(又称递推平均滤波法),时把连续取N个采样值看成一个队列 ,队列的长度固定为N ...

随机推荐

  1. CF996B World Cup 思维 第十四 *

    World Cup time limit per test 1 second memory limit per test 256 megabytes input standard input outp ...

  2. 2017 省赛选拨 火车入站 CSU 1757 模拟

    1757: 火车入站 Submit Page   Summary   Time Limit: 1 Sec     Memory Limit: 128 Mb     Submitted: 512     ...

  3. Docker笔记(十):使用Docker来搭建一套ELK日志分析系统

    一段时间没关注ELK(elasticsearch —— 搜索引擎,可用于存储.索引日志, logstash —— 可用于日志传输.转换,kibana —— WebUI,将日志可视化),发现最新版已到7 ...

  4. 运维核心基础知识之——MD5sum校验文件

    如何使用MD5sum工具校验你的文件. 演示过程截图: 先给文件创建一个md5值 md5sum oldboy.txt 然后将md5sum生成的md5值写入到一个文件police.log md5sum ...

  5. Docker下dubbo开发三部曲之三:java开发

    在前两章<Docker下dubbo开发,三部曲之一:极速体验>和<Docker下dubbo开发,三部曲之二:本地环境搭建>中,我们体验了dubbo环境搭建以及服务的发布和消费, ...

  6. 新版本SpringCloud sleuth整合zipkin

    SpringCloud Sleuth 简介 Spring Cloud Sleuth为Spring Cloud实现了分布式跟踪解决方案. Spring Cloud Sleuth借鉴了Dapper的术语. ...

  7. Apache Hadoop集群安装(NameNode HA + SPARK + 机架感知)

    1.主机规划 序号 主机名 IP地址 角色 1 nn-1 192.168.9.21 NameNode.mr-jobhistory.zookeeper.JournalNode 2 nn-2 ).HA的集 ...

  8. 第一次接触WebSocket遇到的坑以及感受

    要求用.net写一个服务,然后通过webscoket实现客户端与服务端之间的通信. 第一次知道.net还可以用来写服务,然后问题来了,服务是什么- -..下面图里的就是服务,可以停止暂停和启动. 我要 ...

  9. 搭建数据库galera集群

    galera集群 galera简介 galera集群又叫多主集群,用于数据库的同步,保证数据安全 最少3台,最好是奇数台数,当一台机器宕掉时,因为仲裁机制,这台机器就会被踢出集群. 通过wsrep协议 ...

  10. 最新2019Pycharm破解教程,附激活码!

    本教程仅用作个人学习,请勿用于商业获利,造成后果自负!!! Pycharm安装 在这插一个小话题哈,Pycharm只是一个编译器,并不能代替Python,如果要使用Python,还是需要安装Pytho ...