首先看下我们是怎么用zk客户端的。我们一般都会采用如下的代码:
ZooKeeper zk = new ZooKeeper("127.0.0.1:2183", 5000, new DefaultHandler());
然后创建的ZooKeeper实例是我们与服务端通讯的接口。所以从这个类开始看。
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
throws IOException
{
LOG.info("Initiating client connection, connectString=" + connectString
+ " sessionTimeout=" + sessionTimeout + " watcher=" + watcher);
watchManager.defaultWatcher = watcher;
cnxn = new ClientCnxn(connectString, sessionTimeout, this, watchManager);
cnxn.start();
}
这是我们使用使用的构造方法,有几个重载版本。这里需要传入三个参数:服务器的ip端口,等待timeout和一个默认的回调接口。zk是有事件机制的,关于事件会在另一篇中研究,这里只需要知道在zk构造方法中传入的watch是一个默认的watch,watchManger是zk客户端管理watch的类。默认的watch也可以在后续的其他事件注册中使用,把相应的boolean参数设置为true即可。
接下来有一个cnxn实例,从start方法可以看到这是一个线程。这个是zk客户端很重要的一个组件,用于处理zk的io,默认是一个java nio的实现。所以这里着重要分析的是zk的io处理实现。
所以,纵观ZooKeeper类,只是实现了客户端的业务相关的api,而底层的io是交给cnxn这个类来处理的。下面就挑一个zk客户端方法来看,比如最常见的getData方法,这个方法有同步异步两个版本,这里就看同步的:
public byte[] getData(final String path, Watcher watcher, Stat stat)
throws KeeperException, InterruptedException
{
final String clientPath = path;
PathUtils.validatePath(clientPath);
// the watch contains the un-chroot path
WatchRegistration wcb = null;
if (watcher != null) {
wcb = new DataWatchRegistration(watcher, clientPath);
}
final String serverPath = prependChroot(clientPath);
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.getData);
GetDataRequest request = new GetDataRequest();
request.setPath(serverPath);
request.setWatch(watcher != null);
GetDataResponse response = new GetDataResponse();
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
if (r.getErr() != 0) {
throw KeeperException.create(KeeperException.Code.get(r.getErr()),
clientPath);
}
if (stat != null) {
DataTree.copyStat(response.getStat(), stat);
}
return response.getData();
}
首先根据watcher参数来注册watch,然后就构造了一个request对象,封装了getData请求,接着就调用了cnxn类的submitRequest方法来发送这个请求,最后可以发现,这个方法是阻塞的,会等待服务端发送回结果,最后返回请求的data。基本上所有的zk客户端的api实现逻辑都是这个模板。所以我们需要研究的是cnxn这个实例的类。
这个类名是ClientCnxn,专门用于客户端的io逻辑。
这个类里面有两个重要的内部类,一个是SendThread一个EventThread。看名字也可以清楚它们是两个实现了runnable接口的类。其中SendThread用于处理io,使用的是nio。EventThread用于处理收到的事件。还有两个重要的变量outgoingQueue和pendingQueue,outgoingQueue用于存放待发送的packet,一旦一个packet被发送,就会从outgoingQueue中移除,加入到pendingQueue中,等待服务端的返回。这里的顺序是可以保证的,先发送的那么就先进入pending,tcp的顺序可以保证每一次从服务端拿到的请求响应一定是pending中第一个packet的相应,这样完美解决了顺序和匹配的问题。
先看SendThread。
class SendThread extends Thread {
SelectionKey sockKey;
private final Selector selector = Selector.open();
final ByteBuffer lenBuffer = ByteBuffer.allocateDirect(4);
ByteBuffer incomingBuffer = lenBuffer;
boolean initialized;
private long lastPingSentNs;
long sentCount = 0;
long recvCount = 0;
这是它的成员变量,有一个selector,会周期性地处理客户端端口的io事件。
我们只贴出关键性部分,也就是select以后得到了key的部分:
selector.select(to);
Set<SelectionKey> selected;
synchronized (this) {
selected = selector.selectedKeys();
}
// Everything below and until we get back to the select is
// non blocking, so time is effectively a constant. That is
// Why we just have to do this once, here
now = System.currentTimeMillis();
for (SelectionKey k : selected) {
SocketChannel sc = ((SocketChannel) k.channel());
if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {
if (sc.finishConnect()) {
lastHeard = now;
lastSend = now;
primeConnection(k);
}
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
if (outgoingQueue.size() > 0) {
// We have something to send so it's the same
// as if we do the send now.
lastSend = now;
}
if (doIO()) {
lastHeard = now;
}
}
}
for循环会处理一次select到的全部key。基本上是判断事件类型,如果是连接成功,就调用primeConnection方法来处理连接事件。否则如果是read和write事件,那么就调用doIO方法来处理io事件。这里我们只看io部分。
doIO方法也很长。
内部分为了主要的两个case,一个是读事件,一个是写事件。
先看读事件:
if (sockKey.isReadable()) {
int rc = sock.read(incomingBuffer);
if (rc < 0) {
throw new EndOfStreamException(
"Unable to read additional data from server sessionid 0x"
+ Long.toHexString(sessionId)
+ ", likely server has closed socket");
}
if (!incomingBuffer.hasRemaining()) {
incomingBuffer.flip();
if (incomingBuffer == lenBuffer) {
recvCount++;
readLength();
} else if (!initialized) {
readConnectResult();
enableRead();
if (!outgoingQueue.isEmpty()) {
enableWrite();
}
lenBuffer.clear();
incomingBuffer = lenBuffer;
packetReceived = true;
initialized = true;
} else {
readResponse();
lenBuffer.clear();
incomingBuffer = lenBuffer;
packetReceived = true;
}
}
}
发生读事件意味着服务端返回了一个数据,应该是客户端之前发送的请求。在这个处理读事件的case里面,首先调用了read方法,把字节流读到了成员变量buffer里面。然后就根据当前的状态判断如何处理独到的数据。如果说当前的客户端还没有初始化,那么说明什么,说明这些返回的数据是与客户端连接有关的,调用readConnectResult方法来处理连接过程。否则,这就是客户端请求返回的结果,调用readResponse方法处理。
再来看写事件:
if (sockKey.isWritable()) {
synchronized (outgoingQueue) {
if (!outgoingQueue.isEmpty()) {
ByteBuffer pbb = outgoingQueue.getFirst().bb;
sock.write(pbb);
if (!pbb.hasRemaining()) {
sentCount++;
Packet p = outgoingQueue.removeFirst();
if (p.header != null
&& p.header.getType() != OpCode.ping
&& p.header.getType() != OpCode.auth) {
pendingQueue.add(p);
}
}
}
}
}
如果是写事件,那么就意味着可以发送一次客户端的请求,也就是从outgoingQueue中取出一个packet,发送即可,再加入到pedingQueue。
if (outgoingQueue.isEmpty()) {
disableWrite();
} else {
enableWrite();
}
最后修改select监听的事件,主要看两个队列是否有元素。
接下来看readResponse方法,也很长,不过很清楚。首先是一些特殊的case处理。
if (replyHdr.getXid() == -2) {
// -2 is the xid for pings
第一个是对ping的处理。
if (replyHdr.getXid() == -4) {
// -4 is the xid for AuthPacket
if(replyHdr.getErr() == KeeperException.Code.AUTHFAILED.intValue()) {
zooKeeper.state = States.AUTH_FAILED;
eventThread.queueEvent( new WatchedEvent(Watcher.Event.EventType.None,
Watcher.Event.KeeperState.AuthFailed, null) );
}
if (LOG.isDebugEnabled()) {
LOG.debug("Got auth sessionid:0x"
+ Long.toHexString(sessionId));
}
return;
}
第二个是对auth数据的处理。
if (replyHdr.getXid() == -1) {
// -1 means notification
if (LOG.isDebugEnabled()) {
LOG.debug("Got notification sessionid:0x"
+ Long.toHexString(sessionId));
}
WatcherEvent event = new WatcherEvent();
event.deserialize(bbia, "response");
// convert from a server path to a client path
if (chrootPath != null) {
String serverPath = event.getPath();
if(serverPath.compareTo(chrootPath)==0)
event.setPath("/");
else
event.setPath(serverPath.substring(chrootPath.length()));
}
WatchedEvent we = new WatchedEvent(event);
if (LOG.isDebugEnabled()) {
LOG.debug("Got " + we + " for sessionid 0x"
+ Long.toHexString(sessionId));
}
eventThread.queueEvent( we );
return;
}
第三个是对事件的处理,意味着注册是事件发生了,这时服务端返回相应的事件,客户端所做的就是把事件放入到一个事件queue中,让EventThread来处理。以上三种特殊的case处理完以后,就是请求数据的返回。比如getData等。
if (pendingQueue.size() == 0) {
throw new IOException("Nothing in the queue, but got "
+ replyHdr.getXid());
}
Packet packet = null;
synchronized (pendingQueue) {
packet = pendingQueue.remove();
}
首先从一个pendingQueue中拿出一个待处理的packet,那么这次服务器返回的数据就是这个packet想要的。
try {
if (packet.header.getXid() != replyHdr.getXid()) {
packet.replyHeader.setErr(
KeeperException.Code.CONNECTIONLOSS.intValue());
throw new IOException("Xid out of order. Got "
+ replyHdr.getXid() + " expected "
+ packet.header.getXid());
}
packet.replyHeader.setXid(replyHdr.getXid());
packet.replyHeader.setErr(replyHdr.getErr());
packet.replyHeader.setZxid(replyHdr.getZxid());
if (replyHdr.getZxid() > 0) {
lastZxid = replyHdr.getZxid();
}
if (packet.response != null && replyHdr.getErr() == 0) {
packet.response.deserialize(bbia, "response");
}
if (LOG.isDebugEnabled()) {
LOG.debug("Reading reply sessionid:0x"
+ Long.toHexString(sessionId) + ", packet:: " + packet);
}
} finally {
finishPacket(packet);
}
然后就会把相应的结果设置到packet的response中。最后调用finishPacket方法来唤醒客户端。所以这个是阻塞的方法。
finishPacket方法中只是调用了一下notify方法:
private void finishPacket(Packet p) {
if (p.watchRegistration != null) {
p.watchRegistration.register(p.replyHeader.getErr());
}
if (p.cb == null) {
synchronized (p) {
p.finished = true;
p.notifyAll();
}
} else {
p.finished = true;
eventThread.queuePacket(p);
}
}
那么再看一下是zk是如何发送一个packet的。就像前面说的,ZooKeeper的几乎所有业务方法都是调用cnxn的submit方法来发送请求的:
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;
}
这个方法会构造一个packet,加入到cnxn的outgoingQueue中,然后调用一个wait方法阻塞在这里,知道被notify,再返回结果。到目前为止,应该明白了zk的客户端是如何处理请求响应了。也是zk客户端io的处理过程。
接下来是事件的处理,主要在EventThread类里。
有一个阻塞队列用于存放所有的在上述readResponse里收到的event:
class EventThread extends Thread {
private final LinkedBlockingQueue<Object> waitingEvents =
new LinkedBlockingQueue<Object>();
接下来看核心的run方法:
public void run() {
try {
isRunning = true;
while (true) {
Object event = waitingEvents.take();
if (event == eventOfDeath) {
wasKilled = true;
} else {
processEvent(event);
}
if (wasKilled)
synchronized (waitingEvents) {
if (waitingEvents.isEmpty()) {
isRunning = false;
break;
}
}
}
} catch (InterruptedException e) {
LOG.error("Event thread exiting due to interruption", e);
}
LOG.info("EventThread shut down");
}
每一次从queue里面拿一个事件出来,然后调用processEvent函数处理。
processEvent方法很长,但是基本是很多的if case,用于处理不同的事件类型。因为在submit一个packet的时候,我们会注册回调接口。所以这个时候process就直接从packet里面拿出回调接口处理就好。
总结就是,ClientCnxn类里面会开两个线程一个处理io一个处理事件,io有两个队列,事件有一个队列。
这是按照这个思路写的一个客户端:http://blog.csdn.net/u010900754/article/details/78391710
关于顺序的思考,首先客户端的顺序肯定没有问题,因为进入outgoing的顺序就是整个包的顺序,每一次都是从outgoing里拿一个发送,然后就进了pending,所以这个顺序是没问题的,客户端会保持这个packet的顺序,一旦进入了outgoing。然后tcp会保证接受顺序,也就是客户端先发哪一个,服务端就会先接受哪一个。所以服务端接受的顺序也和进入outgoing的顺序一样。那么服务端返回结果的顺序就不一定了,因为有的请求可能需要的处理时间长,有的短,完全有可能后收到的请求先处理完,但是肯定还是按照接受顺序来返回的,因为如果不是这样,那么pedingQueue就混乱了。所以猜测服务端也会有一个queue,顺序就是接受顺序,然后处理完以后会按照接受顺序返回结果。不过还没看服务端的代码。