基于protobuf的RPC实现

http://blog.csdn.net/kevinlynx/article/details/39379957

可以对照使用google protobuf RPC实现echo service一文看,细节本文不再描述。

google protobuf只负责消息的打包和解包,并不包含RPC的实现,但其包含了RPC的定义。假设有下面的RPC定义:

[cpp] view plain copy

  1. service MyService {  
  2.         rpc Echo(EchoReqMsg) returns(EchoRespMsg)   
  3.     }  

那么要实现这个RPC需要最少做哪些事?总结起来需要完成以下几步:

客户端

RPC客户端需要实现google::protobuf::RpcChannel。主要实现RpcChannel::CallMethod接口。客户端调用任何一个RPC接口,最终都是调用到CallMethod。这个函数的典型实现就是将RPC调用参数序列化,然后投递给网络模块进行发送。

[cpp] view plain copy

  1. void CallMethod(const ::google::protobuf::MethodDescriptor* method,  
  2.                   ::google::protobuf::RpcController* controller,  
  3. const ::google::protobuf::Message* request,  
  4.                   ::google::protobuf::Message* response,  
  5.                   ::google::protobuf::Closure* done) {  
  6.         ...  
  7.         DataBufferOutputStream outputStream(...) // 取决于你使用的网络实现
  8.         request->SerializeToZeroCopyStream(&outputStream);  
  9.         _connection->postData(outputStream.getData(), ...  
  10.         ...  
  11.     }  

服务端

服务端首先需要实现RPC接口,直接实现MyService中定义的接口:

[cpp] view plain copy

  1. class MyServiceImpl : public MyService {  
  2. virtual void Echo(::google::protobuf::RpcController* controller,  
  3. const EchoReqMsg* request,  
  4.             EchoRespMsg* response,  
  5.             ::google::protobuf::Closure* done) {  
  6.             ...  
  7.             done->Run();  
  8.         }  
  9.     }  

标示service&method

基于以上,可以看出服务端根本不知道客户端想要调用哪一个RPC接口。从服务器接收到网络消息,到调用到MyServiceImpl::Echo还有很大一段距离。

解决方法就是在网络消息中带上RPC接口标识。这个标识可以直接带上service name和method name,但这种实现导致网络消息太大。另一种实现是基于service name和method name生成一个哈希值,因为接口不会太多,所以较容易找到基本不冲突的字符串哈希算法。

无论哪种方法,服务器是肯定需要建立RPC接口标识到protobuf service对象的映射的。

这里提供第三种方法:基于option的方法。

protobuf中option机制类似于这样一种机制:service&method被视为一个对象,其有很多属性,属性包含内置的,以及用户扩展的。用户扩展的就是option。每一个属性有一个值。protobuf提供访问service&method这些属性的接口。

首先扩展service&method的属性,以下定义这些属性的key:

[cpp] view plain copy

  1. extend google.protobuf.ServiceOptions {  
  2.       required uint32 global_service_id = 1000;   
  3.     }  
  4.     extend google.protobuf.MethodOptions {  
  5.       required uint32 local_method_id = 1000;  
  6.     }  

应用层定义service&method时可以指定以上key的值:

[cpp] view plain copy

  1. service MyService  
  2.     {  
  3.         option (arpc.global_service_id) = 2302;   
  4.         rpc Echo(EchoReqMsg) returns(EchoRespMsg)   
  5.         {  
  6.             option (arpc.local_method_id) = 1;  
  7.         }  
  8.         rpc Echo_2(EchoReqMsg) returns(EchoRespMsg)   
  9.         {  
  10.             option (arpc.local_method_id) = 2;  
  11.         }  
  12.         ...  
  13.     }  

以上相当于在整个应用中,每个service都被赋予了唯一的id,单个service中的method也有唯一的id。

然后可以通过protobuf取出以上属性值:

[cpp] view plain copy

  1.                   ::google::protobuf::RpcController* controller,  
  2. const ::google::protobuf::Message* request,  
  3.                   ::google::protobuf::Message* response,  
  4.                   ::google::protobuf::Closure* done) {  
  5.         ...  
  6.         google::protobuf::ServiceDescriptor *service = method->service();  
  7.         uint32_t serviceId = (uint32_t)(service->options().GetExtension(global_service_id));  
  8.         uint32_t methodId = (uint32_t)(method->options().GetExtension(local_method_id));  
  9.         ...  
  10.     }  

考虑到serviceId methodId的范围,可以直接打包到一个32位整数里:

[plain] view plain copy

  1. uint32_t ret = (serviceId << 16) | methodId;  

然后就可以把这个值作为网络消息头的一部分发送。

当然服务器端是需要建立这个标识值到service的映射的:

[cpp] view plain copy

  1. bool MyRPCServer::registerService(google::protobuf::Service *rpcService) {  
  2. const google::protobuf::ServiceDescriptor = rpcService->GetDescriptor();  
  3. int methodCnt = pSerDes->method_count();  
  4. for (int i = 0; i < methodCnt; i++) {  
  5.             google::protobuf::MethodDescriptor *pMethodDes = pSerDes->method(i);  
  6.             uint32_t rpcCode = PacketCodeBuilder()(pMethodDes); // 计算出映射值
  7.             _rpcCallMap[rpcCode] = make_pair(rpcService, pMethodDes); // 建立映射
  8.         }  
  9. return true;  
  10.     }  

服务端收到RPC调用后,取出这个标识值,然后再从_rpcCallMap中取出对应的service和method,最后进行调用:

[cpp] view plain copy

  1. google::protobuf::Message* response = _pService->GetResponsePrototype(_pMethodDes).New();  
  2. // 用于回应的closure
  3.     RPCServerClosure *pClosure = new (nothrow) RPCServerClosure(   
  4.             _channelId, _pConnection, _pReqMsg, pResMsg, _messageCodec, _version);  
  5.     RPCController *pController = pClosure->GetRpcController();  
  6.     ...  
  7. // protobuf 生成的CallMethod,会自动调用到Echo接口
  8.     _pService->CallMethod(_pMethodDes, pController, _pReqMsg, pResMsg, pClosure);  

参考

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

论获取缓存值的正确姿势

论获取缓存值的正确姿势 cache 时至今日,大家对缓存想必不在陌生。我们身边各种系统中或多或少的都存在缓存,自从有个缓存,我们可以减少很多计算压力,提高应用程...

38280
来自专栏Java 源码分析

Netty 入门

1. 粘包问题 一 .长连接与短连接: 1.长连接:Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收。长连接在 net...

31050
来自专栏互联网杂技

内存卡存储原理,你知道吗?

1、 简介: SD卡(Secure Digital Memory Card)是一种为满足安全性、容量、性能和使用环境等各方面的需求而设计的一种新型存储器...

48460
来自专栏MixLab科技+设计实验室

设计师编程指南之Sketch插件开发 1

发现网上关于sketch插件开发的指南太少了,而且都不一定可以成功运行,于是我就写了这个系列的文章: 1 我们需要了解的语法特点 sketch 是基于 Coc...

70980
来自专栏Golang语言社区

[go语言]吐槽:怎么样实现支持并发访问的数据集合更好?

在go语言里,提倡用信道通讯的方式来替代显式的同步机制。但是我发现有的时候用信道通讯方式实现的似乎也不是很好(暂不考虑效率问题)。 假设有一个帐号的集合,需要在...

43570
来自专栏高性能服务器开发

(六)关于网络编程的一些实用技巧和细节

这些年,接触了形形色色的项目,写了不少网络编程的代码,从windows到linux,跌进了不少坑,由于网络编程涉及很多细节和技巧,一直想写篇文章来总结下这方面的...

40070
来自专栏美团技术团队

Node.js Stream - 进阶篇

在构建较复杂的系统时,通常将其拆解为功能独立的若干部分。这些部分的接口遵循一定的规范,通过某种方式相连,以共同完成较复杂的任务。譬如,shell通过管道|连接各...

43540
来自专栏极客生活

macOS扫雷逆向破解

其中安全帽只有10个,用完了之后就需要在App Store进行购买,同时「高级」和「自定义」功能也需要在应用商店进行购买才可以玩。

14120
来自专栏码神联盟

碎片化 | 第四阶段-40-Struts组件分类讲解-视频

如清晰度低,可转PC网页观看高清版本: http://v.qq.com/x/page/o0567s4azx0.html ---- ---- 版权声明:本视频...

34990
来自专栏小樱的经验随笔

CTF---Web入门第五题 貌似有点难

貌似有点难分值:20 来源: 西普学院 难度:难 参与人数:7249人 Get Flag:2519人 答题人数:2690人 解题通过率:94% 不多说,去看题目...

33160

扫码关注云+社区

领取腾讯云代金券