zookeeper 客户端源码解读(一)
只能讲个大概,细节之处还是要自己点源码
1入口,ZooKeeperMain.main
打开 zkCli.sh 这个文件,可以看到,我们运行这个脚本,实际上是运行的一个 Java 程序。
这个程序在哪里呢?就在 zookeeper 解压后的文件夹里的一个 jar 包里:
所以 zookeeper 底层使用 java 写的。
用 Idea 打开这个 ZooKeeperMain 类:
它运行起来做了两件事:
- 传参给本类的构造函数创建了对象
- 执行了该对象的 run() 方法
1.1 ZooKeeperMain
cl 是什么是这个类很重要的一个属性,如果读者想认真了解客户端源码,一定要先看这篇博文:zookeeper 客户端源码之 MyCommandOptions,这对理解如何解析命令行很有帮助。
1.1.1 MyCommandOptions.parseOptions
1.1.2 ZooKeeperMain.connectToZK
1.1.2.1 ZooKeeper
cnxn 应该是 connection 的意思。
1.1.2.1.1ClientCnxn
1.1.2.1.2 ClientCnxn.start
很简单,就是让这两个线程进入就绪状态。
到这里,这条分支可以总结一下:
1.2 ZooKeeperMain.run
void run() throws KeeperException, IOException, InterruptedException {
//如果在登陆的时候,没有用指令
if (cl.getCommand() == null) {
System.out.println("Welcome to ZooKeeper!");
boolean jlinemissing = false;
// only use jline if it's in the classpath
try {
Class<?> consoleC = Class.forName("jline.ConsoleReader");
Class<?> completorC =
Class.forName("org.apache.zookeeper.JLineZNodeCompletor");
System.out.println("JLine support is enabled");
Object console =
consoleC.getConstructor().newInstance();
Object completor =
completorC.getConstructor(ZooKeeper.class).newInstance(zk);
Method addCompletor = consoleC.getMethod("addCompletor",
Class.forName("jline.Completor"));
addCompletor.invoke(console, completor);
String line;
Method readLine = consoleC.getMethod("readLine", String.class);
//只有有命令行,就不断地执行
while ((line = (String)readLine.invoke(console, getPrompt())) != null) {
executeLine(line);
}
} catch (ClassNotFoundException e) {
LOG.debug("Unable to start jline", e);
jlinemissing = true;
} catch (NoSuchMethodException e) {
LOG.debug("Unable to start jline", e);
jlinemissing = true;
} catch (InvocationTargetException e) {
LOG.debug("Unable to start jline", e);
jlinemissing = true;
} catch (IllegalAccessException e) {
LOG.debug("Unable to start jline", e);
jlinemissing = true;
} catch (InstantiationException e) {
LOG.debug("Unable to start jline", e);
jlinemissing = true;
}
//如果有 jline 就用上面的解析命令行,如果没有,就用下面的解析命令行
if (jlinemissing) {
System.out.println("JLine support is disabled");
BufferedReader br =
new BufferedReader(new InputStreamReader(System.in));
String line;
//这种情况也是不断地执行命令行
while ((line = br.readLine()) != null) {
executeLine(line);
}
}
} else {
// Command line args non-null. Run what was passed.
//如果登陆的时候用换行做了指令,将指令传给processCmd执行
processCmd(cl);
}
}
这段源码看起来很复杂,其实就是:
- 在登陆的时候没有 command,就用 executeLine 执行命令行
- 在登陆的时候就有 command,就将 cl 传给 processCmd 执行
很大一段都是在尝试使用 jline,我也不知道这是个什么东西,反正登录信息里是有的:
1.2.1 ZooKeeperMain.executeLine
public void executeLine(String line)
throws InterruptedException, IOException, KeeperException {
if (!line.equals("")) {
//通过正则解析命令行
cl.parseCommand(line);
addToHistory(commandCount,line);
//其实还是用 processCmd 解析命令行
processCmd(cl);
commandCount++;
}
}
1.2.2 ZooKeeperMain.processCmd
protected boolean processCmd(MyCommandOptions co)
throws KeeperException, IOException, InterruptedException
{
try {
return processZKCmd(co);
} catch (IllegalArgumentException e) {
//...
}catch (KeeperException.BadVersionException e) {
System.err.println("version No is not valid : "+e.getPath());
}
return false;
}
可以看到,这个类主要是尝试调用 processZkCmd,如果不成功,就捕获异常。
1.2.2.1 ZooKeeperMain.processZKCmd
这个方法很长,代码没有贴完,但是很容易看出来,这里就是针对 parseCommand 处理后的 cl,进行所有可能的判断,对每种 command 的合法值,进行对应的处理。
protected boolean processZKCmd(MyCommandOptions co)
throws KeeperException, IOException, InterruptedException
{
Stat stat = new Stat();
String[] args = co.getArgArray();
String cmd = co.getCommand();
if (args.length < 1) {
usage();
return false;
}
if (!commandMap.containsKey(cmd)) {
usage();
return false;
}
boolean watch = args.length > 2;
String path = null;
List<ACL> acl = Ids.OPEN_ACL_UNSAFE;
LOG.debug("Processing " + cmd);
if (cmd.equals("quit")) {
System.out.println("Quitting...");
zk.close();
System.exit(0);
} else if (cmd.equals("redo") && args.length >= 2) {
Integer i = Integer.decode(args[1]);
if (commandCount <= i || i < 0){ // don't allow redoing this redo
System.out.println("Command index out of range");
return false;
}
cl.parseCommand(history.get(i));
if (cl.getCommand().equals( "redo" )){
System.out.println("No redoing redos");
return false;
}
history.put(commandCount, history.get(i));
processCmd( cl);
} else if (cmd.equals("history")) {
for (int i=commandCount - 10;i<=commandCount;++i) {
if (i < 0) continue;
System.out.println(i + " - " + history.get(i));
}
} else if (cmd.equals("printwatches")) {
if (args.length == 1) {
System.out.println("printwatches is " + (printWatches ? "on" : "off"));
} else {
printWatches = args[1].equals("on");
}
} else if (cmd.equals("connect")) {
if (args.length >=2) {
connectToZK(args[1]);
} else {
connectToZK(host);
}
}
// Below commands all need a live connection
if (zk == null || !zk.getState().isAlive()) {
System.out.println("Not connected");
return false;
}
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"))) {
first+=2;
flags = CreateMode.EPHEMERAL_SEQUENTIAL;
} else if (args[1].equals("-e")) {
first++;
flags = CreateMode.EPHEMERAL;
} else if (args[1].equals("-s")) {
first++;
flags = CreateMode.PERSISTENT_SEQUENTIAL;
}
if (args.length == first + 4) {
acl = parseACLs(args[first+3]);
}
path = args[first + 1];
String newPath = zk.create(path, args[first+2].getBytes(), acl,
flags);
System.err.println("Created " + newPath);
} else if (cmd.equals("delete") && args.length >= 2) {
//...
这其中,也有很重要的一个方法 create :
1.2.2.1.1 ZooKeeper.create
public String create(final String path, byte data[], List<ACL> acl,
CreateMode createMode)
throws KeeperException, InterruptedException
{
final String clientPath = path;
PathUtils.validatePath(clientPath, createMode.isSequential());
final String serverPath = prependChroot(clientPath);
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.create);
CreateRequest request = new CreateRequest();
CreateResponse response = new CreateResponse();
request.setData(data);
request.setFlags(createMode.toFlag());
request.setPath(serverPath);
if (acl != null && acl.size() == 0) {
throw new KeeperException.InvalidACLException();
}
request.setAcl(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());
}
}
注意 submitRequest 这个方法,它是一个阻塞方法:
public ReplyHeader submitRequest(RequestHeader h, Record request,
Record response, WatchRegistration watchRegistration)
throws InterruptedException {
ReplyHeader r = new ReplyHeader();
//打包成Packet并加入outgoingQueue
Packet packet = queuePacket(h, r, request, response, null, null, null,
null, watchRegistration);
synchronized (packet) {
while (!packet.finished) {
packet.wait();
}
}
return r;
}
程序运行到这里,就进入等待唤醒状态了。
在 wait 方法前还有一个 queuePacket 方法:
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration)
{
Packet packet = null;
// Note that we do not generate the Xid for the packet yet. It is
// generated later at send-time, by an implementation of ClientCnxnSocket::doIO(),
// where the packet is actually sent.
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 the client is asking to close the session then
// mark as closing
if (h.getType() == OpCode.closeSession) {
closing = true;
}
//LinkedList<Packet>
outgoingQueue.add(packet);
}
}
sendThread.getClientCnxnSocket().wakeupCnxn();
return packet;
}
到这个分支,可以总结一下:
那么这个主线程,如果调用 zk 的 create 方法后进入阻塞状态,肯定是要被之前 SendThread 和 EventThread 中的一个唤醒的。
那么,SendThread 和 EventThread 究竟做了什么,还没有讲,会在下一篇博客介绍,先放一张图,大概了解一下:
参考:deer——Zookeeper