0 序言
protobuf在rpc方面,仅仅提供了一个简单的框架和数据序列化手段,构建client与server之间的连接、通信、存储、线程处理等多方面问题需要用户自己实现。下面,将简单介绍使用protobuf和boost.asio来构建一个简单的rpc。该rpc只是简单阐述protobuf的rpc框架,耦合度较高,具体实现需要根据业务场景来实施。
1 定义.proto文件
protobuf提供了专门的关键字来定义rpc,下面是.proto的文件内容。
// echo.proto
package echo;
cc_generic_services = true; // 指示生成rpc
message RpcRequest { // 存储rpc请求的数据
required string msg = 1;
};
message RpcResponse { // 存储rpc反馈的数据
required string msg = 1;
}
service EchoService { // 定义rpc的接口
rpc Echo(RpcRequest) returns (RpcResponse);
rpc Send(RpcRequest) returns (RpcResponse);
}
上面.proto文件,经过下面的protoc编译,生成echo.pb.h和echo.pb.cc两个文件。
$ protoc --proto_path=./ --cpp_out=./ echo.proto
下面是生成文件中包含的4个类,client和server都知道下面四个类的存在。他们的意义分别是:
- RpcRequest: 存储客户端发送的请求。
- RpcResponse: 存储server端发送的响应。
- EchoService: 在服务器端提供服务。
- EchoService_Stub:在client端作为EchoService的代表提供服务。
class RpcRequest : public google::protobuf::Message;
class RpcRequest : public google::protobuf::Message;
class EchoService : public google::protobuf::Service;
class EchoService_Stub : public EchoService;
需要注意,EchoService运行在服务端,EchoService_Stub代表EchoService,运行在客户端,为EchoService在客户端上的代理——客户端通过EchoService_Stub来提供服务。
此思想与插座相同。用户通过插座获得电能,插座为stub,而实际由远端的电厂来供电。
2 远程调用原理
rpc(远程调用)实际上是client向server发送数据,server根据接受到的数据,转化成数字和字符串,提取信息。根据提取到的信息作对应处理,生成新的信息,返回给client。因此,在rpc的交互过程中,涉及到3个部分:客户端、服务端、客户与服务端之间的连接。
protobuf提供抽象基类接口RpcChannel供用户继承实现自己的连接方式。生成的EchoService_Stub类通过自定义的channel类,来向server端发送数据。这样,我们就有了如下的类:
1. class EchoService : public google::protobuf::Service // 供server调用,用来处理client端发送的请求信息
2. class EchoService_Stub : public EchoService // 供client调用,用来发送请求,处理server发送的反馈信息
3. class MyChannel : public google::protobuf::RpcChannel // 供client调用,用来传输client向server的请求,并接受反馈信息
4. class RpcRequest : public google::protobuf::Message // 存储请求信息,可以序列化
5. class RpcResponse : public google::protobuf::Message // 存储响应信息,可以序列化
6. class MyController : public google::protobuf::RpcController // 提供接口,使client和server控制双方的通信。
7. class MyServer // server,监听某个指定ip:port,接受client发送的请求数据,发送service生成的反馈信息。
基于下面的原因,protobuf为用户提供了用于描述service和method的描述符。在本实现中,我们将service和method名称组合起来,来表示某个特定服务。
- EchoService为基类,server中需要提供各种实现,不同实现需要进行唯一标识。
- 在每种EchoService的视线中,method需要唯一标识。
本实现中,MyServer不仅作为监听请求的接口,也作为容器,根据请求中的service-method标识符,来调用响应服务。下面介绍具体流程。
-
server端
(1) 创建MyServer对象,创建派生自EchoService的类对象,并注册MyServer中,注册这些类对象和函数——service-method作唯一标识符。
(2) MyServer监听端口,接受client请求,提取相关信息,并传递给Service。
(3) 调用Service服务,发送响应数据。 -
client端
(1) 创建RcpChannel派生类对象,建立连接请求。
(2) 创建EchoService_Stub,其为server端EchoService的代理。stub将利用channel来发送数据。
(3) 调用EchoService_Stub中的服务。
3 Service和Method描述符
我们使用service描述符和method描述符唯一确定一个服务接口。google::protobuf::Service::GetDescriptor()来获得ServiceDescirptor对象描述自身,并使用ServiceDescriptor获得其中的所有method描述符,用MethodDescriptor代表。
Service::GetDescriptor() -> ServiceDescriptor -> Service中包含的所有method描述符
这样,EchoService中的所有信息都暴露给用户。
4 server监听端口和接受request
本例中,我们使用 class RpcRequest : public google::protobuf::Message来包裹请求信息。每次进行一次交互,需要确定下面三个信息:
- 请求的服务名称:service-method
- client具体的请求信息:RpcRequest
- server应该返回的内容:RpcResponse
我们使用类 class RpcMeta : public google::protobuf::Message,用来描述request的信息:
// rpcMeta.proto
package "echo";
message RpcMeta {
required string service_name = 1;
required string service method_name = 2;
required int32 data_len = 3;
};
在RpcMeta中,我们需要指出服务的名称和请求信息的长度,这样,传输数据由如下三个字段拼接而成。
字段含义 | RpcMeta的大小 | RpcMeta对象序列化的数据 | RpcRequest对象序列化的数据 |
---|---|---|---|
类型 | int32(4 bytes) | RpcMeta | RpcRequest |
所以,server接收数据的流程为:
- 读取4 byte整数,描述RpcMeta的长度meta_len.
- 根据meta_len来读取RpcMeta对象,解析出servie_name,method_name和data_len。
- 根据data_len,读取Rpcrequest数据,并进行转化。
- 根据service_name,method_name调用对应的服务,并将格式化的数据传入服务中。
5 server调用service-method, 处理request
现在,server端已经拿到所有的数据:service_name, method_name, RpcRequest。在我们的设计中,server为一个容易,其使用service_name和method_name来映射实际的服务——service_name和method_name唯一确定了某个服务。
一旦服务确定,则该服务传入对象和返回对象也都确定,可以使用下面Service的方法来创建传入和返回的对象,进而将网络中传入的数据流格式化成规范化成结构数据。
1. virtual Message* google::protobuf::Service::GetRequestPrototype(MethodDescriptor*).New();
2. virtual Message* google::protobuf::Service::GetResponsePrototype(MethodDescriptor*).New();
这样,所有的格式化结构数据已经准备好,开始调用google::protobuf::Service::CallMethod()方法,其根据传入的MethodDescriptor,来调用响应的函数,至此,整个调用流程就完成了。
下面使CallMethod()所需要的传入参数。
1. MethodDescriptor : 描述Service中的对应方法
2. RpcController派生类对象 : 控制server如何响应请求,本demo并没有传输controller相关数据,可以扩充上面的三个字段,增加controller的信息。
3. Message* request :请求信息。
4. Message* response : 响应信息。
5. google::protobuf::Closure* done:作善后处理的函数,常用作发送响应。
6 client调用stub接口
.proto文件中定义的service会生成EchoService和EchoService_Stub两个类。EchoService_Stub为运行在server上的EchoService上的代理,所以EchoService_Stub提供了所有EchoService能够对外提供的服务。
stub重写了EchoService::Echo(),其接口签名与EchoService相同:
class EchoService : public EchoService {
public:
virtual void Echo(google::protobuf::RpcController* controller,
const echo::RpcRequest* request,
echo::RpcResponse* response,
google::protobuf::Closer* done);
}
7 client连接server:Channel
server端使用我们自定义的MyServer来监听接口,而client则使用RpcChannel来连接并发送数据。EchoService_Stub::Echo()函数调用RpcChannel::CallMethod()方法,来向server发送请求信息,protobuf仅仅提供了RpcChannel接口,需要用户自己定义相应实现来发送数据。
当server端完成数据处理,还需要RpcServer来组装RpcResponse规范化数据,供client使用,最终完成一次“远程过程调用”。
下面我们自定义的RpcChannel。
class MyChannel : public google::protobuf::RpcChannel {
public virtual CallMethod(const google::protobuf::RpcController* controller,
const google::protobuf::Message* request,
google::protobuf::Message* response,
google::protobuf::Closure* done);
};
8 总结
最后,下面的流程总结了protobuf调用框架中的具体调用流程。
-
server端
(1) 定义抽象基类EchoService, 其提供统一调用接口:CallMethod(), 类实现接口Echo(),由CallMethod()来判断应该调用哪个service-method。
(2) CallMethod()根据MethodDescriptor调用Echo() -
client端
(1) EchoService_Stub提供统一调用接口:Echo(), 其调用具体的RpcChannel::CallMethod(),来与server端的Service进行交互。
(2) Echo()调用CallMethod().
client端 | client端 | server端 | server端 |
---|---|---|---|
Echo()-> | CallMethod()-> | ->CallMethod()-> | Echo() |