zk深入之客户端源码

zk客户端端组件

zk客户端由下面几个组件组成:

  • Zookeeper实例: 客户端入口
  • ClientWatchManager: 客户端Watcher管理器
  • HostProvider: 客户端地址列表管理器
  • ClientCnxn: 客户端核心线程,其内部包含SendThread和EventThread,前者是一个IO 线程,,负责zk客户端与服务端之间的网络IO通信,后者是一个事件线程,主要负责对客户端端事件进行处理。


    image.png
image.png
 public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
            boolean canBeReadOnly)
        throws IOException
    {
        LOG.info("Initiating client connection, connectString=" + connectString
                + " sessionTimeout=" + sessionTimeout + " watcher=" + watcher);

        watchManager.defaultWatcher = watcher;

        ConnectStringParser connectStringParser = new ConnectStringParser(
                connectString);
        HostProvider hostProvider = new StaticHostProvider(
                connectStringParser.getServerAddresses());
        cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
                hostProvider, sessionTimeout, this, watchManager,
                getClientCnxnSocket(), canBeReadOnly);
        cnxn.start();
    }

初始化阶段

  1. 初识化zk对象,通过调用zk的构造方法来实例化一个zk对象,在初始化过程中,会创建一个客户端的Watcher管理器,ClientWatchManager
  2. 设置会话默认Wathcer, 如果在Zookeeper的构造方法中传入一个Watcher对象的话,zk就会将这个watcher对象保存在ZKWatcherManager的defaultWatcher中,作为整个客户端会话期间默认的Watcher。
  3. 构造zk服务器地址列表管理器: HostProvider, 对于构造方法中传入的服务器地址,客户端会将其存放在服务器地址列表管理器HostProvider中。
  4. 创建并初始化客户端网络连接器: ClientCnxn: zk客户端首先会穿件ClientCnxn,用来管理客户端与服务器的网络交互,ClientCnxn连接器的底层IO处理器是ClientCnxnSocket,也会创建该对象,此外,在创建ClientCnxn的同时,还会初始化客户端两个核心队列outgoingQueue和pendingQueue,分别作为客户端的请求发送队列和服务端相应的等待队列。
  5. 初始化SendThread和EventThread
    客户端会创建两个核心网络线程,SendThread和EventThread,前者用于管理客户端和服务端之间的所有IO, 后者用于进行客户端的事件处理。同时,客户端还会将ClientCnxnSocket分配给SendThread作为底层网络IO处理器,并初始化EventThread的待处理事件队列waitingEvents,用于存放所有等待被客户端处理的事件。

会话创建阶段

  1. 启动SendThread和EventThread
    SendThread首先会判断当前客户端的状态,进行一系列清理工作,为客户端发送”会话创建“请求做准备。
  2. 获取一个服务器地址
    在开始创建TCP连接之前,SendThread首先需要获取一个zk服务器的目标地址,通常是从HostProvider中随机获取一个地址,然后委托给ClientCnxnSocket去创建与zk服务器之间的TCP连接。
  3. 创建TCP连接
    获取到一个服务器地址后,ClientCnxnSocket负责和服务器创建一个TCP长连接
  4. 构造ConnectRequest请求:
    在TCP连接创建完毕后,只是纯粹地从网络TCP层面完成了客户端与服务端之间的Socket连接,远未完成Zookeeper客户端的会话创建。SendThread会负责根据当前客户端的实际配置,构造出一个ConnectRequest请求,该请求代表了客户端试图与服务器创建一个会话,同时,zk客户端还会进一步将该请求包装成网络I/O层的Packet对象,放入请求发送队列outgoingQueue中去
  5. 发送请求
    当客户端请求准备完毕后,就可以开始向服务端发送请求了。ClientCnxnSocket负责从outgoingQueue中取出一个待发送的Packet对象,将其序列化成ByteBuffer后,向服务端进行发送。

响应处理阶段

  1. 接受服务端响应
    ClientCnxnSocket 接收到服务端的响应后,会首先判断当前的客户端状态是否是已被初始化,如果尚未完成初始化,就可以认为该响应一定是会话创建请求的响应,直接交由readConnectResult方法来处理该响应。
  2. 处理Response
    ClientCnxnSocket会对接收到的服务端响应进行反序列化,得到ConnectResponse对象,并从中获取到zk服务器分配的会话sessionId
  3. 连接成功
    连接成功后,一方面需要通知SendThread线程,进一步对客户端进行会话参数的设置,包括readTimeout和connectTimeout等,并更新客户端状态,另一方面,需要通知地址管理器HostProvider当前成功连接的服务器地址
  4. 生成事件: SyncConnected-None, 为了能够让上层应用感知到会话的成功创建,SendThread会生成一个事件SyncConnected-None,代表客户端与服务器会话创建成功,并将该事件传递给EventThread线程
  5. 查询Watcher, EventThread线程收到事件后,会从ClientWatchManager管理器中查询出对应的Watcher,针对SyncConnected-None事件,找出步骤2中存储的默认Watcher,然后将其放到EventThread的waitingEvents队列中去。
  6. 处理事件
    EventThread不断地从waitingEvents队列中取出待处理的Watcher对象,接着直接调用该对象的process方法,以达到触发Watcher的目的。
服务器地址列表

在使用zk的构造方法时,用户传入zk的服务器地址列表,即connectString参数,比如:192.168.0.1:2181,192.168.0.2:2181... , zk客户端是允许我们将服务器的所有地址都配置在一个字符串上,那么zk客户端在连接服务器的过程中,是如何从这个服务器地址列表中选择服务器地址的呢?顺序访问?还是随机访问?

zk客户端在收到这个服务器地址列表后,使用ConnectStringParser对象封装起来,
image.png
public final class ConnectStringParser {
    private static final int DEFAULT_PORT = 2181;//默认端口

    private final String chrootPath;//客户端命名空间

    private final ArrayList serverAddresses = new ArrayList();//地址列表

    /**
     * 
     * @throws IllegalArgumentException
     *             for an invalid chroot path.
     */
    public ConnectStringParser(String connectString) {
        // parse out chroot, if any
        int off = connectString.indexOf('/');
        if (off >= 0) {
            String chrootPath = connectString.substring(off);
            // ignore "/" chroot spec, same as null
            if (chrootPath.length() == 1) {
                this.chrootPath = null;
            } else {
                PathUtils.validatePath(chrootPath);
                this.chrootPath = chrootPath;
            }
            connectString = connectString.substring(0, off);
        } else {
            this.chrootPath = null;
        }
        String hostsList[] = connectString.split(",");
        for (String host : hostsList) {
            int port = DEFAULT_PORT;
            int pidx = host.lastIndexOf(':');
            if (pidx >= 0) {
                // otherwise : is at the end of the string, ignore
                if (pidx < host.length() - 1) {
                    port = Integer.parseInt(host.substring(pidx + 1));//端口
                }
                host = host.substring(0, pidx);//ip
            }
            serverAddresses.add(InetSocketAddress.createUnresolved(host, port));
        }
    }

    public String getChrootPath() {
        return chrootPath;
    }

    public ArrayList getServerAddresses() {
        return serverAddresses;
    }
}

ConnectStringParser对connectString做两个主要处理,解析chrootPath和保存服务器地址列表。 如果一个zk客户端设置了Chroot,那么该客户端对服务器的任何操作都会被限制在其自己的命名空间下。比如我们希望为应用X分配/apps/X下的所有子节点,该应用可以将其所有zk客户端的Chroot设置为/apps/X,192.168.0.1:2181/apps/X , 这些客户端与zk发起的所有请求中相关的节点路径,都将是一个相对路径,相对于/apps/X。通过设置chroot,我们能够将一个客户端应用与zk服务器的一棵子树对应,在那些多个应用共享一个zk集群的场景下,这对于实现不同应用之间的相互隔离非常有帮助。ConnectStringParser解析出Chroot后保存在chrootPath属性中。

HostProvider

在ConnectStringParser解析器中会对服务器地址做一个简单的处理,将ip和port封装成一个InetSocketAddress对象,以ArrayList形式保存在ConnectStringParser的serverAdderss属性中,然后进一步封装在StaticHostProvider类中,

public interface HostProvider {
    public int size();

    /**
     * The next host to try to connect to.
     * 
     * For a spinDelay of 0 there should be no wait.
     * 
     * @param spinDelay
     *            Milliseconds to wait if all hosts have been tried once.
     */
    public InetSocketAddress next(long spinDelay);

    /**
     * Notify the HostProvider of a successful connection.
     * 
     * The HostProvider may use this notification to reset it's inner state.
     */
    public void onConnected();
}
image.png

StaticHostProvider是zk客户端对HostProvider的默认实现,针对ConnectStringParser.serverAddresses集合中那些没有被解析的服务器地址,StaticHostProvider首先会对这些地址逐个进行解析,再放到serverAddresses中去,同时使用Collections工具类中的shuffle方法来将这个服务器地址列表进行随机的打散

public final class StaticHostProvider implements HostProvider {
    private static final Logger LOG = LoggerFactory
            .getLogger(StaticHostProvider.class);

    private final List serverAddresses = new ArrayList(
            5);//再解析一次

    private int lastIndex = -1;//记录循环队列中当前正在连接的服务器地址的下标

    private int currentIndex = -1;//记录循环队列中当前遍历到的下标

    /**
     * Constructs a SimpleHostSet.
     * 
     * @param serverAddresses
     *            possibly unresolved ZooKeeper server addresses
     * @throws UnknownHostException
     * @throws IllegalArgumentException
     *             if serverAddresses is empty or resolves to an empty list
     */
    public StaticHostProvider(Collection serverAddresses)
            throws UnknownHostException {
        for (InetSocketAddress address : serverAddresses) {
            InetAddress ia = address.getAddress();
            InetAddress resolvedAddresses[] = InetAddress.getAllByName((ia!=null) ? ia.getHostAddress():
                address.getHostName());
            /**
             * 针对ConnectStringParser.serverAddresses集合中哪些没有被解析的服务器地址
             * staticHostProvider首先会对这些地址逐个进行解析,然后再放入serverAddresses列表中去
             */
            for (InetAddress resolvedAddress : resolvedAddresses) {
                // If hostName is null but the address is not, we can tell that
                // the hostName is an literal IP address. Then we can set the host string as the hostname
                // safely to avoid reverse DNS lookup.
                // As far as i know, the only way to check if the hostName is null is use toString().
                // Both the two implementations of InetAddress are final class, so we can trust the return value of
                // the toString() method.
                if (resolvedAddress.toString().startsWith("/") 
                        && resolvedAddress.getAddress() != null) {
                    this.serverAddresses.add(
                            new InetSocketAddress(InetAddress.getByAddress(
                                    address.getHostName(),
                                    resolvedAddress.getAddress()), 
                                    address.getPort()));
                } else {
                    this.serverAddresses.add(new InetSocketAddress(resolvedAddress.getHostAddress(), address.getPort()));
                }  
            }
        }
        
        if (this.serverAddresses.isEmpty()) {
            throw new IllegalArgumentException(
                    "A HostProvider may not be empty!");
        }
        Collections.shuffle(this.serverAddresses);//随机打乱服务器地址顺序
    }

    public int size() {
        return serverAddresses.size();
    }

    public InetSocketAddress next(long spinDelay) {
        ++currentIndex;
        if (currentIndex == serverAddresses.size()) {
            currentIndex = 0;//循环队列的特性
        }
        if (currentIndex == lastIndex && spinDelay > 0) {//如果当前用的地址和上次用的地址一样,则等待spinDelay的时间
            try {
                Thread.sleep(spinDelay);
            } catch (InterruptedException e) {
                LOG.warn("Unexpected exception", e);
            }
        } else if (lastIndex == -1) {//第一次连接
            // We don't want to sleep on the first ever connect attempt.
            lastIndex = 0;
        }

        return serverAddresses.get(currentIndex);//注意serverAddresses是随机打乱过的服务器地址列表
    }

    public void onConnected() {
        lastIndex = currentIndex;//更新当前连接的服务器地址对应的下标
    }
}

获取可用的服务器地址
通过调用StaticHostProvider中的next方法,能够获取一个可用的服务器地址,这个next方法并非简单地从serverAddresses中依次获取一个服务器地址,而是先将随机打算后的服务器地址列表拼接成一个环形循环队列

image.png

比如客户端传入这样一个地址列表,"host1,host2,host3,host4,host5",经过一轮随机打散后,可能的顺序变为"host2,host4,host1,host5,host3", 并且形成上图的环形队列,HostProvider为该环形队列创建两个游标,currentIndex和lastIndex,currentIndex表示循环队列中当前遍历到的那个元素位置,lastIndex表示当前正在使用的服务器地址位置。总的来说,StaticHostProvider就是不断地从上图的环形地址列表队列中去获取一个地址。

几个小问题

StaticHostProvider中的小技巧,当前用的地址和上次用的地址一样,则等待spinDelay的时间的意义是什么?
参照HostProvider对该方法的注解

    /**
     * The next host to try to connect to.
     * 
     * For a spinDelay of 0 there should be no wait.
     * 
     * @param spinDelay
     *            Milliseconds to wait if all hosts have been tried once.
     */
    public InetSocketAddress next(long spinDelay);//返回一个zk服务器地址让客户端进行连接

就是说如果所有的hosts都试过了都没有连上(此时出现currentIndex == lastIndex),那么就sleep一段时间再说

客户端打乱服务器列表的意义是什么
打乱的意义就是避免单台zk服务器压力过大(next方法表示每次client只和一个zk server连接),接收了所有的请求,当所有client端记录的server顺序时一样的,就都发送到了相同的服务器,打乱顺序后可避免

ClientCnxn 网络I/O

ClientCnxn是zk客户端的核心工作类,负责维护客户端与服务端之间的网络连接并进行一些列网络通信。
Packet是ClientCnxn内部定义的一个协议层的封装,作为zk中请求与响应的载体

static class Packet {
              RequestHeader requestHeader;//请求头

        ReplyHeader replyHeader;//响应头

        Record request;//请求体

        Record response;//响应体

        ByteBuffer bb;//序列化之后的byteBuffer

        /** Client's view of the path (may differ due to chroot) **/
        String clientPath;//client节点路径,不含chrootPath
        /** Servers's view of the path (may differ due to chroot) **/
        String serverPath;//server节点路径,含chrootPath

        boolean finished;//是否结束(已经得到响应才能结束)

        AsyncCallback cb;//异步回调

        Object ctx;//上下文

        WatchRegistration watchRegistration;//注册的watcher

        public boolean readOnly;//只读
}

Packet有这么多属性,它们是否都会在客户端与服务端之间进行网络传输呢? 并不会, Packet的createBB()方法负责对Packet对象进行序列化,最终生成可用于底层网络传输的ByteBuffer对象,在这个序列化过程中,只会将requestHeader, request和readOnly三个属性进行序列化,其余属性不会进行与服务端之间的网络传输。

 public void createBB() {//序列化创建byteBuffer记录在bb字段中
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
                boa.writeInt(-1, "len"); // We'll fill this in later
                if (requestHeader != null) {
                    requestHeader.serialize(boa, "header");//序列化请求头,包含xid和type
                }
                if (request instanceof ConnectRequest) {
                    request.serialize(boa, "connect");
                    // append "am-I-allowed-to-be-readonly" flag
                    boa.writeBool(readOnly, "readOnly");
                } else if (request != null) {
                    request.serialize(boa, "request");//序列化request(对于特定请求如GetDataRequest,包含了是否存在watcher的标志位)
                }
                baos.close();
                this.bb = ByteBuffer.wrap(baos.toByteArray());
                this.bb.putInt(this.bb.capacity() - 4);
                this.bb.rewind();
            } catch (IOException e) {
                LOG.warn("Ignoring unexpected exception", e);
            }
        }

ClientCnxn中,有两个核心的队列,outgoingQueue和pendingQueue,分别代表客户端的请求发送队列和服务端的响应等待队列,outgoing队列是一个请求发送队列,专门用于存储那些需要发送到服务端的Packet集合,Pending队列是为了存储那些已经从客户端发送到服务端的,但是需要等待客户端响应的packet集合。

请求发送: 正常情况下,会从outgoingQueue队列中提取出一个可发送的Packet对象,同时生成一个客户端请求序号XID,并将其设置到Packet请求头中去,然后将其序列化后进行发送,请求发送完毕后,会立即将该Packet保存到pendingQueue队列中,以便等待服务端响应返回后进行相应的处理。

image.png
响应接收

客户端获取到来自服务端的完整响应数据后

  • 如果检测到客户端还尚未进行初始化,就说明当前客户端和服务端之间正在进行会话创建,就直接将接收到的ByteBuffer序列化成ConnectResponse对象。
  • 如果当前客户端已经处于正常的会话周期,并且接收到的服务端响应是一个事件,那么zk客户端将会将接收的ByteBUffer序列化成WatcherEvent对象,并将该事件放入待处理队列中。
  • 如果是一个常规的请求响应,比如create、getData等操作请求,就会从pendingQueue队列中取出一个Packet来进行响应的处理。

SendThread
SendThread是客户端ClientCnxn内部的一个核心的IO调度线程,用于管理客户端和服务端之间的所有网络IO操作。在zk客户端的实际运行过程中,一方面SendThread维护了客户端与服务端之间的会话生命周期,通过在一定周期频率内向服务端发送一个ping包来实现心跳检测,同时在会话期间,如果客户端与服务端之间出现tcp连接断开的情况,会自动且透明化完成重连操作。另一方面,SendThread管理了客户端所有的请求发送和响应接收操作,其将上层客户端API操作转换成相应的请求协议并发送到服务端,并完成对同步调用的返回和异步调用的回调,同时SendThread还负责将来自服务端的时间传递给EventThread去处理。

EventThread: 客户端ClientCnxn内部的另一个核心线程,负责客户端的事件处理,并触发客户端注册的Watcher监听,EventThread中有一个waitingEvents队列,用于临时存放那些需要被触发的Object,包括那些客户端注册watcher和异步接口中注册的回调器AyncCallback,同时EventThread会不断地从waitingEvents队列取出Object,识别出其具体类型(Watcher或者AsyncCallback)并分别调用process和processResult接口方法来实现对事件的触发和回调。

ClientCnxnSocket定义了底层Socket通信的 接口.默认是现实ClientCnxnSocketNIO.

private static final Logger LOG = LoggerFactory.getLogger(ClientCnxnSocket.class);

    protected boolean initialized;//是否初始化

    /**
     * This buffer is only used to read the length of the incoming message.
     */
    protected final ByteBuffer lenBuffer = ByteBuffer.allocateDirect(4);//仅仅用来读取 incoming message的长度

    /**
     * After the length is read, a new incomingBuffer is allocated in
     * readLength() to receive the full message.
     */
    protected ByteBuffer incomingBuffer = lenBuffer;
    protected long sentCount = 0;//send次数
    protected long recvCount = 0;//接收次数
    protected long lastHeard;//上次接收时间
    protected long lastSend;//上次发送时间
    protected long now;//当前时间
    protected ClientCnxn.SendThread sendThread;//客户端通信的发送线程

    /**
     * The sessionId is only available here for Log and Exception messages.
     * Otherwise the socket doesn't need to know it.
     */
    protected long sessionId;//仅仅用来辅助log和Exception记录用的
 void introduce(ClientCnxn.SendThread sendThread, long sessionId) {//设置sendThread以及sessionId
        this.sendThread = sendThread;
        this.sessionId = sessionId;
    }

    void updateNow() {//更新now时间
        now = System.currentTimeMillis();
    }

    int getIdleRecv() {//获取接收的闲置时间
        return (int) (now - lastHeard);
    }

    int getIdleSend() {//获取发送的闲置时间
        return (int) (now - lastSend);
    }

    long getSentCount() {//发送次数
        return sentCount;
    }

    long getRecvCount() {//接收次数
        return recvCount;
    }

    void updateLastHeard() {//更新最后一次监听的时间
        this.lastHeard = now;
    }

    void updateLastSend() {//更新最后一次发送的时间
        this.lastSend = now;
    }

    void updateLastSendAndHeard() {//同时更新最后一次监听和发送的时间
        this.lastSend = now;
        this.lastHeard = now;
    }

    protected void readLength() throws IOException {//读取incoming message的length
        int len = incomingBuffer.getInt();
        if (len < 0 || len >= ClientCnxn.packetLen) {//默认长度[0,4M]之间
            throw new IOException("Packet len" + len + " is out of range!");
        }
        incomingBuffer = ByteBuffer.allocate(len);//分配对应长度的空间
    }

    void readConnectResult() throws IOException {//读取connect的response
        if (LOG.isTraceEnabled()) {
            StringBuilder buf = new StringBuilder("0x[");
            for (byte b : incomingBuffer.array()) {
                buf.append(Integer.toHexString(b) + ",");
            }
            buf.append("]");
            LOG.trace("readConnectResult " + incomingBuffer.remaining() + " "
                    + buf.toString());
        }
        ByteBufferInputStream bbis = new ByteBufferInputStream(incomingBuffer);
        BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
        ConnectResponse conRsp = new ConnectResponse();
        conRsp.deserialize(bbia, "connect");

        // read "is read-only" flag
        boolean isRO = false;
        try {
            isRO = bbia.readBool("readOnly");//反序列化,看是否是只读的
        } catch (IOException e) {
            // this is ok -- just a packet from an old server which
            // doesn't contain readOnly field
            LOG.warn("Connected to an old server; r-o mode will be unavailable");
        }

        this.sessionId = conRsp.getSessionId();
        sendThread.onConnected(conRsp.getTimeOut(), this.sessionId,
                conRsp.getPasswd(), isRO);//sendThread完成connect时一些参数验证以及zk state更新以及事件处理
    }
子类ClientCnxnSocketNIO
private static final Logger LOG = LoggerFactory
            .getLogger(ClientCnxnSocketNIO.class);

    private final Selector selector = Selector.open();

    private SelectionKey sockKey;
@Override
    void connect(InetSocketAddress addr) throws IOException {//参数是某一个zk server的地址
        SocketChannel sock = createSock();
        try {
           registerAndConnect(sock, addr);//注册SelectionKey到zk server
        } catch (IOException e) {
            LOG.error("Unable to open socket to " + addr);
            sock.close();
            throw e;
        }
        initialized = false;//还没有初始化,connect ok了但是还读到server的response

        /*
         * Reset incomingBuffer
         */
        lenBuffer.clear();
        incomingBuffer = lenBuffer;
    }

里面调用了createSock和registerAndConnect方法,如下

 /**
     * create a socket channel.
     * @return the created socket channel
     * @throws IOException
     */
    SocketChannel createSock() throws IOException {//创建SocketChannel
        SocketChannel sock;
        sock = SocketChannel.open();
        sock.configureBlocking(false);//非阻塞
        sock.socket().setSoLinger(false, -1);
        sock.socket().setTcpNoDelay(true);
        return sock;
    }

    /**
     * register with the selection and connect
     * @param sock the {@link SocketChannel} 
     * @param addr the address of remote host
     * @throws IOException
     */
    void registerAndConnect(SocketChannel sock, InetSocketAddress addr) 
    throws IOException {
        sockKey = sock.register(selector, SelectionKey.OP_CONNECT);//注册,监听connect事件
        boolean immediateConnect = sock.connect(addr);
        if (immediateConnect) {//如果立即建立了连接
            sendThread.primeConnection();//client把watches和authData等数据发过去,并更新SelectionKey为读写
        }
    }

registerAndConnect中如果立即connect就调用sendThread.primeConnection();
如果没有立即connect上,那么就在下面介绍的doTransport中等待SocketChannel finishConnect再调用

client 和 server的网络交互
 @Override
    void doTransport(int waitTimeOut, List pendingQueue, LinkedList outgoingQueue,
                     ClientCnxn cnxn)
            throws IOException, InterruptedException {
        selector.select(waitTimeOut);//找到就绪的keys个数
        Set 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
        updateNow();
        for (SelectionKey k : selected) {
            SocketChannel sc = ((SocketChannel) k.channel());
            if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {//如果就绪的是connect事件,这个出现在registerAndConnect函数没有立即连接成功
                if (sc.finishConnect()) {//如果次数完成了连接
                    updateLastSendAndHeard();//更新时间
                    sendThread.primeConnection();//client把watches和authData等数据发过去,并更新SelectionKey为读写
                }
            } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {//如果就绪的是读或者写事件
                doIO(pendingQueue, outgoingQueue, cnxn);//利用pendingQueue和outgoingQueue进行IO
            }
        }
        if (sendThread.getZkState().isConnected()) {//如果zk的state是已连接
            synchronized(outgoingQueue) {
                if (findSendablePacket(outgoingQueue,
                        cnxn.sendThread.clientTunneledAuthenticationInProgress()) != null) {//如果有可以发送的packet
                    enableWrite();//允许写
                }
            }
        }
        selected.clear();//清空
    }

再次强调一下,outgoingQueue 是请求发送队列,是client存储需要被发送到server端的Packet队列
pendingQueue是已经从client发送,但是要等待server响应的packet队列

主要调用了doIO 以及 findSendablePacket方法

/**
     * @return true if a packet was received
     * @throws InterruptedException
     * @throws IOException
     */
    void doIO(List pendingQueue, LinkedList outgoingQueue, ClientCnxn cnxn)
      throws InterruptedException, IOException {
        SocketChannel sock = (SocketChannel) sockKey.channel();
        if (sock == null) {
            throw new IOException("Socket is null!");
        }
        if (sockKey.isReadable()) {//若读就绪
            int rc = sock.read(incomingBuffer);//读出len
            if (rc < 0) {//如果<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++;//接收次数+1
                    readLength();//获取len并给incomingBuffer分配对应空间
                } else if (!initialized) {//如果client和server的连接还没有初始化
                    readConnectResult();//读取connect 回复
                    enableRead();//启用读
                    if (findSendablePacket(outgoingQueue,
                            cnxn.sendThread.clientTunneledAuthenticationInProgress()) != null) {//如果有可以发送的packet
                        // Since SASL authentication has completed (if client is configured to do so),
                        // outgoing packets waiting in the outgoingQueue can now be sent.
                        enableWrite();//允许写,因为有要发送的packet
                    }
                    lenBuffer.clear();
                    incomingBuffer = lenBuffer;//还原incomingBuffer
                    updateLastHeard();
                    initialized = true;//client和server连接初始化完成
                } else { //如果已连接,并且已经给incomingBuffer分配了对应len的空间
                    sendThread.readResponse(incomingBuffer);//读取response
                    lenBuffer.clear();
                    incomingBuffer = lenBuffer;//还原incomingBuffer
                    updateLastHeard();
                }
            }
        }
        if (sockKey.isWritable()) {//若写就绪
            synchronized(outgoingQueue) {
                Packet p = findSendablePacket(outgoingQueue,
                        cnxn.sendThread.clientTunneledAuthenticationInProgress());//找到可以发送的Packet

                if (p != null) {
                    updateLastSend();
                    // If we already started writing p, p.bb will already exist
                    if (p.bb == null) {
                        if ((p.requestHeader != null) &&
                                (p.requestHeader.getType() != OpCode.ping) &&
                                (p.requestHeader.getType() != OpCode.auth)) {
                            p.requestHeader.setXid(cnxn.getXid());
                        }
                        p.createBB();//如果packet还没有生成byteBuffer,那就生成byteBuffer
                    }
                    sock.write(p.bb);
                    if (!p.bb.hasRemaining()) {
                        sentCount++;
                        outgoingQueue.removeFirstOccurrence(p);//从待发送队列中取出该packet
                        if (p.requestHeader != null
                                && p.requestHeader.getType() != OpCode.ping
                                && p.requestHeader.getType() != OpCode.auth) {
                            synchronized (pendingQueue) {
                                pendingQueue.add(p);//加入待回复的队列
                            }
                        }
                    }
                }
                if (outgoingQueue.isEmpty()) {
                    // No more packets to send: turn off write interest flag.
                    // Will be turned on later by a later call to enableWrite(),
                    // from within ZooKeeperSaslClient (if client is configured
                    // to attempt SASL authentication), or in either doIO() or
                    // in doTransport() if not.
                    disableWrite();//如果没有要发的,就禁止写
                } else if (!initialized && p != null && !p.bb.hasRemaining()) {
                    // On initial connection, write the complete connect request
                    // packet, but then disable further writes until after
                    // receiving a successful connection response.  If the
                    // session is expired, then the server sends the expiration
                    // response and immediately closes its end of the socket.  If
                    // the client is simultaneously writing on its end, then the
                    // TCP stack may choose to abort with RST, in which case the
                    // client would never receive the session expired event.  See
                    // http://docs.oracle.com/javase/6/docs/technotes/guides/net/articles/connection_release.html
                    disableWrite();
                } else {
                    // Just in case
                    enableWrite();
                }
            }
        }
    }

大概流程为:

主要分为读或者写两个case
  读:
    没有初始化就完成初始化
    读取len再给incomingBuffer分配对应空间
    读取对应的response
  写:
    找到可以发送的Packet
    如果Packet的byteBuffer没有创建,那么就创建
    byteBuffer写入socketChannel
    把Packet从outgoingQueue中取出来,放到pendingQueue中
    相关读写的处理

第一次只读len,然后给incomingBuffer分配对应的空间
第二次再把剩下的内容读完

 private Packet findSendablePacket(LinkedList outgoingQueue,
                                      boolean clientTunneledAuthenticationInProgress) {//bool参数是表示 如果当前client和server在处理sasl的权限
        synchronized (outgoingQueue) {
            if (outgoingQueue.isEmpty()) {//如果没有要发送的
                return null;
            }
            if (outgoingQueue.getFirst().bb != null // If we've already starting sending the first packet, we better finish
                || !clientTunneledAuthenticationInProgress) {//如果有要发送的 或者 没有在处理sasl的权限
                return outgoingQueue.getFirst();
            }

            // Since client's authentication with server is in progress,
            // send only the null-header packet queued by primeConnection().
            // This packet must be sent so that the SASL authentication process
            // can proceed, but all other packets should wait until
            // SASL authentication completes.
            ListIterator iter = outgoingQueue.listIterator();
            while (iter.hasNext()) {
                Packet p = iter.next();
                if (p.requestHeader == null) {//如果在处理sasl的权限,那么只有requestHeader为null的Packet可以被发送
                    // We've found the priming-packet. Move it to the beginning of the queue.
                    iter.remove();
                    outgoingQueue.add(0, p);
                    return p;
                } else {
                    // Non-priming packet: defer it until later, leaving it in the queue
                    // until authentication completes.
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("deferring non-priming packet: " + p +
                                "until SASL authentication completes.");
                    }
                }
            }
            // no sendable packet found.
            return null;
        }
    }

大概流程:

如果没有要发送的就返回null
如果有要发送的或者client没有在处理sasl的权限,那么就拿队列第一个
如果在处理sasl,那么遍历队列,把requestHeader为null的放到队头,返回该packet
 @Override
    boolean isConnected() {//这个只是说SelectionKey有没有初始化,来标示,并不是真正的Connected
        return sockKey != null;
    }

你可能感兴趣的:(zk深入之客户端源码)