前言:

无论是使用框架还是使用原生的Zookeeper.java来创建客户端连接,本质上都是使用Zookeeper来连接。在Zookeeper-3.4.13的源码中不但包含服务端的代码还提供了客户端的代码,我们先来分析下客户端的代码,了解下客户端是如何创建对服务端的连接,后续我们再来看下其是如何发送请求。

1.Zookeeper解析

public class ZooKeeper {
 
    // 只有两个属性,主要用的就是cnxn,后续会重点介绍
    protected final ClientCnxn cnxn;
    private static final Logger LOG;
    static {
        // 打印Env相关信息
        LOG = LoggerFactory.getLogger(ZooKeeper.class);
        Environment.logEnv("Client environment:", LOG);
    }
}

1.1 构造方法解析

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

		// 如果在构造方法中传入watcher,则会将这个watcher作为默认watcher
        watchManager.defaultWatcher = watcher;

        ConnectStringParser connectStringParser = new ConnectStringParser(
                connectString);
        // 构造一个连接地址的解析器,最终会获取到一个具体的地址进行连接。在2中分析        
        HostProvider hostProvider = new StaticHostProvider(
                connectStringParser.getServerAddresses());
        // 创建ClientCnxn 对象,这个比较关键,在3中分析
        cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
                hostProvider, sessionTimeout, this, watchManager,
                getClientCnxnSocket(), canBeReadOnly);
        cnxn.start();
    }

2.地址解析器StaticHostProvider

在生产环境,我们以集群的方式搭建Zookeeper服务端,故我们创建客户端时会将所有的服务端地址都写入,比如如下这样

ZooKeeper zkCli = new ZooKeeper("192.168.0.1:2181,192.168.0.2:2181,192.168.0.3:2181/zktest", 3000, new Watcher() {
    // 回调监听
    @Override
    public void process(WatchedEvent event) {
        try {
            List<String> children = zkCli.getChildren("/", true);
            for (String c : children) {
                // do...
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

客户端在创建连接时会获取到某一个具体的服务端ip:port,创建对其长连接,这个选择的过程就是HostProvider来做的。我们来看下其具体选择过程

2.1 ConnectStringParser解析服务端ip信息

public final class ConnectStringParser {
    // 默认端口为2181
    private static final int DEFAULT_PORT = 2181;

    // 根目录,一般应用在使用时都会创建一个独特的根目录信息,比如dubbo的注册信息都以/dubbo为根目录
    private final String chrootPath;
    // 解析出来的具体server信息集合
    private final ArrayList<InetSocketAddress> serverAddresses = new ArrayList<InetSocketAddress>();
    
    public ConnectStringParser(String connectString) {
        // 我们以connectString="192.168.0.1:2181,192.168.0.2:2181,192.168.0.3:2181/zktest"为例
        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);
                // 这里会使用zktest作为根目录
                this.chrootPath = chrootPath;
            }
            // 192.168.0.1:2181,192.168.0.2:2181,192.168.0.3:2181被解析成connectString
            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);
            }
            // 将connectString解析成InetSocketAddress
            serverAddresses.add(InetSocketAddress.createUnresolved(host, port));
        }
    }

代码比较简单,主要就是解析chrootPath和服务端地址,封装成InetSocketAddress即可。

2.2 StaticHostProvider选择一个具体的服务端连接

public final class StaticHostProvider implements HostProvider {
 
    // 服务端地址集合
    private final List<InetSocketAddress> serverAddresses = new ArrayList<InetSocketAddress>(5);

    // 使用两个index来循环引用
    private int lastIndex = -1;
    private int currentIndex = -1;

    // 解析器
    private Resolver resolver;
    
    public StaticHostProvider(Collection<InetSocketAddress> serverAddresses) {
        this.resolver = new Resolver() {
            @Override
            public InetAddress[] getAllByName(String name) throws UnknownHostException {
                return InetAddress.getAllByName(name);
            }
        };
        // 初始化
        init(serverAddresses);
    }
	private void init(Collection<InetSocketAddress> serverAddresses) {
        if (serverAddresses.isEmpty()) {
            throw new IllegalArgumentException(
                    "A HostProvider may not be empty!");
        }

        // 初始化很简单,就是将在ConnectStringParser解析出来的服务端列表传入serverAddresses,并进行一次shuffle
        this.serverAddresses.addAll(serverAddresses);
        Collections.shuffle(this.serverAddresses);
    }
}

StaticHostProvider初始化的方法很简单,就是接收ConnectStringParser解析出来的服务端列表传入serverAddresses。重点方法在next()中,我们来分析下

2.3 StaticHostProvider.next() 获取特定的服务端地址

public InetSocketAddress next(long spinDelay) {
    // currentIndex递增下
    currentIndex = ++currentIndex % serverAddresses.size();
    // 如果达到了lastIndex,且需要sleep下,则先sleep下
    if (currentIndex == lastIndex && spinDelay > 0) {
        try {
            Thread.sleep(spinDelay);
        } catch (InterruptedException e) {
            LOG.warn("Unexpected exception", e);
        }
        // 初始值为-1,重置为0
    } else if (lastIndex == -1) {
        // We don't want to sleep on the first ever connect attempt.
        lastIndex = 0;
    }

    // 根据获取到的currentIndex,获取指定的InetSocketAddress,代码比较简单
    InetSocketAddress curAddr = serverAddresses.get(currentIndex);
    try {
        String curHostString = getHostString(curAddr);
        // 如果选定的服务端地址是一个域名提供的话,就获取下域名对应的ip列表
        List<InetAddress> resolvedAddresses = new ArrayList<InetAddress>(Arrays.asList(this.resolver.getAllByName(curHostString)));
        if (resolvedAddresses.isEmpty()) {
            return curAddr;
        }
        Collections.shuffle(resolvedAddresses);
        return new InetSocketAddress(resolvedAddresses.get(0), curAddr.getPort());
    } catch (UnknownHostException e) {
        return curAddr;
    }
}

代码很简单,笔者不再赘述。指定currentIndex和lastIndex两个指针,通过递增currentIndex,来获取serverAddresses具体index的InetAddress信息。

3. ClientCnxn的创建

我们直接来分析下其构造方法

public class ClientCnxn {
	public ClientCnxn(String chrootPath, HostProvider hostProvider, int sessionTimeout, ZooKeeper zooKeeper,
            ClientWatchManager watcher, ClientCnxnSocket clientCnxnSocket,
            long sessionId, byte[] sessionPasswd, boolean canBeReadOnly) {
        this.zooKeeper = zooKeeper;
        this.watcher = watcher;
        // 默认sessionId=0
        this.sessionId = sessionId;
        this.sessionPasswd = sessionPasswd;
        // 我们在构造Zookeeper时设置的session过期时间
        this.sessionTimeout = sessionTimeout;
        // 通过connectString解析出来的服务端地址和跟路径chrootpath
        this.hostProvider = hostProvider;
        this.chrootPath = chrootPath;

        // 这里的连接超时时间设置比较有意思,如果我们设置sessionTimeout为3000(也就是3秒钟),提供的服务端有三个ip,那么真正的connectTimeout=1s
        connectTimeout = sessionTimeout / hostProvider.size();
        readTimeout = sessionTimeout * 2 / 3;
        readOnly = canBeReadOnly;

        // 两个比较关键的Thread,后续继续分析
        sendThread = new SendThread(clientCnxnSocket);
        eventThread = new EventThread();

    }
}

3.1 ClientCnxn.start()

public class ClientCnxn {
	public void start() {
        sendThread.start();
        eventThread.start();
    }
}

ClientCnxn的start方法比较简单,就是启动两个线程。那么这两个线程具体是做什么的呢?

3.2 SendThread

/**
     * This class services the outgoing request queue and generates the heart
     * beats. It also spawns the ReadThread.
     */
// 从注释中可以看出,SendThread的主要作用就是发送请求到服务端;生成心跳
class SendThread extends ZooKeeperThread {
    private long lastPingSentNs;
    // 表示对服务端的一个长连接
    private final ClientCnxnSocket clientCnxnSocket;
    private Random r = new Random(System.nanoTime());        
    private boolean isFirstConnect = true;

    public void run() {
        ...
        while (state.isAlive()) {
            try {
                if (!clientCnxnSocket.isConnected()) {
                    ...
                    // 未连接的情况下,则调用hostProvider.next()方法选择一个ip:port进行连接    
                    if (rwServerAddress != null) {
                        serverAddress = rwServerAddress;
                        rwServerAddress = null;
                    } else {
                        serverAddress = hostProvider.next(1000);
                    }
                    // 在这里创建连接,创建连接过程并不复杂,就是NioSocketChannel的连接过程,读者可自行阅读
                    // 如果当前连接失败的话,在这个while循环中,会自动寻找下一个服务端ip:port进行连接
                    startConnect(serverAddress);
                    clientCnxnSocket.updateLastSendAndHeard();
                }

                ...

                if (state.isConnected()) {
                    //1000(1 second) is to prevent race condition missing to send the second ping
                    //also make sure not to send too many pings when readTimeout is small 
                    int timeToNextPing = readTimeout / 2 - clientCnxnSocket.getIdleSend() - 
                        ((clientCnxnSocket.getIdleSend() > 1000) ? 1000 : 0);
                    //send a ping request either time is due or no packet sent out within MAX_SEND_PING_INTERVAL
                    // 在适当的时候进行心跳发送
                    if (timeToNextPing <= 0 || clientCnxnSocket.getIdleSend() > MAX_SEND_PING_INTERVAL) {
                        sendPing();
                        clientCnxnSocket.updateLastSend();
                    } else {
                        if (timeToNextPing < to) {
                            to = timeToNextPing;
                        }
                    }
                }

                ...

                // 发送请求包到服务端    
                clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this);
            } catch (Throwable e) {
                ...
        }
        cleanup();
        clientCnxnSocket.close();
        if (state.isAlive()) {
            eventThread.queueEvent(new WatchedEvent(Event.EventType.None,
                                                    Event.KeeperState.Disconnected, null));
        }
        ZooTrace.logTraceMessage(LOG, ZooTrace.getTextTraceLevel(),
                                 "SendThread exited loop for session: 0x"
                                 + Long.toHexString(getSessionId()));
    }
}

SendThread的工作就是创建对服务端的连接,然后将请求包发送出去,至于具体如何发送请求包,我们后续在分析各种不同请求类型时在进行分析,目前只需要记住SendThread就是用来发送请求的即可。

还有就是:SendThread也可以读取请求响应结果,具体在readResponse()方法中

所以,总结下来,SendThread负责发送请求和接收响应结果。

3.3 EventThread

最后还有一个EventThread没有介绍,这个线程是做什么用的呢?主要是用来处理Event的,这个Event主要是Watcher Event,就是我们在Zookeeper上注册的事件监听器,如果被触发,则会接收到一个WatcherEvent事件,接收到之后,就是由EventThread来处理的。

关于其处理细节,本文不再详述,后面我们会专门写一篇文章来讲述事件监听处理。

总结:

本文主要介绍了Zookeeper客户端的创建过程,创建连接、发送请求、接收响应的工作主要交由SendThread来操作;而事件监听工作主要交由EventThread来处理。

后续我们来分析下各种请求类型下Zookeeper的处理过程。