linux下使用zookeeper C API开发zookeeper应用的方法介绍

1、配置开发环境

构建ZooKeeper本地库的最简单的方式是使用ant构建工具。在你解压缩的ZooKeeper发行包的目录中,有一个名为build.xml的文件,
该文件包含了ant构建所需的构建步骤。你还需要用到automake、autoconf和cppunit这些工具,如果你使用Linux操作系统,需要确保这些工具在你的主机中可用。
一旦安装了所有必需的工具,你可以采用以下方式构建ZooKeeper的库:
ant compile-native

当构建完成,你可以在build/c/build/usr/lib中发现链接库文件,在build/c/build/usr/include/zookeeper发现你所需要的头文件。


2、开始会话

与ZooKeeper进行任何操作之前,我们首先需要一个zhandle_t句柄。我们通过调用zookeeper_init函数来获取句柄,函数定义如下:

ZOOAPI zhandle_t* zookeeper_init(const char* host, //包含ZooKeeper服务集群的主机地址的字符串,地址格式为host:port,每组地址以逗号分隔。
                                 watcher_fn fn,		//用于处理事件的监视点函数
                                 int recv_timeout,	//会话过期时间,以毫秒为单位
                                 const clientid_t* clientid,//之前已建立的一个会话的客户端ID,用于客户端重新连接
                                 void* context,//返回的zkhandle_t句柄所使用的上下文对象
                                 int flags);//该参数暂时没有使用,因此设置为0即可
zookeeper_init调用也许在实际完成会话建立前返回,因此只有当收到ZOO_CONNECTED_STATE事件时才可以认为会话建立完成。该事件可以通过监视点函数的实现来处理,该函数的定义如下:
typedef void (* watcher_fn)(zhandle_t* zh, //观察点函数引用的ZooKeeper句柄。
                            int type,	   //事件类型:ZOO_CREATED_EVENT、ZOO_DELETED_EVENT、ZOO_CHANGED_EVENT、ZOO_CHILD_EVENT、ZOO_SESSION_EVENT。	
                            int state,		//连接状态
                            const char* path,//被观察并触发事件的znode节点路径,如果事件为会话事件,路径为null
                            void* watcherCtx);//观察点的上下文对象
以下为一个监视点函数的实现示例:
static int connected = 0;
static int expired = 0;
void main_watcher(zhandle_t* zkh,
                  int type,
                  int state,
                  const char* path,
                  void* context) {
  if (type == ZOO_SESSION_EVENT) {
    if (state == ZOO_CONNECTED_STATE) {
      connected = 1;//在接收到ZOO_CONNECTED_STATE事件后设置状态为已连接状态
    } else if (state == ZOO_NOTCONNECTED_STATE) {
      connected = 0;
    } else if (state == ZOO_EXPIRED_SESSION_STATE) {
      expired = 1;//在接收到ZOO_EXPIRED_SESSION_STATE事件后设置为过期状态(并关闭会话句柄)
      connected = 0;
      zookeeper_close(zkh);
    }
  }
}
将所有操作串在一起,我们的主节点的init函数如下所示:
static int server_id;
int init(char* hostPort) {
  srand(time(NULL));
  server_id = rand();//设置服务器ID
  zoo_set_debug_level(ZOO_LOG_LEVEL_INFO);//设置log日志的输出级别
  zh = zookeeper_init(hostPort, //创建会话的调用
                      main_watcher,
                      15000,
                      0,
                      0,
                      0);
  return errno;
}
代码前两行设置了生成随机数的种子以及主节点的标识符,我们使用server_id来标识不同的主节点.之后我们设置了日志消息的输出级别。
最后,我们调用了zookeeper_init函数进行初始化

3、引导主节点

引导(Bootstraping)主节点是指创建主从模式例子中使用的一些znode节点并竞选主要主节点的过程。我们首先创建四个必需的znode节点:

void bootstrap() {
  if (!connected) {//如果尚未连接,记录日志并退出。
    LOG_WARN(("Client not connected to ZooKeeper"));
    return;
  }
  create_parent("/workers", "");//创建四个父节点:/workers、/assign、/tasks、/status
  create_parent("/assign", "");
  create_parent("/tasks", "");
  create_parent("/status", "");
  ...
}
以下为create_parent函数:
void create_parent(const char* path,
                   const char* value) {
               zoo_acreate(zh,//异步调用创建znode节点时,需要传入的zhandle_t句柄的实例,在这个实现中,该实例为全局静态变量
              path,//该方法的path参数类型为const char*类型,path用于将客户端与对应的znode节点的子树连接在一起,
              value,//该函数的第三个参数为存储到znode节点的数据信息。
              0,//该参数为保存数据信息(前一个参数)的长度值,本例中,设置为0。
              &ZOO_OPEN_ACL_UNSAFE,//本例中,我们并不关心ACL策略问题,所以我们均设置为unsafe模式。
              0,//这些znode节点为持久性的非有序节点,所以我们不需要传入任何标志位。
              create_parent_completion,//该方法为异步调用,我们需要传入一个完成函数,ZooKeeper客户端会在操作请求完成时调用该函数。
              NULL);//最后一个参数为上下文变量,本例中,不需要传入任何上下文变量。
}
因为该方法为异步调用,我们需要传入一个完成函数,ZooKeeper客户端会在操作请求完成时调用该函数,以下为完成函数的定义
typedef void
(*string_completion_t)(int rc,//rc为返回码,在所有完成函数中都会返回该值
const char *value,//value返回值为字符串
const void *data);//data为在异步调用时传入的上下文变量数据,注意,开发人员负责该数据变量指针所指向的堆存储空间的内存释放

在这个具体的例子中,我们的实现如下:

void create_parent_completion(int rc, const char* value, const void* data) {
  switch (rc) {//判断返回码来确定需要如何处理
    case ZCONNECTIONLOSS:
      create_parent(value, (const char*)data);//在连接丢失时进行重试操作。
      break;
    case ZOK:
      LOG_INFO(("Created parent node", value));
      break;
    case ZNODEEXISTS:
      LOG_WARN(("Node already exists"));
      break;
    default:
      LOG_ERROR(("Something went wrong when running for master"));
      break;
  }
}
很多完成函数均包含简单的日志记录功能,以便告诉我们当前系统正在做什么。
一般,完成函数会更加复杂,最好将各个完成函数的方法中的功能进行分离,就如我们在上面的例子中所展示的那样。


下一个任务为竞选主节点,竞选主节点主要就是尝试创建/master节点,以便锁定主要主节点角色。异步调用create方法与我们之前所讨论的有些不同,代码如下:

void run_for_master() {
  if (!connected) {
    LOG_WARN(LOGCALLBACK(zh),
             "Client not connected to ZooKeeper");
    return;
  }
  char server_id_string[9];
  snprintf(server_id_string, 9, "%x", server_id);
  zoo_acreate(zh,
              "/master",
              (const char*)server_id_string, //在/master节点中保存服务器标识符信息。
              sizeof(int),//将数据的长度信息传入函数中,在这里,如我们所声明的,长度为一个int型的长度。
              &ZOO_OPEN_ACL_UNSAFE,
              ZOO_EPHEMERAL,//该znode节点为临时性节点,因此我们必须传递临时性标志位参数。
              master_create_completion,
              NULL);
}

到目前为止,完成函数也会比之前的版本更加复杂一些:

void master_create_completion(int rc, const char* value, const void* data) {
  switch (rc) {
    case ZCONNECTIONLOSS:
      check_master();//连接丢失时,检查主节点的znode节点是否已经创建成功,或被其他主节点进程创建成功。
      break;
    case ZOK:
      take_leadership();//如果我们成功创建了节点,就获得了管理权资格
      break;
    case ZNODEEXISTS:
      master_exists();//如果主节点znode节点存在(其他进程已经创建并锁定该节点),就对该节点建立监视点,来监视该节点之后可能消失的情况。
 
      break;
    default:
      LOG_ERROR(LOGCALLBACK(zh),
                "Something went wrong when running for master.");
      break;
  }
}
如果该主节点进程发现/master节点已经存在,就需要通过zoo_awexists方法来设置一个监视点:
void master_exists() {
  zoo_awexists(zh,
               "/master",
               master_exists_watcher,//定义/master节点的监视点。
               NULL,
               master_exists_completion,//该exists函数的回调方法的参数。
               NULL);
}
注意,我们可以在这个方法的调用中传入上下文对象,以便传给监视点对象使用,不过我们在这个例子中并未使用

在znode节点被删除时,我们会收到通知消息,以下代码为监视点函数处理通知消息的实现:
void master_exists_watcher(zhandle_t* zh,
                           int type,
                           int state,
                           const char* path,
                           void* watcherCtx) {  
                            if (type == ZOO_DELETED_EVENT) {
                                assert(!strcmp(path, "/master"));
                                run_for_master(); //如果/master节点被删除,开始竞选主节点流程。
                            } else {
                                LOG_DEBUG(LOGCALLBACK(zh),
                                         "Watched event:", type2string(type));
                            }
                    }
再回到我们之前的master_exists函数中,我们实现的完成函数很简单,也是我们目前为止一直采用的模式,细节上需要注意一点,即在执行创建/master节点操作和执行exists请求之间,也许/master节点已经被删
除了(例如,前一个主要主节点进程已经退出),因此在完成函数中,我们还需要再次验证该znode节点是否存在,如果不存在,客户端进程需要再次竞选主节点流程:
void master_exists_completion(int rc,
                              const struct Stat* stat,
                              const void* data) {
  switch (rc) {
    case ZCONNECTIONLOSS:
    case ZOPERATIONTIMEOUT:
      master_exists();
      break;
    case ZOK:
      if (stat == NULL) {//通过判断返回的stat是否为null来检查该znode节点是否存在。
        LOG_INFO(LOGCALLBACK(zh),
                 "Previous master is gone, running for master");
        run_for_master();//如果节点不存在,再次进行主节点竞选的流程。
      }
      break;
    default:
      LOG_WARN(LOGCALLBACK(zh),
               "Something went wrong when executing exists: ",
               rc2string(rc));
      break;
  }
}
一旦主节点进程成为主要主节点,就可以开始行使管理权,我们将在下一节中进行说明。

4、行使管理权

一旦主节点进程被选定为主要主节点,它就可以开始行使其角色,首先它将获取所有可用从节点的列表信息:

void take_leadership() {
  get_workers();
} void get_workers() {
  zoo_awget_children(zh,
                     "/workers",
                     workers_watcher,//设置一个监视点来监视从节点列表的变化情况。
                     NULL,
                     workers_completion,//定义请求完成时需要调用的完成函数。
                     NULL);
}
我们在实现中缓存了上一次读取的从节点列表信息,当我们再次读取到一个新的列表时,我们将会替换掉旧的列表,这些操作将在zoo_awget_children所指定的完成函数中完成:
void workers_completion(int rc,
                        const struct String_vector* strings,
                        const void* data) {
  switch (rc) {
    case ZCONNECTIONLOSS:
    case ZOPERATIONTIMEOUT:
      get_workers();
      break;
    case ZOK:
      struct String_vector* tmp_workers =
          removed_and_set(strings, &workers);//更新从节点列表。
      free_vector(tmp_workers);//我们的例子中,并没有真正使用这些从节点,所以在某个从节点被删除时,我们只是释放缓存资源
      get_tasks();//下一步要进行的是任务的分配操作。
      break;
    default:
      LOG_ERROR(LOGCALLBACK(zh),
                "Something went wrong when checking workers: %s",
                rc2string(rc));
      break;
  }
}
为了获取任务信息,服务端进程需要获取/tasks的所有子节点,并获取自上次读取后新引入的任务列表信息,所以我们每次读取后需要区分获取的列表信息,否则可能会导致任务被分配两次。

void get_tasks() {
  zoo_awget_children(zh,
                     "/tasks",
                     tasks_watcher,
                     NULL,
                     tasks_completion,
                     NULL);
} void tasks_watcher(
    zhandle_t
    *
    zh,
    int type,
    int state,
    const char* path,
    void* watcherCtx) {
  if (type == ZOO_CHILD_EVENT) {
    assert(!strcmp(path, "/tasks"));
    get_tasks();//如果任务列表发生变化,再次获取任务列表信息。
  } else {
    LOG_INFO(LOGCALLBACK(zh),
             "Watched event: ",
             type2string(type));
  }
} void tasks_completion(
    int rc,
    const struct String_vector* strings,
    const void* data) {
  switch (rc) {
    case ZCONNECTIONLOSS:
    case ZOPERATIONTIMEOUT:
      get_tasks();
      break;
    case ZOK:
      LOG_DEBUG(LOGCALLBACK(zh), "Assigning tasks");
      struct String_vector* tmp_tasks = added_and_set(strings, &tasks);
      assign_tasks(tmp_tasks);//将还没有被分配的任务进行分配操作。
      free_vector(tmp_tasks);
      break;
    default:
      LOG_ERROR(LOGCALLBACK(zh),
                "Something went wrong when checking tasks: %s",
                rc2string(rc));
      break;
  }
}


5、任务分配

任务分配包括任务信息的读取,选择一个从节点,通过在从节点任务列表中添加一个znode节点来分配该任务,最后将该任务从/tasks的子节点中删除。
下面的代码实现了这些基本操作流程,获取任务信息、分配任务、删除任务这些操作均采用异步函数实现,提供所需的完成函数。代码如下所示:

void assign_tasks(const struct String_vector* strings) {
  int i;
  for (i = 0; i < strings->count; i++) {
    get_task_data(strings->data[i]);//对于每个任务,首先获取任务详细信息。
  }
}
void get_task_data(const char*
                   task){
  if (task == NULL) return;
  char* tmp_task = strndup(task, 15);
  char* path = make_path(2, "/tasks/", tmp_task);
  zoo_aget(zh,
           path,
           0,
           get_task_data_completion,
           (const void*)tmp_task);//异步调用来获取任务详细信息。
  free(path);
}
struct task_info {	//用于保存任务上下文信息的结构体。
    char *value;
    int value_len;
    char *worker;
};

void get_task_data_completion(int rc, const char *value, int value_len,
                              const struct Stat *stat, const void *data){
    int worker_index;

    switch (rc) {
    case ZCONNECTIONLOSS:
    case ZOPERATIONTIMEOUT:
        get_task_data((const char* )data);
        break;
    case ZOK:
        if (workers != NULL) {
            worker_index = (rand() % workers->count);	//随机选择一个从节点,将任务分配给这个从节点。
            struct task_info *new_task;		//创建一个新的task_info类型的变量来保存任务的详细信息。
            new_task = ( struct task_info *)malloc(sizeof(struct task_info));

            new_task->name = (char*)data;
            new_task->value = strndup(value, value_len);
            new_task->value_len = value_len;
            
            const char * worker_string  = workers->data[worker_index];
            new_task->worker = strdup(worker_string);
            task_assignment(new_task);	//获取任务信息后,我们完成了任务分配操作。
        }
        break;

    default:
        LOG_ERROR(LOCALLBACK(zh),
                    "something went wrong when checking the master lock:%s", rc2string(rc));
        break;
    }
}

到目前为止,以上代码已经完成了任务信息的读取和从节点的选择,下一步,需要创建用于表示任务分配的znode节点:

void task_assignment(struct task_info* task) {
  char* path = make_path(4, "/assign/", task->worker, "/", task->name);
  zoo_acreate(zh,
              path,
              task->value,
              task->value_len,
              &ZOO_OPEN_ACL_UNSAFE,
              0,
              task_assignment_completion,
              (const void*)task);	//创建表示任务分配的znode节点。
  free(path);
} void task_assignment_completion(
    int rc, const char*
    value, const void*
    data) {
  switch (rc) {
    case ZCONNECTIONLOSS:
    case ZOPERATIONTIMEOUT:
      task_assignment((struct task_info*)data);
      break;
    case ZOK:
      if (data != NULL) {
        char* del_path = "";
        del_path = make_path(2, "/tasks/",
                             ((struct task_info*)data)->name);
        if (del_path != NULL) {
          delete_pending_task(del_path);	//一旦任务分配完成,主节点进程从待分配任务列表中删除这个任务。
        }
        free(del_path);
        free_task_info((struct task_info*)data);	//我们为task_info类型的实例分配了堆存储空间,因此我们现在可以释放该堆内存。
      }
      break;
    case ZNODEEXISTS:
      LOG_DEBUG(LOGCALLBACK(zh),
                "Assignment has alreasy been created: %s",
                value);
      break;
    default:
      LOG_ERROR(LOGCALLBACK(zh),
                "Something went wrong when checking the master lock: %s",
                rc2string(rc));
      break;
  }
}
最后一步为删除/tasks下的任务节点,使/tasks节点下只有没有被分配的任务。
void delete_pending_task(const char* path) {
  if (path == NULL) return;
  char* tmp_path = strdup(path);
  zoo_adelete(zh,
              tmp_path,
              -1,
              delete_task_completion,
              (const void*)tmp_path);//异步删除任务节点。
} void delete_task_completion(int rc, const void*
                              data) {
  switch (rc) {
    case ZCONNECTIONLOSS:
    case ZOPERATIONTIMEOUT:
      delete_pending_task((const char*)data);
      break;
    case ZOK:
      free((char*)data);	//任务节点被成功删除后,没有什么其他需要做的,所以我们这里只是释放我们之前为存储路径字符串而分配的内存空间。
      break;
    default:
      LOG_ERROR(LOGCALLBACK(zh),
                "Something went wrong when deleting task: %s",
                rc2string(rc));
      break;
  }
}

6、单线程与多线程客户端

ZooKeeper的发行包中,对于C语言组件提供了两个选项,多线程和单线程版本。建议使用多线程版本,因为单线程版本只是因为一些历史原因。
int initialized = 0;
int run = 0;
fd_set rfds, wfds, efds;
FD_ZERO(& rfds);
FD_ZERO(& wfds);
FD_ZERO(& efds);
while (!is_expired()) {
  int fd;
  int interest;
  int events;
  struct timeval tv;
  int rc;
  zookeeper_interest(zh, &fd, &interest, &tv);//返回该客户端所关心的事件信息
  if (fd != -1) {
    if (interest & ZOOKEEPER_READ) {//添加ZOOKEEPER_READ事件到关注的事件集合中
      FD_SET(fd, &rfds);
    } else {
      FD_CLR(fd, &rfds);
    }
    if (interest & ZOOKEEPER_WRITE) {//添加ZOOKEEPER_WRITE事件到关注的事件集合中
      FD_SET(fd, &wfds);
    } else {
      FD_CLR(fd, &wfds);
    }
  } else {
    fd = 0;
  }
/*
* Master call to get a ZooKeeper handle.
*/
  if (!initialized) {
    if (init(argv[1])) {//此处为启动应用,获取到一个ZooKeeper句柄
      LOG_ERROR(("Error while initializing the master: ", errno));
    }
    initialized = 1;
  }
/*
* The next if block contains
* calls to bootstrap the master
* and run for master. We only
* get into it when the client
* has established a session and
* is_connected is true.
*/
  if (is_connected() && !run) {//当主节点进程连接到ZooKeeper服务器后,就会收到ZOO_CONNECTED_EVENT事件,该部分会执行引导流程,并执行竞选主节点流程
    LOG_INFO(("Connected, going to bootstrap and run for master"));
/*
* Create parent znodes
*/
    bootstrap();
/*
* Run for master
*/
    run_for_master();
    run = 1;
  }
  rc = select(fd+1, &rfds, &wfds, &efds, &tv);//采用select方式来等待新事件
  events = 0;
  if (rc > 0) {
    if (FD_ISSET(fd, &rfds)) {
      events |= ZOOKEEPER_READ;//指示文件描述符fd发生了读事件
    }
    if (FD_ISSET(fd, &wfds)) {
      events |= ZOOKEEPER_WRITE;//指示文件描述符fd发生了写事件
    }
  }
  zookeeper_process(zh, events);//处理其他ZooKeeper事件,需要处理监视事件和完成函数
}
这个事件循环将会关注相关的ZooKeeper事件,如回调和会话事件等。
使用多线程版本库时,编译客户端应用程序需要使用-l zookeeper_mt,并通过-DTHREADED定义THREADED宏选项。
在代码中,在编译多线程库版本的程序时,通过THREADED宏定义会指示在调用执行时需要使用的代码片段。
使用单线程版本库时,使用-l zookeeper_st来编译程序,同时不能指定-DTHREADED选项。在ZooKeeper的发行包中,你可以通过编译步骤指示来使用对应的库。


参考:《Zookeeer分布式过程协同技术详解》 机械工业出版社

你可能感兴趣的:(c/c++,ZooKeeper)