RPC(Remote Procedure Call Protocol)远程过程调用协议,过程示意图
一、提供方配置
1.1、生成服务对象
要提供RPC服务,需要在proto中添加service信息,protoc会为每个service生成对应的C++类,包含虚函数,用户需要继承并实现这些函数。但这些生成的代码并不处理实际的网络通信,只是提供了一个框架,需要用户自己填充具体的逻辑,或者结合其他RPC库使用。
1 | syntax = "proto3"; |
1.2、提取服务
protobuf会为每一个service生成对应的抽象描述,具体实现需要我们继承这些类并实现对应的方法,然后通过protobuf内部实现的多态进行调用。
获取服务对象的描述信息,描述中包含service的
名字,方法数量以及方法的抽象描述,将这些信息封装到map表中,用于处理服务和方法的寻找1
2
3
4
5const google::protobuf::ServiceDescriptor* pserviceDesc = service->GetDescriptor();
std::string service_name = pserviceDesc->name();//service名字
int methodCnt = pserviceDesc->method_count();//service方法数量
const google::protobuf::MethodDescriptor* pmethonDest = pserviceDesc->method(i);//方法的抽象描述
std::string method_name = pmethonDest->name();//方法的名字
1.3、启动RPC服务
现在存在一个问题,那就是通过RPC实现的分布式框架会很多RPC服务提供者,调用方是如何知道每一个服务的调用地址呢,总不能列一个静态配置文件,一个一个的罗列出来吧,这样维护成本太高了,每次添加或删除一个service服务,调用方都要修改配置文件并重新启动。服务方的进行修改时还需要修改调用方,这样的设计实在太糟糕了。
为了解决上述问题,zookeeper就出现了,根据观察者模式的思想,每一个提供方在启动时都在zookeeper中注册信息,调用方的所有调用,只需要发送给zookeeper,至于调用方的地址在哪,调用方都无需关心,一切都由zookeeper负责。这样,调用方就只需要关注zookeeper地址即可,服务方的修改也不会影响到调用方。

连接zookeeper服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 连接zkserver
void ZKClient::Start()
{
std::string host = MprpcApplication::GetInstance().getConfig().Load("zookeeperip");
std::string port = MprpcApplication::GetInstance().getConfig().Load("zookeeperport");
std::string connstr = host + ":" + port;
m_zhandle = zookeeper_init(connstr.c_str(), global_watcher, 30000, nullptr, nullptr, 0);
if (nullptr == m_zhandle)
{
perror("zookeeper_init");
exit(EXIT_FAILURE);
}
sem_t sem;
sem_init(&sem, 0, 0);
zoo_set_context(m_zhandle, &sem);
// 阻塞等待global_watcher函数被调用,在ZooKeeper连接成功建立解除阻塞
sem_wait(&sem);
}往zookeeper中注册服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 注册服务
for(auto &sp : m_serviceMap)
{
std::string service_path = "/" + sp.first;
// 0表示永久性节点
zkclient.Create(service_path.c_str(), nullptr, 0);
for(auto &mp : sp.second.m_methodMap)
{
std::string method_path = service_path + "/" + mp.first;
char method_path_buffer[128] = {0};
snprintf(method_path_buffer, sizeof(method_path_buffer), "%s:%d",ip.c_str(), port);
// ZOO_EPHEMERAL 表示创建临时节点
zkclient.Create(method_path.c_str(), method_path_buffer, strlen(method_path_buffer),ZOO_EPHEMERAL);
}
}最后,启动网络服务完成最后的RPC启动工作
1
2server.start();
m_eventLoop.loop();
二、RPC提供者实现细节
前面提到,protoc会为每个service生成对应的C++类,包含虚函数,用户需要继承并实现这些函数。
首先我们可以看见,FriendServiceRpc是protoc给我们自动生成的service类,我们可以看到它继承了google ::protobuf::Service
- 我们可以先看看google ::protobuf::Service的实现,我们可以看见,这是一个抽象类
1 | class PROTOBUF_EXPORT Service { |
- protobuf生成的服务类
FriendServiceRpc,这个类通过继承google ::protobuf::Service并重写CallMethod方法调用我们重写的函数GetFriendList
1 | class FriendServiceRpc : public ::PROTOBUF_NAMESPACE_ID::Service { |
- 我们自定义的服务类,用于继承并重写FriendServiceRpc中的虚函数
1 | class FriendService : public friendservice::FriendServiceRpc |
从上面我们可以大致看出,RPC服务提供者处理就是在服务端请求到达时,首先会将父类指针google::protobuf::Service*指向子类对象,这个子类对象实际上就是上面的class FriendService : public friendservice::FriendServiceRpc,也就是我们自己实现的类,再通过FriendService的CallMethod方法(由于没有重写,实际上是父类的CallMethod方法)调用我们重写的方法,至于怎么确定是哪个函数,这个由protobuf内部负责的,具体逻辑就是我们们看见的FriendServiceRpc重写的CallMethod方法,这个CallMethod会调用我们重写好的函数。

实际上,protobuf知道我们要调用哪个函数,是需要我们传递一些参数的,我们可以看到CallMethod的函数声明
1 | virtual void CallMethod(const MethodDescriptor* method, |
- method:就是对应调用方法的描述,
const google::protobuf::MethodDescriptor* method,通过这个描述,FriendServiceRpc就能知道要调用哪个方法。 - controller:通过这个RpcController类,我们可以查询返回数据时是否发生错误,并获得相关的RPC的信息,如错误的信息。
- request:包含方法的参数信息
- response: 包含服务提供者返回的响应消息
- done: 用于发送数据给客户端
1 | //request |
首先,我们首先要根据protobuf的数据格式约定,提取出调用方的调用信息
1 | //解析服务描述信息 |
然后查看服务和方法是否存在
1 | auto it = m_serviceMap.find(service_name); |
初始化request的参数数据
1 | google::protobuf::Message* request = service->GetRequestPrototype(method).New(); |
最后调用CallMethod方法
1 | service->CallMethod(method, nullptr, request, response, done); |
具体代码实现:
1 | void RpcProvider::onMessage(const muduo::net::TcpConnectionPtr& conn, |
三、调用方配置
使用姿势
1 | // 初始化框架 |
四、RPC调用者实现细节
protobuf不就会生成服务提供者相关的类,同样还会实现调用者相关的类
与提供者不同的是,调用的创建的父类对象是继承自FriendServiceRpc,也就是提供者的那个父类,每一个服务提供者的类都会生成一个对应的Stub类,调用方通过这个Stub类就能与提供方实现数据间相互处理。
1 | class FriendServiceRpc_Stub : public FriendServiceRpc { |
实际上,FriendServiceRpc_Stub 类并没有直接实现 CallMethod 方法,而是通过一个更精妙的设计来实现 RPC 调用。当我们查看 FriendServiceRpc_Stub 的构造函数时,可以发现它需要一个 RpcChannel 对象作为参数。这个 RpcChannel 类才是真正包含 CallMethod 方法的地方。这样表示着,调用方的所有操作,最终都会通过经过这个RpcChannel类并通过其CallMethod方法发出。 而 RpcChannel 本身是一个抽象类,只定义了这一个纯虚方法,这正是 Protocol Buffers 框架的精髓所在 - 它提供了接口定义,但将具体的网络通信实现留给了开发者。通过继承 RpcChannel 并重写其 CallMethod 方法,我们可以实现序列化和反序列化逻辑,添加额外的元数据(如超时设置、重试策略等)等。
1 | class PROTOBUF_EXPORT RpcChannel { |
具体的执行过程如下:
- 客户端通过Stub类调用对应的RPC接口,在这个接口中调用RpcChannel的CallMethod方法
1 | stub.GetFriendList(&controller, &getFriendListRequest, &getFriendListResponse, nullptr); |
- RpcChannel的CallMethod方法实际上调用的是我们重写的方法,用于封装并发送的数据
1 | class MprpcChannel : public google::protobuf::RpcChannel |
总体来说,调用方只需要继承RpcChannel并重写其CallMethod方法,调用方只需要提供对应的参数,调用的方法就能,就能通过CallMethod方法实现自动处理发送并处理RPC请求与响应,其余交给框架处理。