2022/4/14更新:
在我重新回顾这篇文章的时候,我觉得里面内容有点乱,主要还是因为RPC里面涉及到很多概念和知识点。本来代码内容就已经挺抽象了,还要结合各种概念,让人难以阅读,所以特地写了下面这一篇文章,梳理了RPC框架的基本原理和知识点,顺便拓展了一些RPC在实际应用中会遇到的问题。
本次项目用C++实现了一个简单的RPC分布式网络通信框架,因此写下本篇文章梳理一下该框架的实现逻辑和相关知识点。另外本篇文章和项目还在迭代过程中,还有很多功能,比如客户端异步调用,负载均衡,异常重试,健康检测,熔断机制等等都还没有实现,研究生生涯之前一定会尽量实现,并且出一个超级棒的教程的。
另外项目代码地址为: https://github.com/yyg192/yyg_rpc_server
RPC是远程过程调用(Remote Procedure Call)的缩写,可以通过网络从远程服务器上请求服务(调用远端服务器上的函数并获取返回结果)。简单来说,客户端程序可以像调用本地函数一样直接调用运行在服务端的函数。
大概画了一下RPC通信框架的大致结构流程图。
ZooKeeper在这里作为服务方法的管理配置中心,负责管理服务方法提供者对外提供的服务方法。服务方法提供者提前将本端对外提供的服务方法名及自己的通信地址信息(IP:Port)注册到ZooKeeper。当Caller发起远端调用时,会先拿着自己想要调用的服务方法名询问ZooKeeper,ZooKeeper告知Caller想要调用的服务方法在哪台服务器上(ZooKeeper返回目标服务器的IP:Port给Caller),Caller便向目标服务器Callee请求服务方法调用。服务方在本地执行相应服务方法后将结果返回给Caller。
ProtoBuf能提供对数据的序列化和反序列化,ProtoBuf可以用于结构化数据的串行序列化,并且以Key-Value格式存储数据,因为采用二进制格,所以序列化出来的数据比较少,作为网络传输的载体效率很高。
Caller和Callee之间的数据交互就是借助ProtoBuf完成,具体的使用方法和细节后面会进一步拓展。
Muduo库是基于(Multi-)Reactor模型的多线程网络库,在RPC通信框架中涉及到网络通信。另外我们可以服务提供方实现为IO多线程,实现高并发处理远端服务方法请求。
这里默认你对Muduo库比较熟悉,后续篇幅不对涉及Muduo库的内容进行任何讲解。
RPC通信过程中的代码调用流程图大致就是下面这样(暂时画的还不是很友好,日后会改进的!)
RPC是一种通信协议,所以直接把RPC框架代码摆出来可能比较抽象,这里写一个简单的业务代码,这个通信框架找一个业务场景,之后再深入RPC框架内容。
RPC通信交互的数据在发送前需要用ProtoBuf进行二进制序列化,并且在通信双方收到后要对二进制序列化数据进行反序列化。双方通信时发送的都是固定结构的消息体,比如登录请求消息体(用户名+密码),注册请求消息体(用户id+用户名+消息体)。
关于ProtoBuf基本使用方法,建议自行Google,至此默认你已经会ProtoBuf的一点基本使用了。为了节约文章篇幅,我将user.proto代码截图拼在了一起。
本项目业务场景为Caller调用远端方法Login和Register。Callee中的Login函数接收一个LoginRequest消息体,执行完Login逻辑后将处理结果填写进LoginResponse消息体,再返回给Caller。调用Register函数过程同理。
Callee对外提供远端可调用方法Login和Register,要在user.proto中进行注册(service UserServiceRpc
)。在Callee中的Login方法接受LoginRequest message,执行完逻辑后返回LoginResponse message给Caller。
注意UserServiceRpc就是我们所说的服务名,而Login和Register就是方法名。
接着使用protoc来编译这个.proto文件
protoc user.proto -I ./ -cpp_out=./user
然后就能生成user.cc和user.h文件了。user.cc和user.h里面提供了两个非常重要的类供c++程序使用,其中UserServiceRpc_Stub
类给caller使用,UserServiceRpc
给callee使用。caller可以调用UserServiceRpc_Stub::Login(...)
发起远端调用,而callee则继承UserServiceRpc
类并重写UserServiceRpc::Login(...)
函数,实现Login函数的处理逻辑。
另外我们在user.proto中注册了通信的消息体(LoginRequest、LoginResponse、RegisterResponse(其中嵌套了ResultCode)),这些注册的消息体也会由protoc生成对应的C++类和业务代码友好交互。
好好结合业务层代码来理解。
结合2.1.2节的业务代码好好理解一下。
**Caller远端调用Login函数,Callee执行Login函数并返回结果,Caller获取返回结果 **。
下面的代码中涉及到后面会讲到的rpc框架方法,这里只要稍微理解一下这些方法的作用就可以,看不懂的话暂时还不需要深入。
/****
文件注释:
文件名: calluserservice.cc
caller端代码:caller向callee发起远端调用,即caller想要调用处于callee中的Login函数。
****/
#include
#include "mprpcapplication.h"
#include "user.pb.h"
#include "mprpcchannel.h"
int main(int argc, char **argv)
{
MprpcApplication::Init(argc, argv);
//MprpcApplication类提供了解析argc和argv参数的方法,我们在终端执行这个程序的时候,需要通过-i参数给程序提供一个配置文件,这个配置文件里面包含了一些通信地址信息(后面提到)
fixbug::UserServiceRpc_Stub stub(new MprpcChannel());
//这一步操作后面会讲,这里就当是实例化UserServiceRpc_Stub对象吧。UserServiceRpc_Stub是由user.proto生成的类,我们之前在user.proto中注册了Login方法,
fixbug::LoginRequest request;
request.set_name("zhang san");
request.set_pwd("123456");
//回想起我们的user.proto中注册的服务方法:
// rpc Login(LoginRequest) returns(LoginResponse);
// callee的Login函数需要参数LoginRequest数据结构数据
fixbug::LoginResponse response;
// callee的Login函数返回LoginResponse数据结构数据
stub.Login(nullptr, &request, &response, nullptr);
//caller发起远端调用,将Login的参数request发过去,callee返回的结果放在response中。
if (0 == response.result().errcode())
std::cout << "rpc login response success:" << response.sucess() << std::endl;
else
std::cout << "rpc login response error : " << response.result().errmsg() << std::endl;
//打印response中的内容,别忘了这个result和success之前在user.proto注册过
return 0;
}
/***
文件注释:
文件名: userservice.cc
callee端代码:callee提供caller想要调用的Login函数。
***/
#include
#include
#include "user.pb.h"
#include "mprpcapplication.h"
#include "rpcprovider.h"
class UserService : public fixbug::UserServiceRpc // 使用在rpc服务发布端(rpc服务提供者)
{
public:
bool Login(std::string name, std::string pwd)
{