Zookeeper 在 RPC 中的核心作用
在 RPC 分布式通信中,Zookeeper 主要解决以下核心问题:
服务注册:服务提供者启动时,将自己的地址(IP + 端口)、服务名称等信息注册到 Zookeeper 的指定节点下(如
/rpc/services/HelloService/192.168.1.100:8080)。服务发现:服务消费者启动时,从 Zookeeper 订阅对应服务的节点,获取所有可用的服务提供者地址列表。
动态感知:Zookeeper 的 Watcher 机制能让消费者实时感知服务提供者的上下线(如服务宕机、扩容),动态更新地址列表,无需重启服务。
负载均衡基础:消费者从 Zookeeper 获取的地址列表,可基于轮询、随机等策略实现简单的负载均衡。
核心功能是完成 ZK 连接、节点创建和数据读取,主要用于 RPC 框架中的服务注册与发现
cpp
#pragma once
#include <semaphore.h>
#include <zookeeper/zookeeper.h>
#include <string>
class ZkClient
{
zhandle_t* m_zhandle;
public:
ZkClient();
~ZkClient();
void Start();
void Create(const char *path, const char *data, int datalen, int state=0);
std::string GetData(const char *path);
};#include "zookeeperutil.hpp"
#include"mprpcapplication.hpp"
#include<iostream>
// 全局的watcher观察器 zkserver给zkclient的通知
void global_watcher(zhandle_t *zh, int type,
int state, const char *path, void *watcherCtx)
{
if (type == ZOO_SESSION_EVENT) // 回调的消息类型是和会话相关的消息类型
{
if (state == ZOO_CONNECTED_STATE) // zkclient和zkserver连接成功
{
sem_t *sem = (sem_t*)zoo_get_context(zh);
sem_post(sem);
}
}
}
ZkClient::ZkClient():m_zhandle(nullptr)
{
}
ZkClient::~ZkClient()
{
if(m_zhandle!=nullptr){
zookeeper_close(m_zhandle);
}
}
void ZkClient::Start()
{
FILE* log_file = fopen("/home/wangt/项目/rpcCorrespond/bin/wangt.20260114185335.936854.log", "a");
if (log_file != nullptr) {
zoo_set_log_stream(log_file); // 设置ZK日志输出到文件
}
std::string host = MprpcApplication::GetInstance().GetZkServerIp("zookeeper_ip");
std::string port = std::to_string(MprpcApplication::GetInstance().GetZkServerPort("zookeeper_port"));
std::string connstr = host + ":" + port;
m_zhandle = zookeeper_init(connstr.c_str(), global_watcher, 30000, nullptr, nullptr, 0);
if (nullptr == m_zhandle)
{
std::cout << "zookeeper_init error!" << std::endl;
exit(EXIT_FAILURE);
}
sem_t sem;
sem_init(&sem, 0, 0);
zoo_set_context(m_zhandle, &sem);
sem_wait(&sem);
std::cout << "zookeeper_init success!" << std::endl;
}
void ZkClient::Create(const char *path, const char *data, int datalen, int state)
{
char path_buffer[128];
int bufferlen = sizeof(path_buffer);
int flag;
// 先判断path表示的znode节点是否存在,如果存在,就不再重复创建了
flag = zoo_exists(m_zhandle, path, 0, nullptr);
if (ZNONODE == flag) // 表示path的znode节点不存在
{
// 创建指定path的znode节点了
flag = zoo_create(m_zhandle, path, data, datalen,
&ZOO_OPEN_ACL_UNSAFE, state, path_buffer, bufferlen);
if (flag == ZOK)
{
std::cout << "znode create success... path:" << path << std::endl;
}
else
{
std::cout << "flag:" << flag << std::endl;
std::cout << "znode create error... path:" << path << std::endl;
exit(EXIT_FAILURE);
}
}
}
// 根据指定的path,获取znode节点的值
std::string ZkClient::GetData(const char *path)
{
char buffer[64];
int bufferlen = sizeof(buffer);
int flag = zoo_get(m_zhandle, path, 0, buffer, &bufferlen, nullptr);
if (flag != ZOK)
{
std::cout << "get znode error... path:" << path << std::endl;
return "";
}
else
{
return buffer;
}
}
一、代码整体功能总结
这段代码封装了一个ZkClient类,实现了以下核心功能:
- 初始化并建立与 Zookeeper 服务器的连接(基于信号量确保连接成功);
- 创建指定路径的 ZNode 节点(创建前先检查节点是否存在,避免重复创建);
- 读取指定 ZNode 节点的存储数据;
- 自定义日志输出路径,以及会话级别的 Watcher 监听(处理连接成功事件)。
二、代码逐模块详细解析
1. 头文件与成员变量
cpp
#pragma once
#include <semaphore.h> // 信号量头文件,用于同步连接成功事件
#include <zookeeper/zookeeper.h> // ZK原生C API头文件
#include <string>
class ZkClient
{
zhandle_t* m_zhandle; // ZK客户端句柄,核心操作入口
public:
ZkClient();
~ZkClient();
void Start(); // 初始化连接ZK服务器
void Create(const char *path, const char *data, int datalen, int state=0); // 创建节点
std::string GetData(const char *path); // 读取节点数据
};
zhandle_t*:ZK 客户端的核心句柄,所有 ZK 操作(创建节点、读数据、监听)都需要通过它完成;- 成员函数分工清晰,符合 RPC 框架中 ZK 客户端的基础需求。
2. 全局 Watcher 回调函数
// 全局的watcher观察器 zkserver给zkclient的通知
void global_watcher(zhandle_t *zh, int type,
int state, const char *path, void *watcherCtx)
{
if (type == ZOO_SESSION_EVENT) // 回调的消息类型是和会话相关的消息类型
{
if (state == ZOO_CONNECTED_STATE) // zkclient和zkserver连接成功
{
sem_t *sem = (sem_t*)zoo_get_context(zh); // 取出绑定在句柄上的信号量
sem_post(sem); // 释放信号量,通知主线程连接成功
}
}
}
-
Watcher 作用 :ZK 的事件回调机制,这里只处理
ZOO_SESSION_EVENT(会话事件),且仅关注ZOO_CONNECTED_STATE(连接成功状态); -
信号量同步 :通过
zoo_get_context从 ZK 句柄中取出提前绑定的信号量,调用sem_post释放信号量,让主线程的sem_wait退出阻塞,确保连接成功后再执行后续操作; -
局限性:当前只处理了连接成功事件,未处理会话过期、断开重连等场景(RPC 框架中建议补充)。
3. 构造 / 析构函数
ZkClient::ZkClient():m_zhandle(nullptr) // 初始化句柄为空
{
}
ZkClient::~ZkClient()
{
if(m_zhandle!=nullptr){
zookeeper_close(m_zhandle); // 关闭ZK连接,释放句柄资源
}
}
- 析构函数中安全关闭 ZK 连接,避免资源泄漏,符合 C++ RAII 原则。
4. Start () 核心连接逻辑
void ZkClient::Start()
{
// 自定义ZK日志输出路径(默认输出到终端,这里重定向到文件)
FILE* log_file = fopen("/home/wangt/项目/rpcCorrespond/bin/wangt.20260114185335.936854.log", "a");
if (log_file != nullptr) {
zoo_set_log_stream(log_file); // 设置ZK日志输出到文件
}
// 从配置中读取ZK服务器IP和端口(依赖MprpcApplication单例)
std::string host = MprpcApplication::GetInstance().GetZkServerIp("zookeeper_ip");
std::string port = std::to_string(MprpcApplication::GetInstance().GetZkServerPort("zookeeper_port"));
std::string connstr = host + ":" + port;
// 初始化ZK客户端句柄
m_zhandle = zookeeper_init(connstr.c_str(), global_watcher, 30000, nullptr, nullptr, 0);
if (nullptr == m_zhandle)
{
std::cout << "zookeeper_init error!" << std::endl;
exit(EXIT_FAILURE);
}
// 初始化信号量,用于同步连接成功事件(初始值0,阻塞主线程)
sem_t sem;
sem_init(&sem, 0, 0);
zoo_set_context(m_zhandle, &sem); // 将信号量绑定到ZK句柄,供Watcher回调使用
sem_wait(&sem); // 阻塞,直到Watcher中调用sem_post释放信号量
std::cout << "zookeeper_init success!" << std::endl;
}
关键细节解析:
-
zookeeper_init参数说明:-
connstr.c_str():ZK 服务器地址(如 127.0.0.1:2181); -
global_watcher:全局 Watcher 回调函数; -
30000:会话超时时间(ms); -
后两个
nullptr:分别是会话 ID 和密码(无需自定义); -
0:不启用客户端上下文(这里通过zoo_set_context手动绑定);
-
-
信号量的核心作用 :
zookeeper_init是异步操作(调用后立即返回,连接建立是后台线程完成),通过信号量阻塞主线程,直到 Watcher 回调确认连接成功,避免后续操作(如创建节点)在连接未建立时执行;
5. Create () 节点创建逻辑
void ZkClient::Create(const char *path, const char *data, int datalen, int state)
{
char path_buffer[128];
int bufferlen = sizeof(path_buffer);
int flag;
// 先判断path表示的znode节点是否存在,如果存在,就不再重复创建了
flag = zoo_exists(m_zhandle, path, 0, nullptr);
if (ZNONODE == flag) // 表示path的znode节点不存在
{
// 创建指定path的znode节点
flag = zoo_create(m_zhandle, path, data, datalen,
&ZOO_OPEN_ACL_UNSAFE, // 权限控制:无权限限制(测试/内网环境可用)
state, // 节点类型(0=持久节点,ZOO_EPHEMERAL=临时节点,ZOO_SEQUENCE=有序节点)
path_buffer, bufferlen); // 存储创建后的节点路径(有序节点时会返回带序号的路径)
if (flag == ZOK)
{
std::cout << "znode create success... path:" << path << std::endl;
}
else
{
std::cout << "flag:" << flag << std::endl;
std::cout << "znode create error... path:" << path << std::endl;
exit(EXIT_FAILURE);
}
}
}
关键细节解析:
zoo_exists:检查节点是否存在,0表示不使用 Watcher 监听该节点;zoo_create参数说明:ZOO_OPEN_ACL_UNSAFE:最宽松的权限策略,允许所有客户端操作(生产环境需根据需求配置 ACL);state:节点类型,RPC 中服务提供者注册地址时需传ZOO_EPHEMERAL(临时节点,服务下线自动删除);
- 局限性 :
- 不支持递归创建父节点(如创建
/rpc/services/HelloService时,若/rpc或/rpc/services不存在,会创建失败); - 同样使用
exit终止程序,不够优雅; - 未处理网络异常、节点创建失败的其他场景(如权限不足、节点已存在但判断时网络超时)。
- 不支持递归创建父节点(如创建
6. GetData () 节点数据读取
// 根据指定的path,获取znode节点的值
std::string ZkClient::GetData(const char *path)
{
char buffer[64];
int bufferlen = sizeof(buffer);
// 读取节点数据,0表示不监听,nullptr表示不获取节点状态
int flag = zoo_get(m_zhandle, path, 0, buffer, &bufferlen, nullptr);
if (flag != ZOK)
{
std::cout << "get znode error... path:" << path << std::endl;
return "";
}
else
{
return buffer;
}
}
- 简单封装了
zoo_get,读取节点存储的字符串数据(如 RPC 服务的地址192.168.1.100:8080); - 潜在问题:
buffer大小固定为 64 字节,若节点数据超过该长度会截断,建议改为动态缓冲区或传入缓冲区大小。
总结
- 核心功能 :原代码实现了 ZK 客户端的基础能力(连接、创建节点、读数据),满足 RPC 服务注册 / 发现的基本需求,核心通过信号量同步异步连接 和Watcher 监听连接状态保证操作可靠性;
- 核心问题优化 :
- 解决了信号量生命周期问题(改用智能指针);
- 支持递归创建父节点(RPC 中创建多级服务路径必备);
- 替换粗暴的
exit为异常抛出,增强代码健壮性; - 动态缓冲区读取数据,避免截断;
- 日志路径 / ZK 地址改为配置项,避免硬编码;
- RPC 场景适配 :在 RPC 服务提供者中,调用
Create时需传入ZOO_EPHEMERAL(临时节点),服务消费者通过GetData读取服务地址,还可扩展 Watcher 监听节点变化(感知服务上下线)。
