目录
1.1.1.问题:unordered_map的第3个参数------哈希函数
在之前呢,我们已经写好了这里面每个单独的模块了,但是我们还没有将每个小的模块进行整合

那么在我们这一节里面,我们就会专门对这些写好的模块进行封装
- RPC客户端的封装
- RPC服务端的封装
也就是下面这2个东西
一.RPC客户端代码设计
1.1.成员变量设计
RPC客户端与RPC服务端是建立长连接还是短连接?
我们说的也就是这个连接

短连接模式
短连接的核心思想是"按需创建,用完即关"。
具体流程为:
- 当服务发现组件获取到目标服务的提供者地址后,客户端会立即实例化相应的通信客户端,并基于该地址建立网络连接,随后发起RPC调用。调用完成后,客户端主动断开连接并释放资源。
这种模式的优点是实现简单、资源释放及时,适合调用频率较低或对资源消耗敏感的场景;
缺点是频繁建立与断开连接会引入额外的网络开销与延迟。
实际上,这个看起来实现非常简单,但是请特别注意一个大难题:发起异步RPC请求的时候,如果客户端发送完RPC请求,但是在请求还没有到来的时候,我们自动关闭了这个客户端,那会造成很大的问题。
这个问题解决起来还是比较麻烦的,麻烦程度已经超过了下面这种长连接模式。
再者说我们等待请求到达的时候再去关闭服务器,那么我们这个异步请求还有啥意义,直接变同步请求了。
长连接模式
长连接则采用"连接复用"的设计。
- 客户端启动时会维护一个连接池,在第一次调用某服务时,会创建客户端连接对象并在调用结束后保留至连接池中,而非立即关闭。
- 后续对同一服务的调用可直接从池中获取已建立的连接,省去了重复建立连接的开销。这种方式显著提升了高频调用场景下的性能与效率。
关键挑战:连接池的维护
长连接模式的一个主要难点在于连接状态的管理。**当服务注册者下线或不可用时,客户端需要及时感知,并将对应的失效连接从连接池中移除,避免后续调用使用已不可用的连接。**常见的解决方案包括结合服务发现组件推送的服务变更通知,或在客户端侧引入心跳检测机制,动态淘汰失活连接,确保连接池中始终保持有效的可用连接。
那么,综合考虑,我们这里采用的是长连接的方案。
那么,我们这个RPC服务的成员变量就可以写出来了
只不过,我们需要考虑分情况讨论
- 启用服务注册-发现功能
- 不启用服务注册-发现功能
我们需要借助一个bool _enableDiscovery;来分情况进行处理
cpp
// RPC客户端类,提供完整的RPC调用功能,支持服务发现
class RpcClient
{
public:
......
bool _enableDiscovery; // 是否启用服务发现
DiscoveryClient::ptr _discovery_client; // 服务发现客户端
Requestor::ptr _requestor; // 请求器
RpcCaller::ptr _caller; // RPC调用器
Dispatcher::ptr _dispatcher; // Dispatcher调度器
BaseClient::ptr _rpc_client; // 固定的RPC客户端连接(用于未启用服务发现的情况)
std::mutex _mutex; // 互斥锁,保护连接池的线程安全
// 服务发现的客户端连接池,键为RPC服务器地址,值为对应的RPC客户端连接
//一个RPC服务器应该有一个RPC客户端连接着
std::unordered_map<Address, BaseClient::ptr, AddressHash> _rpc_clients;
};
1.1.1.问题:unordered_map的第3个参数------哈希函数
大家观察仔细一点,就会发现,这个unordered_map的尖括号里面居然有3个类型!!!!
我们之前用的都是键值对的形式(2种)
我们的
对于自定义类型(如 Address),编译器不知道如何计算哈希值,所以需要自定义哈希函数(AddressHash)
如果省略第三个参数,编译器会使用默认的 std::hash<Address>,这会导致编译错误,因为标准库没有为自定义类型预定义哈希函数。
std::unordered_map的哈希函数类型
实际上 std::unordered_map 最多有 5个 模板参数:
cpp
template<
class Key, // 键类型(Address)
class T, // 值类型(BaseClient::ptr)
class Hash = std::hash<Key>, // 哈希函数类型(AddressHash)
class KeyEqual = std::equal_to<Key>, // 键相等比较函数(默认)
class Allocator = std::allocator<std::pair<const Key, T>> // 分配器(默认)
> class unordered_map;
那么对于3个参数的std::unordered_map,第3个参数其实是哈希函数类型。
那么这个哈希函数又是干嘛的?
unordered_map 使用哈希函数将任意大小的"键"转换成一个固定大小的"哈希值"(通常是一个整数),然后根据这个哈希值来快速决定数据存储在哪个"桶"中,从而实现平均常数时间 O(1) 的查找、插入和删除。
假设我们有一个 unordered_map,它使用 % 7(对7取模)作为将哈希值映射到桶的方法。
cpp
键: "Alice" -> 哈希函数 -> 哈希值: 1234 -> 映射到桶: 1234 % 7 = 2
键: "Bob" -> 哈希函数 -> 哈希值: 5678 -> 映射到桶: 5678 % 7 = 0
键: "Charlie" -> 哈希函数 -> 哈希值: 9012 -> 映射到桶: 9012 % 7 = 5
每一步的细节:
-
第1步:计算哈希值
-
当你执行
map["Alice"] = 100;时,容器首先会获取键"Alice"。 -
它将键
"Alice"传递给哈希函数。 -
哈希函数返回一个
std::size_t类型的整数(如1234)。这个值就是该键的"数字指纹"。 -
要求 :对于两个相等的键(通过
operator==判断),它们的哈希值必须相等。反之不要求(哈希碰撞)。
-
-
第2步:映射到桶
-
得到哈希值(如
1234)后,容器需要决定将这个键值对放在内部的哪个存储位置(称为"桶",bucket)。 -
它通常会对哈希值进行一个简单的转换,最常见的是
hash_value % bucket_count(哈希值对桶数量取模)。 -
例如,如果当前有 7 个桶,
1234 % 7 = 2,那么这个键值对就会被分配到第2号桶。
-
-
第3步:处理冲突(哈希碰撞)
-
不同的键完全有可能计算出相同的哈希值(例如
"key1"和"key2"的哈希值对桶数量取模后都是 2),或者不同的哈希值取模后映射到同一个桶(如hash1=9,hash2=16,桶数量为7,则9%7=2,16%7=2)。这称为哈希碰撞。 -
unordered_map通过桶内链表(或类似结构) 解决冲突。同一个桶内的所有元素会被组织成一个链表(在C++标准中未指定具体结构,但主流实现多用单向链表)。 -
当插入或查找时,定位到桶之后,还需要在这个桶内的链表中进行线性搜索 ,使用键的
operator==来逐个比较,直到找到完全匹配的键。
-
如果说,我们的unordered_map只定义了两个参数,那么这个unordered_map就会将键传递默认的哈希函数去计算哈希值。
但是有一个问题,如果说我们定义的键是自定义类型,那么这个unordered_map的默认的哈希函数就计算不了这个哈希值了,就会报错,那么这个时候,就需要定义第3个参数了
我们这里unordered_map定义了3个参数,那么这个unordered_map就是使用我们传递进去的自定义哈希函数。
例子
当我们想用自定义的结构体作为 unordered_map 的键时,直接使用会报错:
cpp
#include <iostream>
#include <unordered_map>
#include <string>
// 自定义类型
struct Person {
std::string name;
int age;
Person(const std::string& n, int a) : name(n), age(a) {}
};
int main() {
// 这会编译错误!因为 Person 没有默认的哈希函数
std::unordered_map<Person, std::string> personMap;
return 0;
}

解决方案:提供自定义哈希函数
cpp
#include <iostream>
#include <unordered_map>
#include <string>
#include <functional>
// 自定义类型
struct Person {
std::string name;
int age;
Person(const std::string& n, int a) : name(n), age(a) {}
// 必须重载 == 运算符
bool operator==(const Person& other) const {
return name == other.name && age == other.age;
}
};
// 自定义哈希函数类
struct PersonHash {
std::size_t operator()(const Person& p) const {
// 组合 name 和 age 的哈希值
return std::hash<std::string>()(p.name) ^ (std::hash<int>()(p.age) << 1);
}
};
int main() {
// 使用自定义哈希函数作为第三个参数
std::unordered_map<Person, std::string, PersonHash> personMap;
// 现在可以正常使用了
personMap[Person("Alice", 25)] = "Engineer";
personMap[Person("Bob", 30)] = "Doctor";
// 查找
auto it = personMap.find(Person("Alice", 25));
if (it != personMap.end()) {
std::cout << "Alice's job: " << it->second << std::endl;
}
// 遍历
for (const auto& pair : personMap) {
std::cout << pair.first.name << " (" << pair.first.age << "): "
<< pair.second << std::endl;
}
return 0;
}

再看看std::pair类型的,因为这个std::pair类型内部已经重载了这个==,所以,我们只需要去提供一个自定义哈希函数类即可
cpp
#include <iostream>
#include <unordered_map>
#include <utility> // 包含 pair
// 自定义哈希函数(非常简单)
struct MyHash {
size_t operator()(const std::pair<int, int>& p) const {
// 最简单的哈希:把两个数拼接起来
return std::hash<int>()(p.first) + std::hash<int>()(p.second);
}
};
int main() {
// 创建一个使用 pair 作为键的 unordered_map
// 三个参数:键类型,值类型,哈希函数类型
std::unordered_map<std::pair<int, int>, std::string, MyHash> myMap;
// 添加一些数据
myMap[{1, 2}] = "第一项";
myMap[{3, 4}] = "第二项";
myMap[{5, 6}] = "第三项";
// 访问数据
std::cout << myMap[{1, 2}] << std::endl; // 输出:第一项
std::cout << myMap[{3, 4}] << std::endl; // 输出:第二项
// 检查是否存在
if (myMap.find({5, 6}) != myMap.end()) {
std::cout << "找到 (5,6):" << myMap[{5, 6}] << std::endl; // 输出:找到 (5,6):第三项
}
return 0;
}

好了,我猜你们应该都会了,那么现在,我们只需要去看看我们现在的情况
cpp
// 服务发现的客户端连接池,键为服务注册者地址,值为对应的客户端连接
std::unordered_map<Address, BaseClient::ptr, AddressHash> _rpc_clients;
这个Address其实是一个std::pair类型
cpp
// 定义地址类型,pair<string, int> 表示 IP 地址和端口
typedef std::pair<std::string, int> Address;
因为pair类型里面已经重载了这个==号,那么我们现在只需要去提供这个自定义哈希函数即可。
我们先看一个简单的例子
现在,我们就来编写我们的哈希函数对象
cpp
// 地址哈希函数对象,用于unordered_map
struct AddressHash
{
size_t operator()(const Address &host) const
{
std::string addr = host.first + std::to_string(host.second);
return std::hash<std::string>{}(addr);
}
};
那么对于我们这个哈希函数,我们可以写一个简单的例子来看看效果
cpp
#include <iostream>
#include <unordered_map>
#include <string>
typedef std::pair<std::string, int> Address;
struct AddressHash
{
size_t operator()(const Address &host) const
{
std::string addr = host.first + std::to_string(host.second);
return std::hash<std::string>{}(addr);
}
};
int main() {
// 创建服务器映射表
std::unordered_map<Address, std::string, AddressHash> servers;
// 添加一些服务器
servers.emplace(std::make_pair("127.0.0.1", 8080), "本地应用");
servers.emplace(std::make_pair("8.8.8.8", 53), "DNS服务器");
// 通过键直接访问(这会自动创建条目,如果不存在的话)
servers[{"8.8.4.4", 53}] = "备用DNS";
// 检查并打印
std::cout << "我们有 " << servers.size() << " 个服务器:" << std::endl;
for (const auto& [address, name] : servers) {
std::cout << name << " 在 " << address.first << ":" << address.second << std::endl;
}
return 0;
}

可以说是很不错了!!
1.1.2.连接池管理接口
那么对于哈希表,我们需要
- 一个插入接口
- 一个删除接口
- 一个查询接口
也就是下面这3个
cpp
// 将客户端连接放入连接池
// @param host: RPC服务端的IP地址+端口号
// @param client: 客户端连接指针
void putClient(const Address &host, BaseClient::ptr &client)
{
std::unique_lock<std::mutex> lock(_mutex); // 加锁保证线程安全
_rpc_clients.insert(std::make_pair(host, client));//往连接池里面插入一个键值对<RPC服务端的IP地址+端口号,对应的RPC客户端连接>
}
// 从连接池中删除客户端连接
// @param host: RPC服务端的IP地址+端口号
void delClient(const Address &host)
{
std::unique_lock<std::mutex> lock(_mutex); // 加锁保证线程安全
_rpc_clients.erase(host);//根据RPC服务端的IP地址+端口号去连接池里面删除对应的RPC客户端连接
}
// 根据RPC服务器的地址信息获取客户端连接
// @param host: 服务注册者的地址
// @return: 对应的客户端连接指针,如果不存在则返回空指针
BaseClient::ptr getClient(const Address &host)
{
std::unique_lock<std::mutex> lock(_mutex); // 加锁保证线程安全
auto it = _rpc_clients.find(host);//根据RPC服务器的IP地址+端口号来去连接池里面查看有没有与这个服务器的RPC客户端的连接
if (it == _rpc_clients.end())//没有找到,说明我们连接池里面没有连接这个RPC服务器的连接
{
return BaseClient::ptr(); // 未找到,返回空指针
}
return it->second; // 找到,返回对应的客户端连接
}
还是很简单的
接下来这个函数就是客户端里面比较重要的接口了,
根据服务方法名称来返回与提供这个RPC服务方法的RPC服务器的连接
但是我们连接池里面存储的是 <RPC服务端地址信息,与这个RPC服务器的客户端连接>
我们怎么知道哪个RPC服务器会提供这个RPC服务方法?
这个时候就需要借助我们RPC客户端类内置的服务发现者客户端,先让这个服务发现者客户端去服务中心进行服务发现,然后服务发现者再返回所有提供者的其中一个RPC服务器的IP地址+端口号即可,然后我们再去连接池 <RPC服务端地址信息,与这个RPC服务器对应的客户端连接>里面看看这个RPC服务端地址信息有没有与这个RPC服务器对应的客户端连接
- 如果有,直接取出这个RPC客户端连接来用
- 如果没有,就创建一个新的RPC客户端连接来用,并且将这个新的 <RPC服务端地址信息,与这个RPC服务器对应的客户端连接>存储到连接池里面去
cpp
// 根据方法名称获取客户端连接
// @param method: 方法名称
// @return: 对应的客户端连接指针,如果获取失败则返回空指针
BaseClient::ptr getClient(const std::string method)
{
BaseClient::ptr client;
if (_enableDiscovery)//启用了服务发现
{
// 1. 通过服务发现,获取服务注册者地址信息
Address host;
//_discovery_client是服务发现客户端
bool ret = _discovery_client->serviceDiscovery(method, host);//服务发现
//去服务中心发现指定服务名称的注册者并在其中选择一个RPC服务器的地址返回给host
if (ret == false)//服务发现失败
{
ELOG("当前 %s 服务,没有找到服务注册者!", method.c_str());
return BaseClient::ptr();
}
//现在host里面就已经有了服务发现得到的那个RPC服务器的IP地址+端口号
// 2. 查看这个RPC服务器在连接池是否已有一个RPC客户端连接连接着这个RPC服务器,有则直接使用,没有则创建
client = getClient(host);
if (client.get() == nullptr)
{ // 没有找到已实例化的客户端,则创建一个RPC客户端去连接这个RPC服务器
client = newClient(host);
}
}
else//没有启用服务发现
{
client = _rpc_client; // 未启用服务发现,使用固定的RPC客户端连接这个RPC服务器
}
return client;
}
// 创建新的客户端连接
// @param host: RPC服务器的地址(IP和端口)
// @return: 新创建的客户端连接指针
BaseClient::ptr newClient(const Address &host)
{
auto message_cb = std::bind(&Dispatcher::onMessage, _dispatcher.get(),
std::placeholders::_1, std::placeholders::_2);
auto client = ClientFactory::create(host.first, host.second); // 创建RPC客户端连接,传递进去RPC服务器的IP地址+端口号
client->setMessageCallback(message_cb); // 设置消息回调
client->connect(); // 连接RPC服务器
putClient(host, client); // 将这个RPC客户端连接放入连接池
return client;
}
1.2.构造函数
那么对于构造函数,我们需要分情况讨论
- 启用服务注册-发现功能
- 不启用服务注册-发现功能
对于这2种不同的功能,我们的构造函数需要完成不同的功能。
-
启用服务注册-发现功能:
此时构造函数需要创建并初始化一个服务发现客户端 (DiscoveryClient),该客户端会连接至服务中心(传入的 ip 和 port是服务中心的了),并注册服务下线时的回调函数,以便在某个服务不可用时及时清理对应的客户端连接。此时并不直接建立到具体 RPC 服务器的连接,而是等到实际调用时通过服务发现机制动态获取可用服务地址再建立连接。
-
不启用服务注册-发现功能:
此时构造函数会直接根据传入的 RPC 服务器地址(传入的 ip 和 port是RPC 服务器的了)创建一个 RPC 客户端连接,并设置消息回调函数,将接收到的消息交给调度器(Dispatcher)处理。然后立即发起连接,后续所有的 RPC 调用都将通过这个固定的连接进行。
通过这样的设计,构造函数可以根据配置灵活地适应有服务中心 和直连服务器两种不同的使用场景。
cpp
// 构造函数,初始化RPC客户端
// @param enableDiscovery: 是否启用服务发现功能
// @param ip: 服务器IP地址(可能是服务中心或RPC服务器的)
// @param port: 服务器端口号(可能是服务中心或RPC服务器的)
RpcClient(bool enableDiscovery, const std::string &ip, int port) : _enableDiscovery(enableDiscovery), // 是否启用服务发现
_requestor(std::make_shared<Requestor>()), // 创建请求器实例
_dispatcher(std::make_shared<Dispatcher>()), // 创建调度器实例
_caller(std::make_shared<jsonRpc::client::RpcCaller>(_requestor))
{ // 创建RPC调用器实例
// 设置RPC响应回调函数,将响应交给请求器处理
auto rsp_cb = std::bind(&client::Requestor::onResponse, _requestor.get(),
std::placeholders::_1, std::placeholders::_2);
_dispatcher->registerHandler<BaseMessage>(MType::RSP_RPC, rsp_cb);//如果收到的响应类型是RPC响应,那么就交给Requestor::onResponse进行处理
// 如果启用了服务发现,则创建服务发现者客户端
// 如果没有启用服务发现,则直接创建RPC客户端连接
if (_enableDiscovery)//启用了服务发现
{
auto offline_cb = std::bind(&RpcClient::delClient, this, std::placeholders::_1);//注册服务下线时的回调函数------RpcClient::delClient
//实际上服务下线也就是某个服务注册者和服务中心断开连接了,那么代表与这个服务注册者关联的那个RPC服务器不会再提供RPC服务了,
//那么我们的连接池也没有必要保持和这个RPC服务器的连接了
//在RpcClient::delClient里面,我们会完成将这个服务下线的处理------从连接池里面删除对应客户端连接
_discovery_client = std::make_shared<DiscoveryClient>(ip, port, offline_cb);//注意这里传递给服务发现者客户端的IP地址+端口号是服务中心的
//构造一个服务发现者客户端,服务发现者客户端会自动连接服务中心
}
else//没有启用服务发现
{
auto message_cb = std::bind(&Dispatcher::onMessage, _dispatcher.get(),
std::placeholders::_1, std::placeholders::_2);
_rpc_client = ClientFactory::create(ip, port); // 创建客户端连接,传递进去RPC服务器的IP地址+端口号(注意这里传递进去的可不是服务中心的IP地址+端口号)
_rpc_client->setMessageCallback(message_cb); // 设置消息回调
_rpc_client->connect(); // 连接RPC服务器
//接下来就直接进行RPC调用了
}
}
1.3.同步/异步/回调请求
有了上面那些接口,我们现在实现这么几个请求其实是很简单的。
cpp
// 同步RPC调用方法
// @param method: 要调用的方法名称
// @param params: JSON格式的参数
// @param result: 输出参数,接收返回的JSON结果
// @return: 调用成功返回true,失败返回false
bool call(const std::string &method, const Json::Value ¶ms, Json::Value &result)
{
// 获取服务注册者客户端连接
BaseClient::ptr client = getClient(method);//根据需要调用的方法来获取 与能提供这个服务的RPC服务器的所对应 的RPC客户端连接
if (client.get() == nullptr)//获取失败
{
return false;
}
// 通过RPC客户端连接,发送RPC请求
return _caller->call(client->connection(), method, params, result);
}
// 异步RPC调用方法(返回future对象)
// @param method: 要调用的方法名称
// @param params: JSON格式的参数
// @param result: 输出参数,接收异步响应的future对象
// @return: 调用成功返回true,失败返回false
bool call(const std::string &method, const Json::Value ¶ms, RpcCaller::JsonAsyncResponse &result)
{
BaseClient::ptr client = getClient(method);
if (client.get() == nullptr)
{
return false;
}
// 通过客户端连接,发送RPC请求
return _caller->call(client->connection(), method, params, result);
}
// 回调式RPC调用方法
// @param method: 要调用的方法名称
// @param params: JSON格式的参数
// @param cb: 结果回调函数
// @return: 调用成功返回true,失败返回false
bool call(const std::string &method, const Json::Value ¶ms, const RpcCaller::JsonResponseCallback &cb)
{
BaseClient::ptr client = getClient(method);
if (client.get() == nullptr)
{
return false;
}
// 通过客户端连接,发送RPC请求
return _caller->call(client->connection(), method, params, cb);
}
1.4.完整代码
现在我们就看看我们RPC客户端的完整代码
cpp
// RPC客户端类,提供完整的RPC调用功能,支持服务发现
class RpcClient
{
public:
// 智能指针类型别名
using ptr = std::shared_ptr<RpcClient>;
// 构造函数,初始化RPC客户端
// @param enableDiscovery: 是否启用服务发现功能
// @param ip: 服务器IP地址(可能是服务中心或RPC服务器的)
// @param port: 服务器端口号(可能是服务中心或RPC服务器的)
RpcClient(bool enableDiscovery, const std::string &ip, int port) : _enableDiscovery(enableDiscovery), // 是否启用服务发现
_requestor(std::make_shared<Requestor>()), // 创建请求器实例
_dispatcher(std::make_shared<Dispatcher>()), // 创建调度器实例
_caller(std::make_shared<jsonRpc::client::RpcCaller>(_requestor))
{ // 创建RPC调用器实例
// 设置RPC响应回调函数,将响应交给请求器处理
auto rsp_cb = std::bind(&client::Requestor::onResponse, _requestor.get(),
std::placeholders::_1, std::placeholders::_2);
_dispatcher->registerHandler<BaseMessage>(MType::RSP_RPC, rsp_cb);//如果收到的响应类型是RPC响应,那么就交给Requestor::onResponse进行处理
// 如果启用了服务发现,则创建服务发现者客户端
// 如果没有启用服务发现,则直接创建RPC客户端连接
if (_enableDiscovery)//启用了服务发现
{
auto offline_cb = std::bind(&RpcClient::delClient, this, std::placeholders::_1);//注册服务下线时的回调函数------RpcClient::delClient
//实际上服务下线也就是某个服务注册者和服务中心断开连接了,那么代表与这个服务注册者关联的那个RPC服务器不会再提供RPC服务了,
//那么我们的连接池也没有必要保持和这个RPC服务器的连接了
//在RpcClient::delClient里面,我们会完成将这个服务下线的处理------从连接池里面删除对应客户端连接
_discovery_client = std::make_shared<DiscoveryClient>(ip, port, offline_cb);//注意这里传递给服务发现者客户端的IP地址+端口号是服务中心的
//构造一个服务发现者客户端,服务发现者客户端会自动连接服务中心
}
else//没有启用服务发现
{
auto message_cb = std::bind(&Dispatcher::onMessage, _dispatcher.get(),
std::placeholders::_1, std::placeholders::_2);
_rpc_client = ClientFactory::create(ip, port); // 创建客户端连接,传递进去RPC服务器的IP地址+端口号(注意这里传递进去的可不是服务中心的IP地址+端口号)
_rpc_client->setMessageCallback(message_cb); // 设置消息回调
_rpc_client->connect(); // 连接RPC服务器
//接下来就直接进行RPC调用了
}
}
// 同步RPC调用方法
// @param method: 要调用的方法名称
// @param params: JSON格式的参数
// @param result: 输出参数,接收返回的JSON结果
// @return: 调用成功返回true,失败返回false
bool call(const std::string &method, const Json::Value ¶ms, Json::Value &result)
{
// 获取服务注册者客户端连接
BaseClient::ptr client = getClient(method);//根据需要调用的方法来获取 与能提供这个服务的RPC服务器的所对应 的RPC客户端连接
if (client.get() == nullptr)//获取失败
{
return false;
}
// 通过RPC客户端连接,发送RPC请求
return _caller->call(client->connection(), method, params, result);
}
// 异步RPC调用方法(返回future对象)
// @param method: 要调用的方法名称
// @param params: JSON格式的参数
// @param result: 输出参数,接收异步响应的future对象
// @return: 调用成功返回true,失败返回false
bool call(const std::string &method, const Json::Value ¶ms, RpcCaller::JsonAsyncResponse &result)
{
BaseClient::ptr client = getClient(method);
if (client.get() == nullptr)
{
return false;
}
// 通过客户端连接,发送RPC请求
return _caller->call(client->connection(), method, params, result);
}
// 回调式RPC调用方法
// @param method: 要调用的方法名称
// @param params: JSON格式的参数
// @param cb: 结果回调函数
// @return: 调用成功返回true,失败返回false
bool call(const std::string &method, const Json::Value ¶ms, const RpcCaller::JsonResponseCallback &cb)
{
BaseClient::ptr client = getClient(method);
if (client.get() == nullptr)
{
return false;
}
// 通过客户端连接,发送RPC请求
return _caller->call(client->connection(), method, params, cb);
}
private:
// 创建新的客户端连接
// @param host: RPC服务器的地址(IP和端口)
// @return: 新创建的客户端连接指针
BaseClient::ptr newClient(const Address &host)
{
auto message_cb = std::bind(&Dispatcher::onMessage, _dispatcher.get(),
std::placeholders::_1, std::placeholders::_2);
auto client = ClientFactory::create(host.first, host.second); // 创建RPC客户端连接,传递进去RPC服务器的IP地址+端口号
client->setMessageCallback(message_cb); // 设置消息回调
client->connect(); // 连接RPC服务器
putClient(host, client); // 将这个RPC客户端连接放入连接池
return client;
}
// 根据RPC服务器的地址信息获取客户端连接
// @param host: 服务注册者的地址
// @return: 对应的客户端连接指针,如果不存在则返回空指针
BaseClient::ptr getClient(const Address &host)
{
std::unique_lock<std::mutex> lock(_mutex); // 加锁保证线程安全
auto it = _rpc_clients.find(host);//根据RPC服务器的IP地址+端口号来去连接池里面查看有没有与这个服务器的RPC客户端的连接
if (it == _rpc_clients.end())//没有找到,说明我们连接池里面没有连接这个RPC服务器的连接
{
return BaseClient::ptr(); // 未找到,返回空指针
}
return it->second; // 找到,返回对应的客户端连接
}
// 根据方法名称获取客户端连接
// @param method: 方法名称
// @return: 对应的客户端连接指针,如果获取失败则返回空指针
BaseClient::ptr getClient(const std::string method)
{
BaseClient::ptr client;
if (_enableDiscovery)//启用了服务发现
{
// 1. 通过服务发现,获取服务注册者地址信息
Address host;
//_discovery_client是服务发现客户端
bool ret = _discovery_client->serviceDiscovery(method, host);//服务发现
//去服务中心发现指定服务名称的注册者并在其中选择一个RPC服务器的地址返回给host
if (ret == false)//服务发现失败
{
ELOG("当前 %s 服务,没有找到服务注册者!", method.c_str());
return BaseClient::ptr();
}
//现在host里面就已经有了服务发现得到的那个RPC服务器的IP地址+端口号
// 2. 查看这个RPC服务器在连接池是否已有一个RPC客户端连接连接着这个RPC服务器,有则直接使用,没有则创建
client = getClient(host);
if (client.get() == nullptr)
{ // 没有找到已实例化的客户端,则创建一个RPC客户端去连接这个RPC服务器
client = newClient(host);
}
}
else//没有启用服务发现
{
client = _rpc_client; // 未启用服务发现,使用固定的RPC客户端连接这个RPC服务器
}
return client;
}
// 将客户端连接放入连接池
// @param host: RPC服务端的IP地址+端口号
// @param client: 客户端连接指针
void putClient(const Address &host, BaseClient::ptr &client)
{
std::unique_lock<std::mutex> lock(_mutex); // 加锁保证线程安全
_rpc_clients.insert(std::make_pair(host, client));//往连接池里面插入一个键值对<RPC服务端的IP地址+端口号,对应的RPC客户端连接>
}
// 从连接池中删除客户端连接
// @param host: RPC服务端的IP地址+端口号
void delClient(const Address &host)
{
std::unique_lock<std::mutex> lock(_mutex); // 加锁保证线程安全
_rpc_clients.erase(host);//根据RPC服务端的IP地址+端口号去连接池里面删除对应的RPC客户端连接
}
private:
// 地址哈希函数对象,用于unordered_map
struct AddressHash
{
size_t operator()(const Address &host) const
{
std::string addr = host.first + std::to_string(host.second);
return std::hash<std::string>{}(addr);
}
};
bool _enableDiscovery; // 是否启用服务发现
DiscoveryClient::ptr _discovery_client; // 服务发现客户端
Requestor::ptr _requestor; // 请求器
RpcCaller::ptr _caller; // RPC调用器
Dispatcher::ptr _dispatcher; // Dispatcher调度器
BaseClient::ptr _rpc_client; // 固定的RPC客户端连接(用于未启用服务发现的情况)
std::mutex _mutex; // 互斥锁,保护连接池的线程安全
// 服务发现的客户端连接池,键为RPC服务器地址,值为对应的RPC客户端连接
//一个RPC服务器应该有一个RPC客户端连接着
std::unordered_map<Address, BaseClient::ptr, AddressHash> _rpc_clients;
};
二.RPC服务端代码设计
这个模块的设计,其实很简单,我们就没有什么好说的,直接看代码,肯定能明白
cpp
// RPC服务端类 - 提供具体的RPC服务
class RpcServer
{
public:
using ptr = std::shared_ptr<RpcServer>;
// 构造函数
// access_addr: RPC服务对外提供服务的地址(IP+端口),客户端调用时连接这个地址
// enableRegistry: 是否启用服务注册功能
// registry_server_addr: 服务中心服务器的地址
RpcServer(const Address &access_addr,
bool enableRegistry = false,
const Address ®istry_server_addr = Address()) : _enableRegistry(enableRegistry), // 是否启用服务注册功能
_access_addr(access_addr), // 保存RPC服务自身地址
_router(std::make_shared<bitrpc::server::RpcRouter>()), // 创建RPC路由器
_dispatcher(std::make_shared<bitrpc::Dispatcher>()) // 创建消息分发器
{
// 如果启用了服务注册功能,创建服务中心客户端
if (enableRegistry)
{
_reg_client = std::make_shared<client::RegistryClient>(
registry_server_addr.first, registry_server_addr.second);
}
// 注册RPC请求处理器RpcRouter - 当收到RPC调用请求时,由RpcRouter模块进行处理
auto rpc_cb = std::bind(&RpcRouter::onRpcRequest, _router.get(),
std::placeholders::_1, std::placeholders::_2); // 针对具体类型调用对应的处理函数
_dispatcher->registerHandler<jsonRpc::RpcRequest>(jsonRpc::MType::REQ_RPC, rpc_cb); // 收到REQ_RPC请求,就调用RpcRouter::onRpcRequest来处理
// 我们在这里往Dispatcher里面注册了如果jsonRpc::RpcRequest类型的请求,就调用RpcRouter::onRpcRequest来进行处理
// 创建TCP服务器,监听RPC服务端口
_server = bitrpc::ServerFactory::create(access_addr.second);
// 设置消息回调 - 当服务器收到消息时,交给Dispatche处理
auto message_cb = std::bind(&jsonRpc::Dispatcher::onMessage, _dispatcher.get(),
std::placeholders::_1, std::placeholders::_2);
_server->setMessageCallback(message_cb);
// 底层使用的Muduo网络库,当有消息到达服务器,服务器会自动调用onMessage函数,
// 那么我们在onMessage函数调用了Dispatcher::onMessage函数,将消息全部交给Dispatcher模块来处理
}
// 注册RPC服务方法
// service: 服务描述对象,包含方法名、参数、处理函数等信息
void registerMethod(const ServiceDescribe::ptr &service) // 注册一个RPC服务
{
// 传递进来一个RPC服务描述类
// 如果启用了服务注册,向服务中心注册此服务
if (_enableRegistry)
{
_reg_client->registryMethod(service->method(), _access_addr);//本质就是往这个服务中心发送服务注册请求
}
// 无论也没有启用服务注册,我们本地都需要进行保存
// 在本地路由器RpcRouter中注册服务方法
_router->registerMethod(service);
}
// 启动RPC服务器
void start()
{
_server->start();
}
private:
bool _enableRegistry; // 是否启用服务注册功能
Address _access_addr; // RPC服务对外地址(IP+端口)
client::RegistryClient::ptr _reg_client; // 服务中心客户端
RpcRouter::ptr _router; // RPC路由器
Dispatcher::ptr _dispatcher; // 消息分发器
BaseServer::ptr _server; // 网络服务器
};
三.测试
3.1.不启用注册-发现模块
test_server.cpp
cpp
#include "../../common/detail.hpp"
#include "../../server/rpc_server.hpp"
// 定义远程方法"Add"的处理函数
void Add(const Json::Value &req, Json::Value &rsp) {
// 从请求JSON中提取参数num1和num2
int num1 = req["num1"].asInt();
int num2 = req["num2"].asInt();
// 计算两个数的和,并存入响应JSON
rsp = num1 + num2;
}
int main()
{
// ========== 创建方法描述工厂并配置方法 ==========
// 创建方法描述工厂对象(智能指针管理)
std::unique_ptr<jsonRpc::server::SDescribeFactory> desc_factory(new jsonRpc::server::SDescribeFactory());
// 配置远程方法的基本信息
desc_factory->setMethodName("Add"); // 设置方法名为"Add"
desc_factory->setParamsDesc("num1", jsonRpc::server::VType::INTEGRAL); // 设置第一个参数为整数类型
desc_factory->setParamsDesc("num2", jsonRpc::server::VType::INTEGRAL); // 设置第二个参数为整数类型
desc_factory->setReturnType(jsonRpc::server::VType::INTEGRAL); // 设置返回类型为整数
desc_factory->setCallback(Add); // 设置处理方法为上面定义的Add函数
// ========== 创建RPC服务器并启动 ==========
// 创建RPC服务器,监听127.0.0.1:8080 , false代表不启用服务发现注册模块
jsonRpc::server::RpcServer server(jsonRpc::Address("127.0.0.1", 8080),false);
// 注册远程方法到服务器
server.registerMethod(desc_factory->build());
// 启动服务器(开始监听客户端请求)
server.start();
// 注意:server.start()可能会阻塞,直到服务器停止
return 0; // 程序结束
}
test_client.cpp
cpp
// 包含必要的头文件
#include "../../common/detail.hpp"
#include "../../client/rpc_client.hpp"
// RPC调用结果的回调函数
void callback(const Json::Value &result) {
ILOG("callback result: %d", result.asInt()); // 打印回调结果
}
int main()
{
//创建RPC客户端,false代表不启用服务发现注册模块
jsonRpc::client::RpcClient client(false, "127.0.0.1", 8080);
// ========== 同步调用示例 ==========
Json::Value params, result; // 创建JSON对象用于存储参数和结果
// 设置请求参数
params["num1"] = 11;
params["num2"] = 22;
// 同步调用远程方法"Add",将结果存储在result中
bool ret = client.call("Add", params, result);
// 检查调用是否成功
if (ret != false) {
ILOG("result: %d", result.asInt()); // 打印同步调用结果
}
// ========== 异步调用示例(使用future) ==========
jsonRpc::client::RpcCaller::JsonAsyncResponse res_future; // 异步响应对象
// 设置新的请求参数
params["num1"] = 33;
params["num2"] = 44;
// 异步调用远程方法"Add",结果通过future获取
ret = client.call("Add", params, res_future);
// 检查调用是否成功
if (ret != false) {
result = res_future.get(); // 阻塞等待并获取异步结果
ILOG("result: %d", result.asInt()); // 打印异步调用结果
}
// ========== 异步调用示例(使用回调函数) ==========
params["num1"] = 55;
params["num2"] = 66;
// 异步调用远程方法"Add",指定回调函数callback
ret = client.call("Add", params, callback);
DLOG("-------\n"); // 输出分隔线
// 主线程等待1秒,确保异步回调有机会执行
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0; // 程序结束
}
makefile
bash
CFLAG= -I ../../../build/release-install-cpp11/include/
LFLAG= -L ../../../build/release-install-cpp11/lib -lmuduo_net -lmuduo_base -pthread -ljsoncpp
all : client server
server : test_server.cpp
g++ -g $(CFLAG) $^ -o $@ $(LFLAG)
client : test_client.cpp
g++ -g $(CFLAG) $^ -o $@ $(LFLAG)
.PHONY: clean
clean:
rm -f server client
那么接下来我们就开始我们的测试




可以看到,完美运行
3.2.启用注册-发现模块
rpc_server.cpp
cpp
// 包含必要的头文件
#include "../../common/detail.hpp" // 公共定义和工具函数
#include "../../server/rpc_server.hpp" // RPC服务器类
// 定义远程方法"Add"的处理函数
// req参数:客户端请求的JSON数据,包含参数num1和num2
// rsp参数:用于返回给客户端的响应JSON数据
void Add(const Json::Value &req, Json::Value &rsp) {
// 从请求JSON中提取参数num1和num2的值并转换为整数
int num1 = req["num1"].asInt();
int num2 = req["num2"].asInt();
// 计算两个数的和,并将结果存入响应JSON
rsp = num1 + num2;
}
int main()
{
// ========== 创建方法描述工厂并配置远程方法 ==========
// 使用智能指针创建方法描述工厂对象,避免内存泄漏
std::unique_ptr<jsonRpc::server::SDescribeFactory> desc_factory(new jsonRpc::server::SDescribeFactory());
// 配置远程方法的基本信息:
desc_factory->setMethodName("Add"); // 设置方法名为"Add"
desc_factory->setParamsDesc("num1", jsonRpc::server::VType::INTEGRAL); // 设置第一个参数为整数类型
desc_factory->setParamsDesc("num2", jsonRpc::server::VType::INTEGRAL); // 设置第二个参数为整数类型
desc_factory->setReturnType(jsonRpc::server::VType::INTEGRAL); // 设置返回类型为整数
desc_factory->setCallback(Add); // 设置处理方法为上面定义的Add函数
// ========== 创建RPC服务器并启动 ==========
// 创建RPC服务器对象,参数说明:
// 第一个参数:服务地址(127.0.0.1:8080)-RPC服务器对外提供服务的地址
// 第二个参数:true-启用服务注册-发现模块,服务启动时会自动注册到注册中心
// 第三个参数:注册中心地址(127.0.0.1:8888)-服务注册中心的地址
jsonRpc::server::RpcServer server(jsonRpc::Address("127.0.0.1", 8080),
true,
jsonRpc::Address("127.0.0.1", 8888));
// 将配置好的远程方法注册到服务器
server.registerMethod(desc_factory->build());
// 启动服务器,开始监听客户端请求
// 这个方法会阻塞,直到服务器被停止
server.start();
return 0; // 程序正常结束
}
rpc_client.cpp
cpp
// 包含必要的头文件
#include "../../common/detail.hpp" // 公共定义和工具函数
#include "../../client/rpc_client.hpp" // RPC客户端类
// RPC调用结果的回调函数
// 当异步调用完成时,这个函数会被自动调用
// result参数:远程方法返回的JSON格式结果
void callback(const Json::Value &result) {
// 打印回调结果(使用ILOG信息级别日志)
ILOG("callback result: %d", result.asInt());
}
int main()
{
// ========== 创建RPC客户端对象 ==========
// 参数说明:
// 第一个参数:true - 启用服务注册-发现功能,客户端将从注册中心获取服务地址
// 第二个参数:"127.0.0.1" - 注册中心的IP地址
// 第三个参数:8888 - 注册中心的端口号
// 注意:当启用服务注册-发现功能时,这里传递的是服务中心的地址,而不是具体RPC服务器的地址
jsonRpc::client::RpcClient client(true, "127.0.0.1", 8888);
// ========== 同步调用示例 ==========
// 创建JSON对象用于存储请求参数和接收结果
Json::Value params, result;
// 设置请求参数
params["num1"] = 11;
params["num2"] = 22;
// 同步调用远程方法"Add"
// 参数说明:
// 第一个参数:"Add" - 要调用的远程方法名
// 第二个参数:params - 包含参数的JSON对象
// 第三个参数:result - 用于接收返回结果的JSON对象
// 返回值:bool类型,表示调用是否成功发起(注意:不是远程方法执行是否成功)
bool ret = client.call("Add", params, result);
// 检查调用是否成功发起
if (ret != false) {
// 打印同步调用结果
ILOG("result: %d", result.asInt());
}
// ========== 异步调用示例(使用future) ==========
// 创建异步响应对象,用于获取异步调用的结果
jsonRpc::client::RpcCaller::JsonAsyncResponse res_future;
// 设置新的请求参数
params["num1"] = 33;
params["num2"] = 44;
// 异步调用远程方法"Add"(使用future方式)
// 参数说明:
// 第一个参数:"Add" - 要调用的远程方法名
// 第二个参数:params - 包含参数的JSON对象
// 第三个参数:res_future - 异步响应对象,用于后续获取结果
// 返回值:bool类型,表示调用是否成功发起
ret = client.call("Add", params, res_future);
// 检查调用是否成功发起
if (ret != false) {
// 通过future获取异步调用的结果(这里会阻塞直到结果返回)
result = res_future.get();
// 打印异步调用结果
ILOG("result: %d", result.asInt());
}
// ========== 异步调用示例(使用回调函数) ==========
// 设置新的请求参数
params["num1"] = 55;
params["num2"] = 66;
// 异步调用远程方法"Add"(使用回调函数方式)
// 参数说明:
// 第一个参数:"Add" - 要调用的远程方法名
// 第二个参数:params - 包含参数的JSON对象
// 第三个参数:callback - 回调函数,当远程调用完成时自动调用
// 返回值:bool类型,表示调用是否成功发起
ret = client.call("Add", params, callback);
// 输出分隔线(使用DLOG调试级别日志)
DLOG("-------\n");
// 主线程等待1秒,确保异步回调有机会执行完成
// 注意:在实际应用中,通常不会这样简单等待,而是通过事件循环等方式管理
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0; // 程序结束
}
serviceCentor.cpp
cpp
#include "../../common/detail.hpp"
#include "../../server/rpc_server.hpp"
int main()
{
jsonRpc::server::ServiceCenter serverCentor(8888);//服务中心对外提供服务的端口号
serverCentor.start();//启动服务中心
return 0;
}
makefile
bash
CFLAG= -I ../../../build/release-install-cpp11/include/
LFLAG= -L ../../../build/release-install-cpp11/lib -lmuduo_net -lmuduo_base -pthread -ljsoncpp
all : service_center rpc_server rpc_client
service_center : serviceCenter.cpp
g++ -g $(CFLAG) $^ -o $@ $(LFLAG)
rpc_server : rpc_server.cpp
g++ -g $(CFLAG) $^ -o $@ $(LFLAG)
rpc_client : rpc_client.cpp
g++ -g $(CFLAG) $^ -o $@ $(LFLAG)
.PHONY: clean
clean:
rm -f service_center rpc_server rpc_client

那么现在,测试开始
我们先启动这个服务中心

我们启动我们的RPC服务端


可以看到,我们RPC服务器一启动就去连接了我们的服务中心,往里面注册好了Add方法
现在我们启动我们的客户端

我们服务中心显示我们的客户端进行了服务发现

我们的服务端显示,我们的客户端发来了3次请求

非常的完美。
接下来我们终止我们的RPC服务端,再次启动一个新的RPC客户端,那么就是下面这个情况

我们的服务注册发现模块可以说是写的非常的好!!!!