RPC分布式通信(4)--Zookeeper

Zookeeper 在 RPC 中的核心作用

在 RPC 分布式通信中,Zookeeper 主要解决以下核心问题:

  1. 服务注册:服务提供者启动时,将自己的地址(IP + 端口)、服务名称等信息注册到 Zookeeper 的指定节点下(如/rpc/services/HelloService/192.168.1.100:8080)。

  2. 服务发现:服务消费者启动时,从 Zookeeper 订阅对应服务的节点,获取所有可用的服务提供者地址列表。

  3. 动态感知:Zookeeper 的 Watcher 机制能让消费者实时感知服务提供者的上下线(如服务宕机、扩容),动态更新地址列表,无需重启服务。

  4. 负载均衡基础:消费者从 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类,实现了以下核心功能:

  1. 初始化并建立与 Zookeeper 服务器的连接(基于信号量确保连接成功);
  2. 创建指定路径的 ZNode 节点(创建前先检查节点是否存在,避免重复创建);
  3. 读取指定 ZNode 节点的存储数据;
  4. 自定义日志输出路径,以及会话级别的 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(临时节点,服务下线自动删除);
  • 局限性
    1. 不支持递归创建父节点(如创建/rpc/services/HelloService时,若/rpc/rpc/services不存在,会创建失败);
    2. 同样使用exit终止程序,不够优雅;
    3. 未处理网络异常、节点创建失败的其他场景(如权限不足、节点已存在但判断时网络超时)。
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 字节,若节点数据超过该长度会截断,建议改为动态缓冲区或传入缓冲区大小。

总结

  1. 核心功能 :原代码实现了 ZK 客户端的基础能力(连接、创建节点、读数据),满足 RPC 服务注册 / 发现的基本需求,核心通过信号量同步异步连接Watcher 监听连接状态保证操作可靠性;
  2. 核心问题优化
    • 解决了信号量生命周期问题(改用智能指针);
    • 支持递归创建父节点(RPC 中创建多级服务路径必备);
    • 替换粗暴的exit为异常抛出,增强代码健壮性;
    • 动态缓冲区读取数据,避免截断;
    • 日志路径 / ZK 地址改为配置项,避免硬编码;
  3. RPC 场景适配 :在 RPC 服务提供者中,调用Create时需传入ZOO_EPHEMERAL(临时节点),服务消费者通过GetData读取服务地址,还可扩展 Watcher 监听节点变化(感知服务上下线)。
相关推荐
重生之绝世牛码2 小时前
Linux软件安装 —— Flink集群安装(集成Zookeeper、Hadoop高可用)
大数据·linux·运维·hadoop·zookeeper·flink·软件安装
廋到被风吹走2 小时前
【分布式缓存】分布式缓存架构全解析:从 Redis Cluster 到多级缓存策略
分布式·缓存·架构
敏叔V58719 小时前
联邦学习与大模型:隐私保护下的分布式模型训练与微调方案
分布式
短剑重铸之日20 小时前
《7天学会Redis》特别篇: Redis分布式锁
java·redis·分布式·后端·缓存·redission·看门狗机制
独自破碎E21 小时前
什么是Spring IOC
java·spring·rpc
重生之绝世牛码1 天前
Linux软件安装 —— zookeeper集群安装
大数据·linux·运维·服务器·zookeeper·软件安装
重生之绝世牛码1 天前
Linux软件安装 —— kafka集群安装(SASL密码验证)
大数据·linux·运维·服务器·分布式·kafka·软件安装
填满你的记忆1 天前
【从零开始——Redis 进化日志|Day5】分布式锁演进史:从 SETNX 到 Redisson 的完美蜕变
java·数据库·redis·分布式·缓存
無森~1 天前
ZooKeeper
分布式·zookeeper·云原生