a rpc using boost.asio and protobuf

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都知道下面四个类的存在。他们的意义分别是:

  1. RpcRequest: 存储客户端发送的请求。
  2. RpcResponse: 存储server端发送的响应。
  3. EchoService: 在服务器端提供服务。
  4. 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名称组合起来,来表示某个特定服务。

  1. EchoService为基类,server中需要提供各种实现,不同实现需要进行唯一标识。
  2. 在每种EchoService的视线中,method需要唯一标识。

本实现中,MyServer不仅作为监听请求的接口,也作为容器,根据请求中的service-method标识符,来调用响应服务。下面介绍具体流程。

  1. server端
    (1) 创建MyServer对象,创建派生自EchoService的类对象,并注册MyServer中,注册这些类对象和函数——service-method作唯一标识符。
    (2) MyServer监听端口,接受client请求,提取相关信息,并传递给Service。
    (3) 调用Service服务,发送响应数据。

  2. 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来包裹请求信息。每次进行一次交互,需要确定下面三个信息:

  1. 请求的服务名称:service-method
  2. client具体的请求信息:RpcRequest
  3. 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接收数据的流程为:

  1. 读取4 byte整数,描述RpcMeta的长度meta_len.
  2. 根据meta_len来读取RpcMeta对象,解析出servie_name,method_name和data_len。
  3. 根据data_len,读取Rpcrequest数据,并进行转化。
  4. 根据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调用框架中的具体调用流程。

  1. server端
    (1) 定义抽象基类EchoService, 其提供统一调用接口:CallMethod(), 类实现接口Echo(),由CallMethod()来判断应该调用哪个service-method。
    (2) CallMethod()根据MethodDescriptor调用Echo()

  2. client端
    (1) EchoService_Stub提供统一调用接口:Echo(), 其调用具体的RpcChannel::CallMethod(),来与server端的Service进行交互。
    (2) Echo()调用CallMethod().

client端 client端 server端 server端
Echo()-> CallMethod()-> ->CallMethod()-> Echo()
发布了31 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/zhizifengxiang/article/details/88634953