目录
一、zookeeper安装(包括C库安装)
1.下载zookeeper包
下载链接:http://archive.apache.org/dist/zookeeper/
进入要下载的版本的目录,选择.tar.gz文件下载
目前使用的是3.5.4
2.将zookeeper包拷贝到系统/home/pdds/目录下,使用tar -zxf zookeeper-3.5.4-beta.tar.gz命令解压
3.配置
在主目录下创建data和logs两个目录用于存储数据和日志:
cd /home/pdds/zookeeper-3.5.4-beta
mkdir data
mkdir logs
在/home/pdds/zookeeper-3.5.4-beta/conf目录下新建zoo.cfg文件,写入以下内容保存:
tickTime=2000
dataDir=/home/pdds/zookeeper-3.5.4-beta/data
dataLogDir=/home/pdds/zookeeper-3.5.4-beta/logs
clientPort=2181
4.安装jdk
https://blog.csdn.net/smile_from_2015/article/details/80056297
5.启动zookeeper
cd /home/pdds/zookeeper-3.5.4-beta/bin
./zkServer.sh start
启动成功则zookeeper安装完毕
6.安装zookeeper的C库
cd /home/pdds/zookeeper-3.5.4-beta/src/c/
./configure
make
sudo make install
二、需要的头文件
#include <zookeeper/zookeeper.h>
#include <zookeeper/zookeeper_log.h>
编译时需要制定这两个头文件位置:-I/usr/local/include/zookeeper
三、需要的动态库
zookeeper有两个动态库,libzookeeper_st.so和libzookeeper_mt.so,分别用于单线程和多线程模式,目前主要使用多线程模式。
编译使需要链接多线程库-L/usr/local/lib/ -lzookeeper_mt,其中/usr/local/lib/为源码安装后libzookeeper_mt.so的默认路径
如果运行时出现error while loading shared libraries: libzookeeper_mt.so.2: cannot open shared object file: No such file or directory
说明程序运行时寻找动态库的路径是/usr/lib/i386-linux-gnu/(32位系统)/或/usr/lib/x86_64-linux-gnu(64位系统),只需要把
/usr/local/lib/路径下的 libzookeeper_mt.so.2复制到/usr/lib/i386-linux-gnu//或/usr/lib/x86_64-linux-gnu即可
四、C API
参考:
https://blog.csdn.net/yangzhen92/article/details/53248294
https://cailin.iteye.com/blog/2014486/
https://www.cnblogs.com/caosiyang/archive/2012/11/09/2763190.html
https://www.cnblogs.com/xiohao/p/5541093.html
1.连接到zookeeper服务器
我们的程序启动之后,首先要连接到zookeeper服务器,然后才能进行创建节点和订阅等操作。以下为实现连接到zookeeper服务器的代码。
#define ZK_SERVER_IP "192.168.1.200"
#define ZK_SERVER_PORT 2181
zhandle_t* zkhandle = NULL;
void QueryServer_watcher_g(zhandle_t* zh, int type, int state, const char* path, void* watcherCtx)
{
if (type == ZOO_SESSION_EVENT) {
if (state == ZOO_CONNECTED_STATE) {
printf("Connected to zookeeper service successfully!\n");
}
else if (state == ZOO_EXPIRED_SESSION_STATE) {
printf("Zookeeper session expired!\n");
}
}
}
int main()
{
//连接到zk服务器
char zk_server[25];
memset(zk_server, 0, 25);
sprintf(zk_server, "%s:%d", ZK_SERVER_IP, ZK_SERVER_PORT);
const char* host = zk_server;
int timeout = 30000;
zoo_set_debug_level(ZOO_LOG_LEVEL_WARN);
zkhandle = zookeeper_init(host, QueryServer_watcher_g, timeout, 0,(void*)"hellozookeeper.", 0);
if (zkhandle == NULL) {
printf("Error when connecting to zookeeper servers...\n");
}
zookeeper_close(zkhandle);
return;
}
运行代码后如果屏幕打印Connected to zookeeper service successfully!则说明程序成功连接到了zookeeper服务器。
zoo_set_debug_level用于设置zookeeper日志级别,ZOO_LOG_LEVEL_DEBUG为调试级别。
这里使用到了zookeeper_init接口,原型如下:
ZOOAPI zhandle_t *zookeeper_init(const char *host, watcher_fn fn,
int recv_timeout,
const clientid_t * clientid,
void *context, int flags);
参数说明:
(1)host:zookkeeper服务器的地址,格式IP:PORT,注意一定是const char *类型的,传参时需要注意const char *类型的赋值方式
(2)fn:全局的监视器回调函数,当发生事件通知时,该函数会被调用,watcher_fn函数原型如下:
typedef void (*watcher_fn)(zhandle_t *zh, int type, int state, const char *path,void *watcherCtx);
其中type为事件类型,state为状态类型,path为触发监视事件zonode节点的路径,如果为NULL,则事件类型为ZOO_SESSION_EVENT,watcherCtx为监视器上下文,type和state的具体取值见附录,后两个参数目前我没有用到过
(3)timeout:超时时间,我没试过,我理解单位应该是秒,超时后会收到ZOO_EXPIRED_SESSION_STATE
(4)clientid:客户端尝试重连的先前会话的ID,如果不需要重连先前的会话,则设置为0。
(5)context:与zhandle_t实例相关联的“上下文对象”(可以通过该参数为 zhandle_t 传入自定义类型的数据),应用程序可以通过 zoo_get_context 访问它(例如在监视器回调函数中),当然 zookeeper 内部没有用到该参数,所以 context 可以设置为 NULL。
(6)flags:一般设置为0。
2.创建节点(服务注册)
连接zookeeper服务器成功后,我们通常需要建立一个代表自己的节点。
zookeeper创建节点的代码如下。
int ret = zoo_acreate(zkhandle, "/DISP/DISP2", "1", 1, &ZOO_OPEN_ACL_UNSAFE, 0, NULL, NULL);
if (ret) {
printf("Error zoo_acreate\n");
}
这里用到了zoo_acreate接口,原型如下:
ZOOAPI int zoo_acreate(zhandle_t * zh, const char *path,
const char *value, int valuelen,
const struct ACL_vector *acl, int flags,
string_completion_t completion, const void *data);
参数说明:
(1)path:创建的节点路径,如果该路径已经存在,则该函数不生效
(2)value:该节点保存的数据
(3)valuelen:数据长度
(4)acl: 该节点初始 ACL,ACL 不能为null 或空。zookeeper使用ACL(Access Control List)控制对节点的访问。
(5)flags:该参数可以设置为 0,或者创建标识符 ZOO_EPHEMERAL(临时节点), ZOO_SEQUENCE (顺序节点),如果设置ZOO_EPHEMERAL,客户端会话失效,节点将自动删除;如果设置ZOO_SEQUENCE,一个唯一的自动增加的序列号附加到路径名,序列号宽度是10个数字的宽度,不足用0填充
tip1:如果要设置临时顺序节点,这里应填ZOO_SEQUENCE | ZOO_EPHEMERAL
tip2:如果flags填0或
ZOO_EPHEMERAL,path应为节点名,如/PATH/PATH1,如果flags填ZOO_SEQUENCE,path应为路径,如/PATH/,区别在于最后是否加/
tip3:
如果创建的是顺序节点,比如已经创建了000,001,如果这时001被删除了,再创建的节点是002,即节点号一直递增,不管前面是否连续
(6)completion:创建节点请求完成后调用的函数,不使用时填为NULL
我在使用的时候一开始这里填了NULL,结果执行完zoo_acreate之后,无论是sleep还是其他方式想让程序阻塞住都会导致程序崩溃,后来使用了这个函数,函数里只有一行打印,程序就不会崩溃了,具体原因还没有想清楚
string_completion_t 原型为typedef void(* string_completion_t)(int rc, const char *value, const void *data);其中data为传入的数据,rc是异步返回的错误码,value是返回的字符串
(7)data:completion 函数被调用时,传递给 completion 的数据,不使用时填NULL
创建节点完成之后,可以在系统使用命令行查看刚刚创建的节点以及节点的内容
cd /home/pdds/zookeeper-3.5.4-beta/bin
./zkCli.sh -server 192.168.1.200:2181
使用ls命令可以列出节点
使用get命令可以获取节点数据
3.订阅节点(服务发现)
连接zookeeper服务器成功之后,程序可以订阅其他节点,这样就可以在其他节点状态改变之后得到通知。
为了实现订阅功能,封装了一下几个函数
void QueryServerd_awexists1(zhandle_t *zh)
{
int ret = zoo_awexists(zh, "/MSC/MSC1", QueryServerd_watcher_awexists1, (void*)"QueryServerd_awexists.", QueryServerd_stat_completion, "zoo_awexists");
if (ret) {
printf("Error zoo_awexists!\n");
}
}
这个函数用于订阅,里面调用了zoo_awexists接口,原型如下:
ZOOAPI int zoo_awexists(zhandle_t * zh, const char *path,
watcher_fn watcher, void *watcherCtx,
stat_completion_t completion, const void *data);
参数说明:
(1)path:订阅的节点路径
(2)watcher:节点状态发生变化时的回调函数
(3)watcherCtx:传入watcher函数的参数
(4)completion:zoo_awexists执行完毕之后执行的函数
(5)data:传入completion的参数
需要注意的是zoo_awexists是一次性的,即订阅的节点发生状态变化调用回调函数之后,订阅就失效了,所以回调函数最后要再建立起订阅
void QueryServerd_stat_completion(int rc, const struct Stat *stat, const void *data)
{
printf("Zoo_awexists complete!\n");
}
这是zoo_awexists执行完毕之后会调用的函数,这里只打印了zoo_awexists成功
void QueryServerd_watcher_awexists1(zhandle_t *zh, int type, int state, const char *path, void *watcherCtx)
{
if (state == ZOO_CONNECTED_STATE) {
if (type == ZOO_DELETED_EVENT) {
printf("MSC has gone, need to restart it\n");
}
else if (type == ZOO_CREATED_EVENT) {
printf("MSC started...\n");
}
}
QueryServerd_awexists1(zh);
}
这是订阅节点状态发生变化时会调用的回调函数,函数类型为watcher_fn,原型上文有提到过,注意这个函数最后要再调用一次订阅函数,这是因为上面提到过的订阅是一次性的
tip1:在订阅A节点之后,A节点创建或者删除都会触发回调函数,例如运行zkCli.sh,执行create A或delete A都会立即触发回调函数,但是,如果另一个进程T运行了zookeeper客户端程序并创建了A,那么需要注意两点,第一,A必须是临时节点,即创建时要使用ZOO_EPHEMERAL类型,否则进程T结束A并不会被删除,回调函数也不会触发;第二如果A是临时节点,进程T意外退出,那么只有在zookeeper服务器没有按时收到进程T发来的握手时才会删除A,回调函数才会触发,因此回调函数何时触发由zookeeper客户端和服务器的握手周期决定。在zookeeper的配置文件中(/home/pdds/zookeeper-3.5.4-beta/conf/zoo.cfg),tickTime参数代表ZK中的一个时间单元。ZK中所有时间都是以这个时间单元为基础,进行整数倍配置的,因此,调小tickTime可以减小zookeeper客户端和服务器之间的握手周期,但是同样也会使zookeeper的CPU占用率增加。tickTime默认为2000,此时握手周期为20多秒,zookeeper的CPU占用为0.5%,将tickTime调为20,握手周期小于1秒,即进程T退出后,能在1秒之内触发回调函数,但此时zookeeper的CPU占用率增长到了1.5%。
tip2:zoo_awexists只能订阅节点创建(ZOO_CREATED_EVENT)、节点删除(ZOO_DELETED_EVENT)和节点内容改变(ZOO_CHANGED_EVENT),不能订阅节点的子节点列表信息(ZOO_CHILD_EVENT)
4.订阅子节点
由于zoo_awexists不能订阅子节点信息,因此想要获取子节点信息时需要使用另一个接口,zoo_awget_children,这个接口只能订阅子节点列表信息,包括子节点的添加和删除,不能订阅子节点的内容变化,也不能订阅本节点的增加删除和内容变化。
使用zoo_awget_children的方法与zoo_awexists类似,也是封装了三个函数
void QueryServerd_awexists1_child(zhandle_t *zh)
{
int ret = zoo_awget_children(zkhandle, "/MSC", QueryServerd_watcher_awexists_child, NULL, QueryServerd_stat_completion_child, NULL);
if (ret) {
printf("Error QueryServerd_awexists1_child!\n");
}
}
void QueryServerd_watcher_awexists_child(zhandle_t *zh, int type, int state, const char *path, void *watcherCtx)
{
if (state == ZOO_CONNECTED_STATE) //会话已建立
{
if(type == ZOO_CHILD_EVENT)
{
printf("MSC child changed...\n");
}
}
else
{
printf("Other type...\n");
}
QueryServerd_awexists1_child(zh);
}
void QueryServerd_stat_completion_child(int rc, const struct String_vector *strings, const void *data)
{
printf("Zoo_awget_children complete!\n");
}
5.获取子节点列表
通过zoo_awget_children只能获取子节点列表改变信息,当得知子节点列表改变时,想要获取最新的子节点信息,包括增加、删除了哪些子节点,就需要主动获取子节点信息。
这里没有设置watcher,所以只用了两个函数
void QueryServerd_awexists1_child_list(zhandle_t *zh)
{
int ret = zoo_aget_children(zkhandle, "/MSC", 0, QueryServerd_strings_completion_child, NULL);
if (ret) {
printf("Error QueryServerd_awexists1_child_list!\n");
}
}
这个函数用于向zookeeper服务器获取/MSC节点的子节点列表,其中用到了zoo_aget_children接口,原型如下:
ZOOAPI int zoo_aget_children(zhandle_t * zh, const char *path,
int watch,
strings_completion_t completion,
const void *data);
参数说明:
(1)watch:如果设为非0值,zookeeper服务器会启动一个watcher监听节点消息,但是这个watcher在哪执行什么我没搞清楚,所以我用的时候填成了0,之后自己再订阅。
(2)completion:zoo_aget_children执行完毕运行的函数
(3)data:传入zoo_aget_children的参数
void QueryServerd_strings_completion_child(int rc, const struct String_vector *strings, const void *data)
{
printf("QueryServerd_strings_completion_child complete, child_num is %d!\n", (int)strings->count);
int i = 0, n = (int)strings->count;
for(i = 0; i < n; i++)
printf("path%d is %s\n", i, strings->data[i]);
//释放内存
deallocate_String_vector(strings);
}
这是zoo_aget_children执行完毕运行的函数,原型如下:
typedef void(* strings_completion_t)(int rc, const struct String_vector *strings, const void *data);
参数说明:
strings为子节点列表查询结果,是一个String_vector类型的结构体,String_vector结构体定义如下
struct String_vector {
int32_t count;
char * *data;
};
其中count为子节点个数,data存储了所有节点的路径
tip:这里strings在调用api时会通过malloc分配内存空间,将子节点所有的目录存放在data字段中,需要客户端调用deallocate_String_vector(strings)做释放处理。
6.获取节点数据
zookeeper的API一般都有同步异步两个版本,同步是等到执行完再继续执行下面的代码,异步是执行之后直接运行下面的代码,等执行结果出来以后再执行回调函数。
获取节点数据的同步API是zoo_get,原型如下:
int zoo_get(zhandle_t *zh, const char *path, int watch, char *buffer, int* buffer_len, struct Stat *stat);
调用这个接口完成之后,path节点的数据会存储在buffer里,数据长度存在buffer_len里,
但是很奇怪,我调用这个接口每次得到的buffer_len都是0,之后我试了一下异步接口zoo_aget,就成功读到了数据
zoo_aget原型如下:
int zoo_aget(zhandle_t *zh, const char *path, int watch, data_completion_t completion, const void *data);
这个接口是异步的,执行成功之后会回调completion函数,data_completion_t 原型如下
typedef void(* data_completion_t)(int rc, const char *value, int value_len, const struct Stat *stat, const void *data);
其中value是节点的数据,value_len是数据长度。
//获取节点数据
zoo_aget(zkhandle, "/MSC", 0, QueryServerd_data_completion, NULL);
void QueryServerd_data_completion(int rc, const char *value, int value_len, const struct Stat *stat, const void *data)
{
printf("QueryServerd_data_completion data is %s, len is %d!\n", value, value_len);
}
tip:在使用zoo_aget的过程中遇到过一个问题,就是把获取到的value打印出来,发现不只有节点数据,后面还跟了一个奇怪的序号,再次获取序号会+1,但是value_len是对的,就是没有算上后面那个序号的长度。后来我重新编译了程序,就没再出现这个问题,目前还没有找到具体原因,所以建议读取获取到的value时根据value_len来读取,以免数据后面跟了其他数据。
附录
1.Watcher通知的状态类型和事件类型
状态类型(state)
状态码 说明
-112 会话超时(ZOO_EXPIRED_SESSION_STATE)
-113 认证失败(ZOO_AUTH_FAILED_STATE)
1 连接建立中(ZOO_CONNECTING_STATE)
2 连接建立中(ZOO_ASSOCIATING_STATE)
3 连接已建立(ZOO_CONNECTED_STATE)
999 无连接状态
事件类型(type)
事件码 说明
1 创建节点事件(ZOO_CREATED_EVENT)
2 删除节点事件(ZOO_DELETED_EVENT)
3 更改节点事件(ZOO_CHANGED_EVENT)
4 子节点列表变化事件(ZOO_CHILD_EVENT)
-1 会话session事件(ZOO_SESSION_EVENT)
-2 监视被移除事件(ZOO_NOTWATCHING_EVENT)