今天聊一聊 RPC 的相关内容,来看一下如何利用 Google 的开源序列化工具protobuf,来实现一个我们自己的 RPC 框架。文章比较长,但是值得想了解RPC的小伙伴阅读参考。整个系列内容分为四个部分:
Protobuf是Protocol Buffers的简称,它是 Google 开发的一种跨语言、跨平台、可扩展的用于序列化数据协议,
Protobuf 可以用于结构化数据序列化(串行化),它序列化出来的数据量少,再加上以 K-V 的方式来存储数据,非常适用于在网络通讯中的数据载体。
只要遵守一些简单的使用规则,可以做到非常好的兼容性和扩展性,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
Protobuf 中最基本的数据单元是message,并且在message中可以多层嵌套message或其它的基础数据类型的成员。
Protobuf 是一种灵活,高效,自动化机制的结构数据序列化方法,可模拟 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更简单,而且它支持 Java、C++、Python 等多种语言。
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
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:服务端程序实现接口中定义的方法,提供服务;客户端调用接口函数,调用远程的服务。
(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是如何实现数据通信的。
原文链接