利用protobuf实现RPC框架之protobuf 基本使用

一、前言

今天聊一聊 RPC 的相关内容,来看一下如何利用 Google 的开源序列化工具protobuf,来实现一个我们自己的 RPC 框架。文章比较长,但是值得想了解RPC的小伙伴阅读参考。整个系列内容分为四个部分:

  • RPC介绍
  • protobuf 基本使用
  • 网络通信框架libevent介绍
  • 实现 RPC 框架

二、protobuf 基本使用

1.基本知识

Protobuf是Protocol Buffers的简称,它是 Google 开发的一种跨语言、跨平台、可扩展的用于序列化数据协议,

Protobuf 可以用于结构化数据序列化(串行化),它序列化出来的数据量少,再加上以 K-V 的方式来存储数据,非常适用于在网络通讯中的数据载体。

只要遵守一些简单的使用规则,可以做到非常好的兼容性和扩展性,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

Protobuf 中最基本的数据单元是message,并且在message中可以多层嵌套message或其它的基础数据类型的成员。

Protobuf 是一种灵活,高效,自动化机制的结构数据序列化方法,可模拟 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更简单,而且它支持 Java、C++、Python 等多种语言。

2. 使用步骤

Step1:创建 .proto 文件,定义数据结构

例如,定义文档, 其中的内容为: echo_service.proto,参考用例如下:

message EchoRequest {
	string message = 1;
}

message EchoResponse {
	string message = 1;
}

message AddRequest {
	int32 a = 1;
	int32 b = 2;
}

message AddResponse {
	int32 result = 1;
}

service EchoService {
	rpc Echo(EchoRequest) returns(EchoResponse);
	rpc Add(AddRequest) returns(AddResponse);
}

最后的 ,是让 protoc 生成接口类,其中包括 2 个方法Echo 和 Add:service EchoService。

Echo 方法:客户端调用这个方法,请求的「数据结构」 EchoRequest 中包含一个 string 类型,也就是一串字符;服务端返回的「数据结构」 EchoResponse 中也是一个 string 字符串; Add 方法:客户端调用这个方法,请求的「数据结构」 AddRequest 中包含 2 个整型数据,服务端返回的「数据结构」 AddResponse 中包含一个整型数据(计算结果);

Step2: 使用 protoc 工具,编译 .proto 文档,生成界面(类以及相应的方法)

protoc echo_service.proto -I./ --cpp_out=./

执行以上命令,即可生成两个文件:,在这2个文件中,定义了2个重要的类,也就是下图中绿色部分:echo_service.pb.h, echo_service.pb.c

利用protobuf实现RPC框架之protobuf 基本使用_第1张图片
EchoService 和EchoService_Stub这 2 个类就是接下来要介绍的重点。 我把其中比较重要的内容摘抄如下(为减少干扰,把命名空间字符都去掉了):

class EchoService : public ::PROTOBUF_NAMESPACE_ID::Service {
    virtual void Echo(RpcController* controller, 
                    EchoRequest* request,
                    EchoResponse* response,
                    Closure* done);
                    
   virtual void Add(RpcController* controller,
                    AddRequest* request,
                    AddResponse* response,
                    Closure* done);
                    
    void CallMethod(MethodDescriptor* method,
                  RpcController* controller,
                  Message* request,
                  Message* response,
                  Closure* done);
}
class EchoService_Stub : public EchoService {
 public:
  EchoService_Stub(RpcChannel* channel);

  void Echo(RpcController* controller,
            EchoRequest* request,
            EchoResponse* response,
            Closure* done);
            
  void Add(RpcController* controller,
            AddRequest* request,
            AddResponse* response,
            Closure* done);
            
private:
    // 成員變數,比較關鍵
    RpcChannel* channel_;
};

Step3:服务端程序实现接口中定义的方法,提供服务;客户端调用接口函数,调用远程的服务。

利用protobuf实现RPC框架之protobuf 基本使用_第2张图片
请关注上图中的绿色部分。

(1)服务端:EchoService

EchoService类中的两个方法 Echo 和 Add 都是虚函数,我们需要继承这个类,定义一个业务层的服务类 EchoServiceImpl,然后实现这两个方法,以此来提供远程调用服务。

EchoService 类中也给出了这两个函数的默认实现,只不过是提示错误信息:

void EchoService::Echo() {
  controller->SetFailed("Method Echo() not implemented.");
  done->Run();
}
void EchoService::Add() {
  controller->SetFailed("Method Add() not implemented.");
  done->Run();
}

图中的EchoServiceImpl就是我们定义的类,其中实现了 Echo 和 Add 这两个虚函数:

void EchoServiceImpl::Echo(RpcController* controller,
                   EchoRequest* request,
                   EchoResponse* response,
                   Closure* done)
{
	// 获取请求消息,然后在末尾加上资讯:", welcome!",返回給客戶端
	response->set_message(request->message() + ", welcome!");
	done->Run();
}

void EchoServiceImpl::Add(RpcController* controller,
                   AddRequest* request,
                   AddResponse* response,
                   Closure* done)
{
	// 获取请求数据中的 2 个整型数据
	int32_t a = request->a();
	int32_t b = request->b();

	// 计算结果,然后放入响应数据中
	response->set_result(a + b);

	done->Run();
}

2)客户端:EchoService_Stub

EchoService_Stub就相当于是客户端的代理,应用程序只要把它"当做"远程服务的替身,直接调用其中的函数就可以了(图中左侧的步骤1)。

因此,EchoService_Stub此类中肯定要实现 Echo 和 Add 这 2 个方法,看一下 protobuf自动生成的实现代码:

void EchoService_Stub::Echo(RpcController* controller,
                            EchoRequest* request,
                            EchoResponse* response,
                            Closure* done) {
  channel_->CallMethod(descriptor()->method(0),
                       controller, 
                       request, 
                       response, 
                       done);
}

void EchoService_Stub::Add(RpcController* controller,
                            AddRequest* request,
                            AddResponse* response,
                            Closure* done) {
  channel_->CallMethod(descriptor()->method(1),
                       controller, 
                       request, 
                       response, 
                       done);
}

看到没,每一个函数都调用了成员变量 channel_ 的 CallMethod 方法(图中左侧的步骤2),这个成员变量的类型是google::p rotobuf:RpcChannel。

从字面上理解:channel 就像一个通道,是用来解决数据传输问题的。 也就是说方法会把所有的数据结构序列化之后,通过网络发送给服务器。channel_->CallMethod

既然 RpcChannel 是用来解决网络通信问题的,因此客户端和服务端都需要它们来提供数据的接收和发送。

图中的是客户端使用的 Channel, 是服务端使用的 Channel,它俩都是继承自 protobuf 提供的。RpcChannelClientRpcChannelServer RpcChannel
在这里插入图片描述
注意:这里的,只是提供了网络通讯的策略,至于通讯的机制是什么(TCP? UDP? http?),protobuf并不关心,这需要由RPC框架来决定和实现。 RpcChannel

protobuf 提供了一个基类,其中定义了方法。 我们的 RPC 框架中,客户端和服务端实现的 Channel必须继承protobuf 中的,然后重载这个方法。 RpcChannelCallMethod RpcChannelCallMethod

CallMethod 方法的几个参数特别重要,我们通过这些参数,来利用 protobuf 实现序列化、控制函数调用等操作,也就是说这些参数就是一个纽带,把我们写的代码与 protobuf 提供的功能,连接在一起。

小结

本篇文章对protobuf使用做了说明,同时也给出了对应的例子,但是还没有谈到数据之间的传输如何进行。下一篇文章我们将介绍著名的网络库libevent是如何实现数据通信的。

原文链接

你可能感兴趣的:(RPC框架,rpc,网络,c++)