【zookeeper】客户端 底层实现
来源:互联网 发布:数据库数据存储方式 编辑:程序博客网 时间:2024/06/03 11:17
首先看下我们是怎么用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,顺序就是接受顺序,然后处理完以后会按照接受顺序返回结果。不过还没看服务端的代码。
- 【zookeeper】客户端 底层实现
- zookeeper 客户端的实现
- ZooKeeper客户端--java实现
- Zookeeper JAVA客户端(Kotlin 实现) CRUD
- zookeeper-客户端
- Zookeeper客户端
- zookeeper客户端
- Zookeeper客户端
- Zookeeper客户端
- 客户端底层 Socket 实现IPV4 IPV6网络环境的兼容
- 使用Node.js实现一个简单的ZooKeeper客户端
- ZooKeeper客户端框架Curator实现屏障服务(Barrier)
- 使用Node.js实现一个简单的ZooKeeper客户端
- Thrift 客户端 C# ---实现zookeeper监视(1)
- 登录zookeeper客户端管理zookeeper
- Curator是Netflix开源的一套ZooKeeper客户端框架. Netflix在使用ZooKeeper的过程中发现ZooKeeper自带的客户端太底层, 应用方在使用的时候需要自己处理很多事情
- ZooKeeper学习笔记:使用zookeeper的API实现增删查改以及客户端的观察者模式
- ZooKeeper客户端命令
- expat & scew
- Brew命令
- JAVA形参和实参的区别
- 2.3Groovy灵活的参数初始化
- 第一篇帖子
- 【zookeeper】客户端 底层实现
- 2.4Groovy可变参数
- 复习
- 2.5Groovy使用多赋值(方法的结果返回给多个变量)
- Effective C++ 读书笔记_1:构建全空对象数组/带参对象数组/Operator /Placement/new/指针Cast/分配一片内存
- c++汉字与区位码互转换
- 2.7Groovy布尔求值
- linux iptables开放/关闭端口命令
- android添加菜单