一个基于protocol buffer的RPC实现

一个基于protocol buffer的RPC实现

Protocol Buffer仅仅是提供了一套序列化和反序列化结构数据的机制,本身不具有RPC功能,但是可以基于其实现一套RPC框架。

Services

protocol buffer的Services类型是专门用来给RPC实现定义服务用的。

定义示例如下:

1
2
3
service SearchService {
   rpc Search (SearchRequest) returns (SearchResponse);
}

Search是方法名,SearchRequest是参数,SearchResponse是返回类型,SearchRequest、SearchResponse分别都是预先定义的Message类型。这个Service经过编译后会生成一个SearchService类和其对应的stub实现SearchService_Stub。SearchService_Stub把调用都转给RpcChannel处理,RpcChannel是一个接口类,RPC系统中一般自己重载RpcChannel,例如你可以在重载类中把调用请求序列化后通过网络传输到服务端。然后客户端就可以像下面的代码一样进行RPC调用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using   google::protobuf;
protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;
void   DoSearch() {
   // You provide classes MyRpcChannel and MyRpcController, which implement
   // the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
   channel = new   MyRpcChannel( "somehost.example.com:1234" );
   controller = new   MyRpcController;
   // The protocol compiler generates the SearchService class based on the
   // definition given above.
   service = new   SearchService::Stub(channel);
   // Set up the request.
   request.set_query( "protocol buffers" );
 
   // Execute the RPC.
 
   service->Search(controller, request, response, protobuf::NewCallback(&Done));
 
}
 
void   Done() {
 
   delete service;
 
   delete channel;
 
   delete controller;
 
}

服务端这边要实现Service接口,就是负责具体RPC函数的实现。并且在一网络接口上监听请求,处理请求,反序列化收到的网络数据后转调到这个函数的实现,之后把返回值序列化发回客户端作为调用结果。像下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using   google::protobuf;
 
class   ExampleSearchService : public   SearchService {
 
public :
 
   void   Search(protobuf::RpcController* controller,
 
               const   SearchRequest* request,
 
               SearchResponse* response,
 
               protobuf::Closure* done) {
 
     if   (request->query() == "google" ) {
 
       response->add_result()->set_url( "http://www.google.com" );
 
     } else   if   (request->query() == "protocol buffers" ) {
 
       response->add_result()->set_url( "http://protobuf.googlecode.com" );
 
     }
 
     done->Run();
 
   }
 
};
 
int   main() {
 
   // You provide class MyRpcServer.  It does not have to implement any
 
   // particular interface; this is just an example.
 
   MyRpcServer server;
 
   protobuf::Service* service = new   ExampleSearchService;
 
   server.ExportOnPort(1234, service);
 
   server.Run();
 
   delete service;
 
   return   0;
 
}

一个RPC实现

代码在这(https://github.com/persistentsnail/easy_pb_rpc

协议

1
2
3
4
5
6
7
8
9
10
11
12
13
package RPC;
option cc_generic_services = true ;                                              
message RpcRequestData {                                                        
     required uint32 service_id = 1;    // 对应Service                                          
     required uint32 method_id = 2;      // 对应Service中的函数                                         
     required uint32 call_id = 3;        // 对应本次调用(可能同一函数短时间有多次请求调用)                      
     required bytes content = 4;       // 对应已经序列化了的函数参数Message                                          
}
message RpcResponseData {
     required uint32 call_id = 1;       // 对应请求的call_id                                          
 
     required bytes content = 2;        // 对应已经序列化了的返回值Message                        
}

RPC请求是RpcRequestData Message,返回是RpcResponseData Message。service_id定义在一个配置文件services.cfg中,一个service_id对应一个服务名字,由服务端客户端共享,在程序启动时初始化一个一一映射的map。(这样的实现不太好,后面会提到)。在网络上传递的数据格式比较简单:

| Length of Encoding Binary Data (unsigned int) | RpcRequestData or RpcResponseData |。

客户端

支持RPC同步异步调用,例如:

1
2
3
4
5
6
7
void   Foo(::google ::protobuf:: RpcController* controller ,
 
const   ::FooRequest * request,
 
                        :: FooResponse* response ,
 
                        :: google::protobuf ::Closure* done);

以回调参数Closure为准,若为NULL则是同步调用,反之异步回调之。内部实现上创建了一个底层工作线程,重载的RpcChannel实现把每次调用结构化一个一个msg放到msg queue中,工作线程从msg queue中取msg处理,具体来说就是把msg序列化通过网络接口把请求传出去。逻辑上一个RpcChannel实例代表一个网络连接,所以可以重复使用一个RpcChannel对象。下面是同步调用一个EchoService的Foo方法的客户端代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RpcClient client ;
 
RpcChannel channel(&client , "127.0.0.1:18669" );
 
EchoService::Stub echo_clt(& channel);
 
FooRequest request ;
 
request.set_text ( "test1" );
 
request.set_times (1);
 
FooResponse response ;
 
RpcController controller ;
 
echo_clt.Foo (&controller, & request, &response , NULL);

RpcClient管理所有连接会话,管理消息队列,工作线程,只需要一个实例对象,RpcChannel 使用RpcClient完成连接和转调。

服务端

首先注册服务,就是创建Service的实现类对象,放到容器里面。然后在一个网络端口上监听连接,解析网络数据包,根据不同请求在服务容器里面找合适的service调用相应method。实现的比较简单,一个单线程服务器,同时只能处理一个请求。一个提供EchoService服务的server代码看起来是这样:

1
2
3
4
5
6
7
EchoServiceImpl *impl = new   EchoServiceImpl();
 
RpcServer rpc_server ;
 
rpc_server.RegisterService (impl);
 
rpc_server.Start ();

服务端客户端网络数据处理使用的都是libevent。

一些protocol buffer的细节

1. 之前用到了service_id,要在双端同时维护一份service id和name互相对应的配置文件,不利于部署和更新。protocol buffer可以通过DescriptorPool自省出自己有哪些服务和方法的,可以参见http://www.cnblogs.com/Solstice/archive/2011/04/03/2004458.html。所以在定义协议的时候可以直接用service name而不是id,而那份配置文件自然也不需要。客户端用服务名字做一个RPC请求,服务端通过名字判断是否自己存在这个服务。相应的method_id也可以考虑用method name。但是用id也是有好处,id是数值类型使用的Base 128 Varints变长编码比字符串表示的name生成的数据包更小,另外数值做的哈希应该比DescriptorPool通过名字查找服务类更快。

 

2.应该充分使用protocol buffer错误处理方式,那就是使用RpcController 来做错误跟踪。

 

3.协议字段类型多使用optional,因为required字段是必须有数据的,相反optional却不一定需要,如果没有就是一个默认值。optional类型通常用来升级协议,比如一个Message添加了一个新的optinal字段,以前使用老的Message格式的代码序列出来的Message仍然能够被使用新的Message格式的代码正确解析,因为optional字段不存在,他会使用默认值;类似的,使用新的Message格式的代码序列出来的Message也能够被使用老的Message格式的代码正确解析,因为他会忽略不认识的字段,而且他不丢掉这个字段,也就是这个Message还能被继续正确的传输。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值