1.zookeeper_init
ZOOAPIzhandle_t*zookeeper_init(constchar*host,// 服务器地址列表(可含 chroot)watcher_fn fn,// 全局 watcher 回调intrecv_timeout,// 期望的会话超时(毫秒)constclientid_t*clientid,// 会话复用:旧会话凭证,NULL 则新建void*context,// 用户上下文,回调时透传intflags// 行为标志位);1.1.参数解释
host:连接字符串
格式:host1:port1,host2:port2,…,hostN:portN[/chroot]
- 多地址用逗号分隔,客户端按内置顺序轮询连接(带指数退避),只要连上一台即可——通常填集群所有节点,客户端自动故障转移;
- 末尾可带 chroot(如 /app/cluster1):所有操作的路径自动加上该前缀,实现命名空间隔离。客户端代码里写 /workers,实际服务器路径是 /app/cluster1/workers。创建句柄时 chroot 路径必须已存在(否则首次写操作会报 ZNONODE);
- 地址在连接过程中才做 DNS 解析(update_addrs),所以填主机名没问题。
fn:全局 watcher 回调
typedefvoid(*watcher_fn)(zhandle_t*zh,inttype,intstate,constchar*path,void*watcherCtx);- 每个会话级事件都会通知它:连接建立(ZOO_CONNECTED_STATE)、断线重连中(ZOO_CONNECTING_STATE)、会话过期(ZOO_EXPIRED_SESSION_STATE)、认证失败(ZOO_AUTH_FAILED_STATE)、只读模式(ZOO_READONLY_STATE),事件 type 固定为 ZOO_SESSION_EVENT;
- 传 NULL 则内部挂 null_watcher_fn,功能正常只是收不到通知;
- 典型用法是在此回调里维护连接状态机(如重建临时节点、重发订阅)。
对业务侧:
- ZOO_CONNECTED_STATE/ZOO_READONLY_STATE用于让业务层感知到和集群某节点建立会话。-
- ZOO_CONNECTING_STATE用于让业务层感知到和当前集群节点断开了连接,此后客户端库内部负责选择集群节点,重连,多次尝试并失败期间,不会再次收到ZOO_CONNECTING_STATE。
- ZOO_EXPIRED_SESSION_STATE/ZOO_AUTH_FAILED_STATE代表此会话已经不可继续用。
recv_timeout:期望的会话超时(毫秒)
- 表示"客户端允许多久收不到服务器消息才认为会话过期"的期望值;
- 注意:服务器有最终裁量权。合法范围 2~20 倍 tickTime(默认 4s~40s),ConnectResponse 会返回协商后的实际值,可用 zookeeper_recv_timeout() 查询;
- 语义影响:超时内断线 → 重连后会话保留(临时节点、watcher 都在);超过超时 → 会话被服务器判定过期,临时节点删除,必须重建一切。
clientid:会话复用(关键参数,多数教程讲不透)
typedefstruct{int64_tclient_id;// 会话 IDcharpasswd[16];// 会话密码}clientid_t;- 传 NULL:创建全新会话;
- 传入之前通过 zookeeper_client_id(zh) 保存的凭证:尝试恢复原会话。对应前面 prime_connection 里 req.sessionId = zh->seen_rw_server_before ? zh->client_id.client_id : 0 的逻辑;
- 典型场景:应用进程重启但想保住会话——退出前保存 clientid_t,下次启动传进来。若旧会话在超时窗口内未过期,临时节点和 watcher 全部保留;已过期则服务器静默分配新会话(可用 zookeeper_client_id 对比确认),但客户端库会视为会话过期,反馈给业务层。
context:用户上下文指针
- 完全由用户定义,客户端不解释;
- 会原样传给全局 watcher(回调的 watcherCtx 参数);
- 常用来传应用状态对象,避免回调里用全局变量。
flags:行为标志位
flags:行为标志位
| 标志 | 含义 |
|---|---|
ZOO_READONLY | 集群失去 quorum(多数派)时,允许连接进入只读模式(state 为ZOO_READONLY_STATE),可读不可写;不带此标志则宁可断线重试也不接受只读服务 |
ZOO_NO_LOG_CLIENTENV | 抑制启动时的环境变量日志(log_env),用于日志敏感场景 |
其余位保留,传 0 即默认行为。
1.2.返回值与错误处理
- 成功:返回 zhandle_t* 句柄;
- 失败:返回 NULL,错误原因看 errno(如 ENOMEM、EINVAL——比如 chroot 路径非法);
- 重要:返回成功 ≠ 已连上服务器。此时只完成了句柄、线程、地址表的初始化(状态是 ZOO_NOTCONNECTED_STATE),连接由后台线程异步进行。是否连上要通过全局 watcher 的 ZOO_CONNECTED_STATE 事件感知——调用任何操作前通常应等待该事件或容忍 ZCONNECTIONLOSS 重试。
1.3.完整用法示例
staticvoidglobal_watcher(zhandle_t*zh,inttype,intstate,constchar*path,void*ctx){if(type==ZOO_SESSION_EVENT){if(state==ZOO_CONNECTED_STATE){printf("connected, session id=%#llx\n",zoo_client_id(zh)->client_id);}elseif(state==ZOO_EXPIRED_SESSION_STATE){// 会话彻底终结:必须 zookeeper_close 后重新 zookeeper_init}}}zhandle_t*zh=zookeeper_init("zk1:2181,zk2:2181,zk3:2181/myapp",// 集群地址 + chrootglobal_watcher,10000,// 期望 10s 超时NULL,// 新会话(想复用则传上次保存的 clientid_t)NULL,// context0);// 默认 flagsif(!zh){/* 按 errno 处理 */}// 之后用 zoo_create / zoo_get / zoo_wget 等操作,全部经由 zh1.4.注意事项
- 每个进程/每个逻辑实例一个句柄:句柄内部带 I/O 线程,不该高频创建销毁;
- 用完后必须 zookeeper_close(zh)——它会停线程、发错误回调清掉在途请求、释放全部资源;
- 会话过期后不能复用旧句柄,只能 close 后重新 init(可传入保存的 clientid_t 碰碰运气)。
2.zoo_awget
// 异步读,带独立 watcher 函数 + 上下文ZOOAPIintzoo_awget(zhandle_t*zh,constchar*path,watcher_fn watcher,void*watcherCtx,data_completion_tcompletion,constvoid*data);2.1.参数逐项说明
zh
zookeeper_init 返回的句柄
path
节点路径,客户端视角(chroot 由库自动拼接/剥离)
completion
读结果的回调,见下方签名
typedefvoid(*data_completion_t)(intrc,constchar*value,intvalue_len,conststructStat*stat,constvoid*data);| 回调参数 | 含义 |
|---|---|
rc | 结果码:ZOK成功;ZNONODE节点不存在;ZCONNECTIONLOSS/ZSESSIONEXPIRED等连接类错误 |
value/value_len | 节点数据及长度;失败时为 NULL/0 |
stat | 节点元数据(版本号、时间戳等);失败时为 NULL |
data | 就是你提交时传的data |
注意:value、stat 指向的内存只在回调执行期间有效,回调返回后可能失效——想保留必须自行拷贝。
ZCONNECTIONLOSS:请求执行中遭遇和当前集群节点连接断开。业务侧一般的应对策略是再次请求。
ZSESSIONEXPIRED:请求执行中收到通知,会话超期了。业务侧一般的应对策略是结束对此句柄的继续使用。
ZOK:请求执行完毕,取回了所需的数据。业务侧一般的应对策略是提取反馈的数据信息。
ZNONODE:请求执行完毕,因为节点不存在,所以无法取得数据。业务侧一般视具体业务情况决定下一步如何。
watcher + watcherCtx:
zoo_awget(zh,"/cfg",my_watcher,my_ctx,my_completion,ctx);watcher:本次监听专用的 watcher 函数,传 NULL 表示无监听;
watcherCtx:该 watcher 被触发时透传的上下文。
监控在服务侧实际执行请求时,才被注册到服务侧。监控在服务侧回复抵达客户端时,实际被注册到客户端侧。
和当前集群节点断开,并在随后再次连接到集群节点时,客户端库一次性把有效的watch传递给服务侧节点。服务侧节点收到这批watch后,依据传递的lastZxid进行一次检测,保证断联期间的改动,有对应的watch可以触发的,在会话重连那一刻即被引发。
watcher 触发时的回调签名
voidmy_watcher(zhandle_t*zh,inttype,intstate,constchar*path,void*watcherCtx){// 节点数据变化时,再次获取if(type!=ZOO_SESSION_EVENT){// 节点事件:ZOO_CHANGED_EVENT 等,path 有效if(state==ZOO_CONNECTED_STATE)zoo_awget(zh,path,my_watcher,watcherCtx,on_get_done,watcherCtx);return;}// —— 以下是会话事件,path 恒为 "",不要试图用 path 分发 ——switch(state){caseZOO_CONNECTED_STATE:// 连接建立/重连成功:watcher 注册已由库自动恢复,无需处理break;caseZOO_CONNECTING_STATE:// 断线中:在途请求将以 ZCONNECTIONLOSS 回调 completion,// 注册依然有效,通常无需动作(幂等读由 completion 里重试)break;caseZOO_EXPIRED_SESSION_STATE:caseZOO_AUTH_FAILED_STATE:// 句柄已死:后台重建会话是唯一出路request_session_rebuild();// 通知业务主线程 close + re-init + 重建一切break;}}对 zoo_awget 注册的监听,节点数据变化时:
- type = ZOO_CHANGED_EVENT,state = ZOO_CONNECTED_STATE,path = “/cfg”,watcherCtx = my_ctx;
- 一次性触发后即失效——想持续监听,必须在回调里(或回调后的逻辑中)重新发起 zoo_awget。这是 ZK watcher 最核心的使用范式。
对 zoo_awget 注册的监听,遭遇连接中断,会话超时,授权失败,连接建立这类会话类型通知时:
- 这类通知产生时,监控依然存在。
- ZOO_CONNECTED_STATE/ZOO_CONNECTING_STATE/ZOO_EXPIRED_SESSION_STATE/ZOO_AUTH_FAILED_STATE一般在全局watch中处理
2.2.用法示例
staticvoidon_data_changed(zhandle_t*zh,inttype,intstate,constchar*path,void*ctx){if(type==ZOO_CHANGED_EVENT&&state==ZOO_CONNECTED_STATE){// 事件=数据变化:重新发起监听式读取,完成"持续监听"zoo_awget(zh,path,on_data_changed,ctx,on_get_done,ctx);}elseif(state==ZOO_CONNECTING_STATE||state==ZOO_EXPIRED_SESSION_STATE){// 连接类事件:按业务状态机处理(过期则要重建会话)}}staticvoidon_get_done(intrc,constchar*value,intlen,conststructStat*stat,constvoid*data){MyConfig*cfg=(MyConfig*)data;if(rc==ZOK){parse_config(cfg,value,len);// 注意:value 回调返回后失效}elseif(rc==ZNONODE){// 节点不存在:可等待创建(用 zoo_awexists 监听)}// 连接类错误(ZCONNECTIONLOSS 等):读操作幂等,直接重试即可}// 启动时:读取配置 + 挂监听zoo_awget(zh,"/app/config",on_data_changed,cfg,on_get_done,cfg);几个用法要点:
- 返回值的含义:两函数返回的是"提交是否成功"(ZOK 已入队;ZBADARGUMENTS 路径非法;ZINVALIDSTATE 会话已过期;ZCONNECTIONLOSS 等入队失败)——不代表读操作本身的结果,读结果永远走 completion 回调;
- 并发友好:同一 zh 上可同时挂任意多个在途 zoo_awget,靠 data/watcherCtx 区分;
- 重试策略:读操作幂等,回调里收到 ZCONNECTIONLOSS 直接重新提交即可;但必须先排除句柄已不可恢复(ZSESSIONEXPIRED/ZINVALIDSTATE),那种情况重试无意义。
2.3.类似api
ZOOAPIintzoo_aget(zhandle_t*zh,constchar*path,intwatch,data_completion_tcompletion,constvoid*data);区别在于:
- watch为1时,支持监控。复用全局的watch和其上下文。为0时,无监控。
3.监控机制专题
zk客户端内部本地注册表共三张:active_node_watchers(数据类)、active_exist_watchers(存在类)、active_child_watchers(子节点类)。增删都严格跟随服务器反馈,规则如下。
3.1.何时添加注册——唯一入口:activateWatcher
本地添加只发生在 I/O 线程处理响应时(zookeeper_process),提交时只"携带注册单",绝不提前入库。判定逻辑是各 API 专属的 checker(zh, rc):rc 满足条件 → 返回目标表指针并插入;否则返回 NULL,不注册。
| 操作 API | checker | 添加条件(rc) | 添加到 | 后续被哪些非会话事件触发 |
|---|---|---|---|---|
zoo_awget/zoo_aget(watch=1)/zoo_wget | data_result_checker | 仅ZOK | active_node_watchers | 数据变化、节点删除 |
zoo_awexists/zoo_aexists(watch=1)/zoo_wexists | stat_result_checker | ZOK或ZNONODE | active_exist_watchers | 创建、数据变化、删除("等节点出现"靠ZNONODE也注册这条例外) |
zoo_awget_children(2)/zoo_aget_children(watch=1) | strings(_stat)_result_checker | 仅ZOK | active_child_watchers | 子节点增删、节点删除 |
三条关键推论:
- 注册严格镜像服务器:服务器只对成功执行的请求挂监听,客户端也只对成功响应做本地注册——两边永不脱节;
- 请求失败(除 exists 的 ZNONODE)或断线丢失 → 无注册,业务重试时必须重新发起带 watch 的请求,不能假设监听还在;
- 一次成功响应最多添加一条(同一 path 同一 (watcher,ctx) 去重,search_watcher 幂等)。
3.2.何时移除注册——三个途径
途径 A:节点事件触发(一次性语义,最主要的移除方式)
服务器推来 WATCHER_EVENT 时,collectWatchers 里的 add_for_event 用 hashtable_remove 把该路径的监听从表中整体剪切走(move 语义),再克隆投递给用户。按事件类型,移除范围是:
| 到达的事件 | 从注册表移除的监听 |
|---|---|
CREATED_EVENT_DEF/CHANGED_EVENT_DEF | 该 path 的 node 表 + exist 表监听 |
CHILD_EVENT_DEF | 该 path 的 child 表监听 |
DELETED_EVENT_DEF | 该 path 的三张表全部监听 |
途径 B:手动移除 API(zoo_remove_watches / zoo_aremove_watches)
- 先向服务器发 RemoveWatches 请求;
- 响应 rc == 0(服务器确认)后,deactivateWatcher 在本地对应表中删除:按 (watcher, context) 精确匹配,或 watcher 传 NULL 时删除该 path 的全部监听;
- 失败则本地不动——同样是"跟随服务器事实"原则。
途径 C:永不因会话事件移除(重要的反向规则)
| 场景 | 本地注册表动作 |
|---|---|
断线(CONNECTING通知) | 不移除,仅克隆通知(collect_session_watchers/copy_table) |
会话过期(EXPIRED_SESSION) | 同样不移除(表在内存里还在,但句柄已废,等于名存实亡) |
| 重连成功 | 不移除,反而由send_set_watches读取全表重新登记到服务器 |
途径 D:句柄销毁(生命周期终点)
zookeeper_close → destroy → destroy_zk_hashtable:三张表整体清空并释放。这是注册的最终归宿。