导读
Zookeeper源码在当前各种开源框架中不算是多的,只有几万行,但是如果没有一个正确的指导或者方法,去读源码也是非常枯燥,没有头绪,而且非常容易放弃,笔者这一系列文章是笔者源码读下来的历程,梳理出来给想要读Zookeeper源码的后来者一个读源码的思路,以及更好的理解Zookeeper原理而写的。
文章会以顺序模式进行,会对单机下一个命令是怎么执行的、ACL原理、CloseSession,到集群模式下数据的同步、领导者选举等等核心功能进行一一讲解,并不会对每一个命令行进行细致的解说,因为命令执行的模式基本上是一致的,而且重复的讲解只是在充字数,浪费篇幅和时间。而且内容太多也会让新学者产生距离感,不利于顺利读下去,笔者也会根据笔者在源码中记录的笔记,酌情调整篇幅长度,尽量在阅读上减少大家的疲劳感。为了方便大家观看这一系列的博客,我已经整理了已个目录出来,大家可以直接到这里查看【Zookeeper 源码解读系列目录】。
我们要学的是Zookeeper中使用的多线程的思路,以及对分布式更深层次的理解,而不是机械的把源码全部看一遍。本系列所有的代码会按照整体->分解->整体这样的思路去进行,一般情况下对源码的分析会在源码贴入以后进行,而源码之中也会有笔者阅读源码的心得和笔记,用来帮助大家更好的理解源码,因此阅读本文的最佳方法是对照着源码一步一步的阅读。因为源码解读的过程中肯定会涉及到方法的跳入和跳出,会酌情对冗余代码删减,并且分割上层代码,在分割中加入必要的下层代码,力求还原阅读源码的真实步骤,所以笔者才会强烈建议对照源码阅读本文,如果还没有安装源码,可以移步我的文章Zookeeper-3.4.14 Idea 源码安装与运行 Win10手把手教程。那么我们接下来正式开始探究源码的过程。
单机模式之一:
很多人面对一个框架,会比较迷茫的一点就是要从哪里入手,其实一个很好的入手点就是架构中/bin
目录下的可执行文件。为什么这么说呢,因为这里是程序的入口,也是我们最好下手的地方,那么我们就先从Zookeeper的客户端开始 zkCli.cmd
,笔者用的是win10系统,其实zkCli.sh
内容也差不多,并不影响使用Linux的同学阅读本文,Zookeeper在Windows和Linux上的区别,也就只有这些可执行程序入口不一样的区别了,所以完全不用担心自己看的和笔者的不一样。无论是 zkCli.cmd
还是zkCli.sh
我们打开并且要找到的其实就只有org.apache.zookeeper.ZooKeeperMain
这句话,它就是程序的执行入口,那么我们就要从源码入手,看看当我们运行客户端的时候源码里到底做了什么事情。
首先,我们既然要找到这个ZooKeeperMain.java
类或者文件,那就要找到文件的位置,根据版本的不同,新版本和老版本位置是不一样的。新版本3.4.14
、3.5.*
和3.6.*
是独立出来,比如3.4.14
,在解压后的\zookeeper-3.4.14\zookeeper-server\src\main\java\org\apache\zookeeper
下,之前的一些的版本(比如3.4.13
)在解压后的\zookeeper-3.4.13\src\java\main\org\apache\zookeeper
下。目前这些版本大体的核心代码差别不多,并不会对阅读源码理解Zookeeper构建思想造成很大的影响,这里请根据自己的喜好下载,笔者并不会推荐使用哪一个固定的版本,但是要说明下笔者的版本是3.4.14
,因为笔者就职的公司使用的是这一版本。
ZkCli.cmd启动后是怎么连接的
既然我们说到入手最好的地方是客户端,那么我们就打开zkCli.cmd
看看里面是什么样子。
@echo off
setlocal
call "%~dp0zkEnv.cmd"
set ZOOMAIN=org.apache.zookeeper.ZooKeeperMain
call %JAVA% "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%"
-cp "%CLASSPATH%" %ZOOMAIN% %*
endlocal
里面可以说是非常的简单,首先声明了ZOOMAIN变量制定了运行类,然后call调起了Java命令,指定了%CLASSPATH%
和我们要运行的类%ZOOMAIN%
,所以其实我们运行脚本的时候就是在运行的ZooKeeperMain这个类,那么既然是运行的Java类,那就必然有一个入口方法,众所周知Java的入口方法就是main()方法,于是我们就有了第一个目标:打开ZooKeeperMain.java找到main()方法看看里面写了什么。
public static void main(String args[]) throws KeeperException, IOException, InterruptedException
{
//初始化
ZooKeeperMain main = new ZooKeeperMain(args);
main.run();
}
从代码里看,这里就只有一个初始化的命令,那么我们接这跳入new ZooKeeperMain(args)
这里探究。
public ZooKeeperMain(String args[]) throws IOException, InterruptedException {
cl.parseOptions(args);//校验输入的命令
System.out.println("Connecting to " + cl.getOption("server"));
connectToZK(cl.getOption("server"));//建立连接
}
在这个里面,只做了两个工作,首先对我们的输入的命令进行校验,然后建立连接。这里的命令校验就是在看我们输入的命令有没有-server,-timeout之类,我们先略过,重点在于如何建立连接的 connectToZK(cl.getOption("server"));
看名字就知道这里就是连接ZK的,而且可以传入一个参数cl.getOption("server")
这里你可以传入hostname:port,这里就是命令行传入的参数,Zk的命令行可以指定server参数就是在这里指定的,那么进入这个方法。
protected void connectToZK(String newHost) throws InterruptedException, IOException {
if (zk != null && zk.getState().isAlive()) {
zk.close();
}
host = newHost;
//这里是ZK的只读模式,我们以后再说,如果以后碰到,非必要也会作为冗余代码略去
boolean readOnly = cl.getOption("readonly") != null;
//zk原生客户端
zk = new ZooKeeper(host,
Integer.parseInt(cl.getOption("timeout")),
new MyWatcher(), readOnly);
}
这里就发现了一个重点new ZooKeeper(***)
,这个类其实就是我们Zookeeper的原生客户端,那么我们就进入看下客户端是怎么启动的
ZooKeeper原生客户端
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,boolean canBeReadOnly) throws IOException
{
/**log代码略过**/
watchManager.defaultWatcher = watcher;
//解析传入的连接字符串,分割host等等,包装了我们使用的hostnames
ConnectStringParser connectStringParser = new ConnectStringParser(connectString);
HostProvider hostProvider = new StaticHostProvider(connectStringParser.getServerAddresses());
//拿到一个NIO的client,getClientCnxnSocket()
cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
hostProvider, sessionTimeout, this, watchManager,
getClientCnxnSocket(), canBeReadOnly);
cnxn.start();
}
首先,可以看到我们Java代码里创建Client的时候所绑定的Watcher保存在这里,然后解析连接字符串,当我们自己创建ZooKeeper客户端的时候是可以这样localhost:2181, localhost:2182, localhost:2183
传入好多个host的,ConnectStringParser(connectString)
这个方法就是用来分割hostname的,然后做成一个hostname的list,再包装成HostProvider对象供后面使用,这个方法里的重点是在cnxn = new ClientCnxn(***);
。可以把ClientCnxn理解为客户端的一个管链接的上下文,在调用这个构造方法之前还在参数中调用了getClientCnxnSocket()
这个方法,我们进入看下。
private static ClientCnxnSocket getClientCnxnSocket() throws IOException {
String clientCnxnSocketName = System.getProperty(ZOOKEEPER_CLIENT_CNXN_SOCKET);
if (clientCnxnSocketName == null) {//如果是null,就赋值ClientCnxnSocketNIO类名字
clientCnxnSocketName = ClientCnxnSocketNIO.class.getName();
}
try {//做一个ClientCnxnSocketNIO的实例转为ClientCnxnSocket返出去
return (ClientCnxnSocket)Class.forName(clientCnxnSocketName)
.getDeclaredConstructor().newInstance();
} catch (Exception e) {
IOException ioe = new IOException("Couldn't instantiate " + clientCnxnSocketName);
ioe.initCause(e);
throw ioe;
}
}
这个方法就是拿到ClientCnxnSocket这个抽象类的实例,首先我们看到系统属性里面取出一个值String clientCnxnSocketName = System.getProperty(ZOOKEEPER_CLIENT_CNXN_SOCKET);
如果clientCnxnSocketName==null
就是说系统属性里面没有拿到,就把ClientCnxnSocketNIO
这个类的名字给它 clientCnxnSocketName = ClientCnxnSocketNIO.class.getName()
,其实ZOOKEEPER_CLIENT_CNXN_SOCKET
这个属性的默认值就是"zookeeper.clientCnxnSocket"
,那么其实这里两者拿到的都是一样的东西,都是ClientCnxnSocketNIO
这个类的名字,因为ClientCnxnSocketNIO
是ClientCnxnSocket
这个抽象类的实现类,即便使用上面的默认值,也是ClientCnxnSocketNIO
真正处理的,这里只是做了一个保险机制。有了这个类的名字以后,可以看到调用了newInstance()
给ClientCnxnSocket
生成一个实例然后把这个实例返回去出去了,此处就很明显了:zk连接使用的socket对象其实是ClientCnxnSocketNIO
提供的,并且zk底层的连接客户端和服务端用的是NIO的思想实现的传输层,NIO是什么以后如果有时间,我会另写一篇文章介绍,请不懂的同学先自己百度一下。我们接着返回上层:
public ZooKeeper(***) throws IOException
{ /**代码略过**/
cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
hostProvider, sessionTimeout, this, watchManager,
getClientCnxnSocket(), canBeReadOnly);
cnxn.start();
}
在getClientCnxnSocket()
里面生成实例以后在传入ClientCnxn类里面去
public ClientCnxn(***) throws IOException {
//进入this
this(chrootPath, hostProvider, sessionTimeout, zooKeeper, watcher,
clientCnxnSocket, 0, new byte[16], canBeReadOnly);
}
进入this后看到里面就是一些属性和计算超时的时间
public ClientCnxn(***) {
this.zooKeeper = zooKeeper;
/**初始化属性代码略过**/
this.chrootPath = chrootPath;
//计算timeout时间,只读模式等等
connectTimeout = sessionTimeout / hostProvider.size();
readTimeout = sessionTimeout * 2 / 3;
readOnly = canBeReadOnly;
//最关键的两个属性
sendThread = new SendThread(clientCnxnSocket);
eventThread = new EventThread();
}
SendThread和EventThread初识
撇去这些初始化的步骤以后,最关键的就是SendThread
和EventThread
,那么这两个线程是做什么用的呢?我们一会儿再看,但是有一点我们的ClientCnxn到这里就已经初始化完了,所以我们可以跳出去,执行cnxn.start();
了,进入start():
public void start() {
sendThread.start();
eventThread.start();
}
进入后发现,就是启动了刚刚new的两个线程类SendThread
和EventThread
。那我们就按照顺序先去SendThread
里面看下在这个线程中做了什么事情。所以我们跳到SendThread.run()
方法里。这个方法里面的内容很长,我们分段去研究,我们先看第一部分。
创建NIO实例
public void run() {
//clientCnxnSocket这个实例就是new SendThread传入的NIO实例
clientCnxnSocket.introduce(this,sessionId);
/**类似的初始化,略**/
while (state.isAlive()) {
try {
if (!clientCnxnSocket.isConnected()) {//如果socket还没有接,就进入这个if里面做连接
if(!isFirstConnect){//如果不是第一次就会进入这个if里面
try {
Thread.sleep(r.nextInt(1000));
} catch (InterruptedException e) {
LOG.warn("Unexpected exception", e);
}
}
/**略过**/
//第一次连接rwServerAddress==null跳过if往下
if (rwServerAddress != null) {
serverAddress = rwServerAddress;
rwServerAddress = null;
} else {
serverAddress = hostProvider.next(1000);
}
startConnect(serverAddress);//进行socket连接
clientCnxnSocket.updateLastSendAndHeard();
}
/***暂时略***/
}
}
}
首先clientCnxnSocket这个实例就是new SendThread传入的socket NIO的实例,略过一些初始化代码,到了一个while (state.isAlive())
,这里面state.isAlive()
就是在判断clientCnxnSocket是不是存活的,点进入就可以发现它的默认值是state = States.NOT_CONNECTED
,其实如果这个state不是CLOSED
也不是AUTH_FAILED
那么就是存活的,这部分逻辑在isAlive(){return this != CLOSED && this != AUTH_FAILED;}
方法里,在我们实例化的时候它的默认值NOT_CONNECTED
这也可以认为是存活的。Zk的重试机制其实也就是这个while无限的循环配合里面的try-catch以及flag一起实现的。那么下面的if(!clientCnxnSocket.isConnected())
就是判断连接了,接着我们是不是第一次连接呢?假设我们现在是首次连接的,那么就会直接跳到serverAddress = hostProvider.next(1000);
这里就会取出刚刚传入的某一个地址,然后startConnect(serverAddress);
去连接这个serverAddress
地址。下面我们先看下是怎么进行连接的:
private void startConnect(InetSocketAddress addr) throws IOException { // 开始连接了
saslLoginFailed = false;
state = States.CONNECTING;//首先修改连接状态
//给这个连接取一个随机名字
setName(getName().replaceAll("\\(.*\\)", "(" + addr.getHostName() +":" + addr.getPort()+")"));
if (ZooKeeperSaslClient.isEnabled()) {
/**非主流程模式,略**/
}
}/**略**/
//开始连接,使用NIO socket连接,也是在这里修改的连接标记
clientCnxnSocket.connect(addr);
}
首先修改连接状态为连接中(CONNECTING),然后给这个连接一个随机名字,跳过非主流程,就直接到了clientCnxnSocket.connect(addr);
,那么我们就找到这个方法,这个方法是个接口,但是还记得我们说的clientCnxnSocket
这个实例是NIO实现的,所以我们得去ClientCnxnSocketNIO.connect(***)里面:
@Override
void connect(InetSocketAddress addr) throws IOException {
SocketChannel sock = createSock();//在这里创建socket
try {
registerAndConnect(sock, addr);//然后注册和连接
} catch (IOException e) {
/**略**/
}
/**略**/
}
进入后我们看到socket在这里创建了,然后进行了注册和连接,进入看下:
void registerAndConnect(SocketChannel sock, InetSocketAddress addr) throws IOException {
//注册一个连接的事件
sockKey = sock.register(selector, SelectionKey.OP_CONNECT);
//然后就去连接地址addr
boolean immediateConnect = sock.connect(addr);
if (immediateConnect) { //如果立刻成功了
//调用primeConnection(),修改首次标记isFirstConnect=false,记住这里zk的重试机制会讲到。
sendThread.primeConnection();
}
}
在这个方法里,我们注册了socket连接事件,然后去连接目标地址addr,如果立刻连接成功了,就在primeConnection()
里面修改连接标记isFirstConnect=false
等等,这里就超出了主流程,所以这个方法我们略过先不说。到这里我们其实就已经可以先总结一下到目前为止客户端启动都做了什么:
客户端启动,入口是ZookeeperMain
,然后里面会
1. 初始化Zookeeper
2. 初始化ClientCnxn,在初始化过程中会
3. 初始化两个线程SendThread和EventThread
4. Start()启动SendThread建立socket连接,成功后就可以传输数据了
main线程的启动
我们到这里就基本上把连接讲完了,SendThread.run()
后面我们略过的代码先不看,我们现在只分析连接,那么我们先回头来看我们的客户端,包括我们的ZookeeperMain
这个类,其实我们一层层的点进来,首先要做的就是连接,如果连接做好了,我们要返回到哪里去接着看源码呢?最终我们还是要返回我们的入口main()方法里,为了方便,我还贴出来这个方法
public static void main(String args[]) throws ***Exception
{
//到目前为止,我们一直停在初始化这里-->ZooKeeperMain(args)
ZooKeeperMain main = new ZooKeeperMain(args);
//初始化结束,开始run
main.run();
}
到目前位置我们都是在new ZooKeeperMain(args)
这里面,如果连接初始化都结束了,那么我们就要开始运行main.run()
了,那么我们就进入这个主类的run()
方法里:
void run() throws ***Exception {
if (cl.getCommand() == null) {
/**略**/
try {//Java的命令行工具,用这两个工具才能识别输入的java语句
Class<?> consoleC = Class.forName("jline.ConsoleReader");
Class<?> completorC = Class.forName("org.apache.zookeeper.JLineZNodeCompletor");
/**略**/
while ((line = (String)readLine.invoke(console, getPrompt())) != null) {
executeLine(line);//执行客户端输入的命令行,凡是输入的文字行都会送到这里来执行
}
} catch (***) {
/**略**/
}
/**略**/
} else {
processCmd(cl);
}
}
省略掉冗余代码后,有两个工具类要注意下,即Java的命令行工具:jline.ConsoleReader
和org.apache.zookeeper.JLineZNodeCompletor
用这两个工具才能识别输入的Java语句,再往下到while循环,executeLine(line);
执行客户端输入的命令行,循环里面还会调用一个getPrompt()
的方法,它其实就是客户端前面的提示信息[zk: localhost:2181(CONNECTED) 1]
输入代码的前面的那一串,里面也只有一行return "[zk: " + host + "("+zk.getState()+")" + " " + commandCount + "] ";
,过了这一小点,我们来看下executeLine(line)
里面写了什么:
public void executeLine(String line)
throws InterruptedException, IOException, KeeperException {
if (!line.equals("")) {//命令行不为空,开始执行
cl.parseCommand(line);
addToHistory(commandCount,line);//加入历史命令
processCmd(cl);//处理命令,在这里处理输入进来的指令行
commandCount++;
}
}
命令不是空,先校验一下,然后把 命令加入到系统的命令history里面,然后开始处理命令,所以我们着重要看processCmd(cl)
这个里面的代码,处理命令:
protected boolean processCmd(MyCommandOptions co) throws ***Exception
{
try {
return processZKCmd(co);//返回执行命令
} catch (***Exception e) {
/**一堆异常注释掉**/
}
return false;
}
进入以后我们发现也没有什么,只有一堆异常,注释掉,所以继续进入processZKCmd(co)
:
protected boolean processZKCmd(MyCommandOptions co) throws ***Exception
{
Stat stat = new Stat();
String[] args = co.getArgArray();
String cmd = co.getCommand();//拿到命令
/**对命令验证,略过**/
//这里判断输入的是什么字符串
if (cmd.equals("quit")) {
/***/
} else if (cmd.equals("redo") && args.length >= 2) {
/***/
} /**这里有很多else if,暂时与本文无关略过**/
if (cmd.equals("create") && args.length >= 3) {//创建命令
//创建后进入开始创建
int first = 0;
CreateMode flags = CreateMode.PERSISTENT;
//识别参数,顺序还是临时等等
if ((args[1].equals("-e") && args[2].equals("-s"))||(args[1]).equals("-s")&&(args[2].equals("-e"))) {
//-e -s 临时顺序节点
first+=2;
flags = CreateMode.EPHEMERAL_SEQUENTIAL;
} else if (args[1].equals("-e")) {
//-e 临时节点
first++;
flags = CreateMode.EPHEMERAL;
} else if (args[1].equals("-s")) {
//-s 持久化的顺序节点
first++;
flags = CreateMode.PERSISTENT_SEQUENTIAL;
}
/**ACL相关暂时略过**/
path = args[first + 1];
//开始正式创建新的节点-->跳转到Zookeeper类中
String newPath = zk.create(path, args[first+2].getBytes(), acl,flags);
//打印,这就是创建成功后打印提示信息的地方,"Created /node_name"
System.err.println("Created " + newPath);
} else if (cmd.equals("delete") && args.length >= 2) {
/**这里有很多else if,暂时与本文无关略过**/
}
return watch;
}
以Create命令为例探究Zk命令执行的过程
进入以后看到了很多if语句,这里有很多判断,其实看到这些判断就知道这些逻辑是干什么的,像quit,redo,create,delete,get等等就是在这里,我们以"Create"
命令为例子,我们在客户端创建一个节点就会走到if (cmd.equals("create") && args.length >= 3)
这里来,我们现在开始分析当我们创建一个节点这部分逻辑会做什么。它会根据命令里所包含的标记,来决定要做什么事情,比如create命令里有 -e -s
那么就创建一个临时顺序节点CreateMode.EPHEMERAL_SEQUENTIAL
,如果有-s
就是持久化的顺序节点CreateMode.PERSISTENT_SEQUENTIAL
,那么这里的flag就是节点的类型。准备完毕后就会调用客户端的create(***)
方法,又会跳到Zookeeper这个类的create(***)
方法里面去了:
public String create(final String path, byte data[], List<ACL> acl, CreateMode createMode) throws ***Exception
{
final String clientPath = path; //传入的路径,比如/rr
PathUtils.validatePath(clientPath, createMode.isSequential()); //验证路径
final String serverPath = prependChroot(clientPath); //给传入的路径加上一个根节点
RequestHeader h = new RequestHeader(); //验证完成以后,构建一个请求头
//传入要进行测操作,ZooDefs.OpCode.create里面存的就是对应命令的代码,create=1
h.setType(ZooDefs.OpCode.create);
CreateRequest request = new CreateRequest(); //创建一个Create的请求
CreateResponse response = new CreateResponse(); //创建一个Create的response
request.setData(data);//数据内容赋值
request.setFlags(createMode.toFlag());//数据内容类型
request.setPath(serverPath);//节点名字
/**ACL检查和设置,略**/
ReplyHeader r = cnxn.submitRequest(h, request, response, null);
if (r.getErr() != 0) { //有错误就抛出异常
throw KeeperException.create(KeeperException.Code.get(r.getErr()), clientPath);
}
//没有问题把路径返回出去,然后显示出来
if (cnxn.chrootPath == null) {
return response.getPath();
} else {
return response.getPath().substring(cnxn.chrootPath.length());
}
}
比如,我们执行了一个create /rr 1
的命令
我们分析一下,首先我们的create方法是要有一个路径进来的,所以这里面的path就是在客户端输入create命令后所写的path,首先会验证一下path是不是合法的,是不是顺序节点,这里比较复杂,我们单独再说,然后给路径加入一个根节点,根节点一般没有值,也不做讨论,验证完成以后,构建一个请求头RequestHeader
,这里就比较重要了,因为在创建节点的时候是向服务端发送一个请求的,所以首先构建一个请求头RequestHeader
,然后这个请求的type就是创建OpCode.create
,然后进行节点请求的初始化,最后又用到了cnxn
,其实就是咱们之前说的NIO.submitRequest(***);
这时候Zk的客户端又用到了ClientCnxn
这个类来提交这个请求,这里就是去把这个请求通过socket给传输到我们的服务端去。如果拿到这个头部以后,检车有没有错误信息,有错错误就抛出异常,没有错误就把这个路径给response返回出去,跳出create()方法,执行打印System.err.println("Created " + newPath);
显示出来。
所以着重我们就还是要回到ClientCnxn.submitRequest(***)这个方法去,这就是真正提交请求的地方,不管任何命令(create、set等等),最终都会走到这个方法里去,然后由这个方法发送给服务端,并且根据服务端的返回结果来构造response再返回出去使用。下面我们就去看下submitRequest(***)
这个方法做了什么。
public ReplyHeader submitRequest(RequestHeader h, Record request,
Record response, WatchRegistration watchRegistration)
throws InterruptedException {
ReplyHeader r = new ReplyHeader();
Packet packet = queuePacket(h, r, request, response, null, null, null,null, watchRegistration);
synchronized (packet) {
while (!packet.finished) {
packet.wait();
}
}
return r;
}
首先传入刚刚创建好的请求头,请求体,然后进行一个queuePacket(***)
,这是要干嘛呢?我们进入这个方法看下是在做什么:
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration)
{
Packet packet = null;
synchronized (outgoingQueue) {
packet = new Packet(h, r, request, response, watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
if (!state.isAlive() || closing) {
conLossPacket(packet);
} else {
if (h.getType() == OpCode.closeSession) {
closing = true;
}
outgoingQueue.add(packet);//把命令加入queue
}
}
sendThread.getClientCnxnSocket().wakeupCnxn();
return packet;
}
进入以后我们发现其实所有传入的内容,header,request之类的都包装成为一个包(packet),然后这些packet都会被加入到一个队列outgoingQueue
里(outgoingQueue.add(packet)
)。所以什么是Zk的提交请求呢,就是把请求转变为一个packet,然后加到outgoingQueue
队列里面去,那么问题就来了:这个队列,肯定是要有消费者的,什么时候取出outgoingQueue
里的数据呢?这个问题先放这里。既然方法已经执行完了,我们就跳出这里,回到submitRequest()
:
public ReplyHeader submitRequest(RequestHeader h, Record request,
Record response, WatchRegistration watchRegistration)
throws InterruptedException {
ReplyHeader r = new ReplyHeader();
Packet packet = queuePacket(h, r, request, response, null, null, null,null, watchRegistration);
synchronized (packet) {
while (!packet.finished) {
packet.wait();
}
}
return r;
}
我们发现后面就直接使用synchronized (packet){***}
给阻塞住了。其实可以这样想一下,我们把请求加入到队列里面以后发送到服务端,按照逻辑来说客户端是要等待服务端的结果的,现在这个方法里的ReplyHeader
也好,response
也好都是没有值的,而这个值都应该是服务端给我们的,所以说这里就先wait()住,把线程阻塞直到服务端把结果告诉我了以后再唤醒,此时就是在等待Sever端传来的数据。等到拿到值以后,就可以return r
返回出去了。
总结
到这里调用create
命令其实在客户端就已经告一段落,需要等待服务器端返回才能接着往下走,那么我们先画个ZkCli.cmd启动流程图总结一下,然后在下一篇【Zookeeper 源码解读系列, 单机模式(二)】里探秘后续,服务端是怎样接收并返回数据的: