客户端与服务端之间是怎么交互的

  • 我的源码链接
  • 简单描述
  • 源码入口
  • 客户端启动流程
  • ZookeeperMain.java
  • 重点看下 ZooKeeperMain main = new ZooKeeperMain(args)
  • 客户端总结
  • 我将整个逻辑精简后主要关注doTransport() 方法。
  • 服务端启动流程
  • QuorumPeerMain.main()启动流程
  • ZooKeeperServerMain.main(args);
  • cnxnFactory.configure()
  • 看看run方法
  • cnxnFactory.startup(zkServer) 回到第二个重要的方法
  • zks.startup();
  • 请求处理链


我的源码链接

github,我已经添加上了很多注释,拉下来就可以直接看。

简单描述

先总述一下,zk的客户端与服务端交互的大概思路。
客户端 一个线程 while循环队列(LinkedList)中取数据发送给服务端或者接受服务端的数据
服务端 同理。

源码入口

客户端:
我们知道在linux中如果想链接zk则要通过zkCli.sh脚本。(这个不知道的请先去熟悉一下zk)

#!/usr/bin/env bash
# use POSTIX interface, symlink is followed automatically
ZOOBIN="${BASH_SOURCE-$0}"
ZOOBIN="$(dirname "${ZOOBIN}")"
ZOOBINDIR="$(cd "${ZOOBIN}"; pwd)"

if [ -e "$ZOOBIN/../libexec/zkEnv.sh" ]; then
  . "$ZOOBINDIR"/../libexec/zkEnv.sh
else
  . "$ZOOBINDIR"/zkEnv.sh
fi

"$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
     -cp "$CLASSPATH" $CLIENT_JVMFLAGS $JVMFLAGS \
     org.apache.zookeeper.ZooKeeperMain "$@"

通过上面的脚本可以知道最终zkCli.sh 就是执行了org.apache.zookeeper.ZooKeeperMain的main方法。

服务端:
同理服务端也是通过zkServer.sh 脚本来启动(由于该脚本有点长。找到核心代码)

ZOOMAIN="org.apache.zookeeper.server.quorum.QuorumPeerMain"
=====================
start)
    echo  -n "Starting zookeeper ... "
    if [ -f "$ZOOPIDFILE" ]; then
      if kill -0 `cat "$ZOOPIDFILE"` > /dev/null 2>&1; then
         echo $command already running as process `cat "$ZOOPIDFILE"`. 
         exit 0
      fi
    fi
    nohup "$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
    -cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG" > "$_ZOO_DAEMON_OUT" 2>&1 < /dev/null &
    if [ $? -eq 0 ]
    then
      case "$OSTYPE" in
      *solaris*)
        /bin/echo "${!}\\c" > "$ZOOPIDFILE"
        ;;
      *)
        /bin/echo -n $! > "$ZOOPIDFILE"
        ;;
      esac
      if [ $? -eq 0 ];
      then
        sleep 1
        echo STARTED
      else
        echo FAILED TO WRITE PID
        exit 1
      fi
    else
      echo SERVER DID NOT START
      exit 1
    fi
    ;;

可以看到最终启动的时候也就是执行了QuorumPeerMain的main方法

客户端启动流程

ZookeeperMain.java

public static void main(String args[])
        throws KeeperException, IOException, InterruptedException
    {
        // 初始化
        // 去建立与服务端的sokcet
        // 开启后台线程,循环从sokcet读、写数据
        ZooKeeperMain main = new ZooKeeperMain(args);

        //监听  后续命令行 命令并处理
        //就是你执行完zkCli.sh后控制台与你交互的命令行,就是在这里执行的。
        main.run();
    }

重点看下 ZooKeeperMain main = new ZooKeeperMain(args)

public ZooKeeperMain(String args[]) throws IOException, InterruptedException {
        // 命令行 参数解析
        cl.parseOptions(args);
        System.out.println("Connecting to " + cl.getOption("server"));
        // 连接服务器,并且初始化zk
        connectToZK(cl.getOption("server"));
        //zk = new ZooKeeper(cl.getOption("server"),
//                Integer.parseInt(cl.getOption("timeout")), new MyWatcher());
    }
 protected void connectToZK(String newHost) throws InterruptedException, IOException {
        if (zk != null && zk.getState().isAlive()) {
            zk.close();
        }
        host = newHost;
        boolean readOnly = cl.getOption("readonly") != null;
        // 初始化 ZooKeeper 类
        /**
         * 1、getClientCnxnSocket() 准备好一个 Socket连接
         * 2、sendThread = new SendThread(clientCnxnSocket);   发送线程(包括连接、发送数据)
         * 3、eventThread = new EventThread();  事件线程
         * 调用 start方法
         */
        zk = new ZooKeeper(host,
                 Integer.parseInt(cl.getOption("timeout")),
                 new MyWatcher(), readOnly);
    }
    
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();
    }

通过上面代码我也加了注释,简单的我加了注释就不展开了。重点关注最后两个方法调用

// 第一个方法
    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;
        this.sessionId = sessionId;
        this.sessionPasswd = sessionPasswd;
        this.sessionTimeout = sessionTimeout;
        this.hostProvider = hostProvider;
        this.chrootPath = chrootPath;

		// 设置超时时间等等
        connectTimeout = sessionTimeout / hostProvider.size();
        readTimeout = sessionTimeout * 2 / 3;
        readOnly = canBeReadOnly;
		
		// 最核心的两个线程
        sendThread = new SendThread(clientCnxnSocket);
        eventThread = new EventThread();
    }
//    第二个方法
    public void start() {
        sendThread.start();
        eventThread.start();
    }

客户端总结

此时应该恍然大悟: 整个zk客户端启动 最重要的就是启动了两个线程SendThread、EventThread。见名知意这两个线程,一个用来做数据交互的、一个是用来触发zk事件的。本次主要来看数据交互的流程。所以只要跟着看SendThread的run()方法即可。

@Override
public void run() {

     while (state.isAlive()) {
         try {
        // 此处省略了很多逻辑包括:
        // 1、连接断开、或者第一次连接的sokcet建立
        // 2、连接中密码校验
        // 3、连接成功的 发送ping保持心跳
        // 4、链接失败、检测是否超时等。抛出异常后继续尝试链接
        //================================================
        // 5、进行传输,传输的是outgoingQueue队列中的Packet    Transport :传输
        clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this);
         } catch (Throwable e) {
         /。。。。省略
         }

我将整个逻辑精简后主要关注doTransport() 方法。

/**
     * 1、outgoingQueue 是请求发送队列,是client存储需要被发送到server端的Packet队列
     * 2、pendingQueue是已经从client发送,但是要等待server响应的packet队列。
     *
     * doIO() 方法会 从outgoingQueue 取数据发送
     *       从 pendingQueue取数据 将返回结果写入
     */
    @Override
    void doTransport(int waitTimeOut, List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue,
                     ClientCnxn cnxn)
            throws IOException, InterruptedException {
        selector.select(waitTimeOut);
        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
        updateNow();
        for (SelectionKey k : selected) {
            SocketChannel sc = ((SocketChannel) k.channel());
            // 1、如果是连接就绪,调用sendThread连接操作
            if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {
                // 如果就绪的是connect事件,这个出现在registerAndConnect函数没有立即连接成功
                if (sc.finishConnect()) {
                    updateLastSendAndHeard();
                    // 初始化链接-----
                    sendThread.primeConnection();
                }
            } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
            // 2、如果就绪的是读或者写事件
            // 若读写就绪,调用doIO函数
                doIO(pendingQueue, outgoingQueue, cnxn);
            }
        }
        if (sendThread.getZkState().isConnected()) {
            synchronized(outgoingQueue) {
                if (findSendablePacket(outgoingQueue,
                        cnxn.sendThread.clientTunneledAuthenticationInProgress()) != null) {
                    // 如果有可以发送的packet
                    enableWrite();
                }
            }
        }
        selected.clear();
    }

//=====================================
主要关注这个方法,主要是从outgoingQueue取出数据发送出去。并放在pendingQueue中等待服务端响应。
doIO(pendingQueue, outgoingQueue, cnxn);
该方法里面代码随多,但是逻辑非常简单。就不展开了。

服务端启动流程

QuorumPeerMain.main()启动流程

public static void main(String[] args) {
        QuorumPeerMain main = new QuorumPeerMain();
        try {
            main.initializeAndRun(args);
            } catch (IllegalArgumentException e) {。。。。}
      }

protected void initializeAndRun(String[] args)
    throws ConfigException, IOException
{
    QuorumPeerConfig config = new QuorumPeerConfig();
    if (args.length == 1) {
        config.parse(args[0]);
    }

    // Start and schedule the the purge task
    // 开启一个 清除数据的线程(不重要)
    DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
            .getDataDir(), config.getDataLogDir(), config
            .getSnapRetainCount(), config.getPurgeInterval());
    purgeMgr.start();

    if (args.length == 1 && config.servers.size() > 0) {
        // 集群模式
        runFromConfig(config);
    } else {
        LOG.warn("Either no config or no quorum defined in config, running "
                + " in standalone mode");
        // there is only server in the quorum -- run as standalone
        // 单机模式
        ZooKeeperServerMain.main(args);
    }
}

注意:zk服务端启动时,包含两种模式 :单机模式集群模式 此两种模式差距巨大。一个不需要选举,一个需要选举。
本次我们简单点看看单机模式:

ZooKeeperServerMain.main(args);

public static void main(String[] args) {
        ZooKeeperServerMain main = new ZooKeeperServerMain();
        try {
            // 初始化并且运行
            main.initializeAndRun(args);
        } catch (IllegalArgumentException e) {
	。。。。。
	}
    protected void initializeAndRun(String[] args)
        throws ConfigException, IOException
    {
        try {
            ManagedUtil.registerLog4jMBeans();
        } catch (JMException e) {
            LOG.warn("Unable to register log4j JMX control", e);
        }

        ServerConfig config = new ServerConfig();
        if (args.length == 1) {
            config.parse(args[0]);
        } else {
            //主要 设置 配置文件路径
            config.parse(args);
        }

        // 解析配置 并 运行
        runFromConfig(config);
    }


    /**
     * 1、创建了 FileTxnSnapLog 事务、快照 日志 工具类
     * 2、构建ZooKeeperServer 类,此类相当于ZooKeeper的服务端
     * 3、通过ServerCnxnFactory.createFactory()创建ServerCnxnFactory ,它的作用非常大。
     * 4、cnxnFactory.configure()
     *      4.1 构建服务端SocketServer,并监听端口。
     *      4.2 cnxnFactory其本身就是一个Runable接口的实现类。那么肯定有地方启动他的run方法
     * 5、cnxnFactory.startup()
     *      5.1 开启cnxnFactory 线程的run方法
     *      5.2 初始化数据
     *      5.3 调用zks.startup();比较重要 可以继续查看
     *
     *
     * 后面 关注两个重点;
     * 1、cnxnFactory.run()里面做了什么事?
     * 2、zks.startup() 里面做了什么事?
     *
     *
     * Run from a ServerConfig.
     * @param config ServerConfig to use.
     * @throws IOException
     */
    public void runFromConfig(ServerConfig config) throws IOException {
        LOG.info("Starting server");
        // 一个工具类,用于 记录事务日志和数据快照
        FileTxnSnapLog txnLog = null;
        try {
            final ZooKeeperServer zkServer = new ZooKeeperServer();
            //。。。。。。。省略部分代码
            // 获取建立socket工厂,工厂方法模式 默认 org.apache.zookeeper.server.NIOServerCnxnFactory(是一个线程)
            cnxnFactory = ServerCnxnFactory.createFactory();
            // 建立socketService 并监听
            cnxnFactory.configure(config.getClientPortAddress(),
                    config.getMaxClientCnxns());
            // 重要 启动
            cnxnFactory.startup(zkServer);
            // Watch status of ZooKeeper server. It will do a graceful shutdown
            // if the server is not running or hits an internal error.
            shutdownLatch.await();
            shutdown();

            cnxnFactory.join();
            if (zkServer.canShutdown()) {
                zkServer.shutdown(true);
            }
        } catch (InterruptedException e) {
            // warn, but generally this is ok
            LOG.warn("Server interrupted", e);
        } finally {
            if (txnLog != null) {
                txnLog.close();
            }
        }
    }

根据流程撸下来。服务端启动最终 最重要的两个方法 cnxnFactory.configure()和cnxnFactory.startup() 挨个分析吧。

cnxnFactory.configure()

@Override
    public void configure(InetSocketAddress addr, int maxcc) throws IOException {
        configureSaslLogin();

        // 把当前类作为线程
        thread = new ZooKeeperThread(this, "NIOServerCxn.Factory:" + addr);
        // java中线程分为两种类型:用户线程和守护线程。
        // 通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。
        // 如果不设置次属性,默认为用户线程。
        // 守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。
        // 那Java的守护线程是什么样子的呢。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出
        // 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。
        // 它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

        // 所以这里的这个线程是为了和JVM生命周期绑定,只剩下这个线程时已经没有意义了,应该关闭掉。
        thread.setDaemon(true);
        maxClientCnxns = maxcc;
        this.ss = ServerSocketChannel.open();
        ss.socket().setReuseAddress(true);
        LOG.info("binding to port " + addr);
        ss.socket().bind(addr);
        ss.configureBlocking(false);
        ss.register(selector, SelectionKey.OP_ACCEPT);
    }

该方法主要作用就是将cnxnFactory封装成一个thread。并设置serverSocket绑定端口。该线程的作用就是与客户端交互。建立链接和传输数据。与客户端那边代码相似最终都是doIO()方法。由此可见,服务端建立链接和客户端交互的逻辑肯定在改类的run()方法里面。

看看run方法
/**
     * 启动该线程后
     * 1、只要服务端的serverSocket 没有关闭,就会一直循环
     * 2、建立与客户端的连接
     * 3、与客户端进行IO操作
     */
    public void run() {
        // 如果socket没有关闭掉
        // selector是跟nio有关系的,我们看核心代码
        while (!ss.socket().isClosed()) {
            try {
                selector.select(1000);
                Set<SelectionKey> selected;
                synchronized (this) {
                    selected = selector.selectedKeys();
                }
                ArrayList<SelectionKey> selectedList = new ArrayList<SelectionKey>(
                        selected);
                Collections.shuffle(selectedList);

                /**
                 * nio知识
                 */
                for (SelectionKey k : selectedList) {
                    if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) { // 连接就绪
                        // 建立连接
                        SocketChannel sc = ((ServerSocketChannel) k
                                .channel()).accept();
                        InetAddress ia = sc.socket().getInetAddress();
                        int cnxncount = getClientCnxnCount(ia);
                        if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns){
                            LOG.warn("Too many connections from " + ia
                                     + " - max is " + maxClientCnxns );
                            sc.close();
                        } else {
                            LOG.info("Accepted socket connection from "
                                     + sc.socket().getRemoteSocketAddress());
                            sc.configureBlocking(false);
                            SelectionKey sk = sc.register(selector,
                                    SelectionKey.OP_READ);
                            NIOServerCnxn cnxn = createConnection(sc, sk);
                            // 将NIOServerCnxn 绑定给当前 SocketChannel
                            sk.attach(cnxn);
                            addCnxn(cnxn);
                        }
                    } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
                        // 接收数据,这里会间歇性的接收到客户端ping
                        NIOServerCnxn c = (NIOServerCnxn) k.attachment(); //从当前 ServerSocketChannel中取出绑定的属性
                        c.doIO(k);
                    } else {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Unexpected ops in select "
                                      + k.readyOps());
                        }
                    }
                }
                selected.clear();
            } catch (RuntimeException e) {
                LOG.warn("Ignoring unexpected runtime exception", e);
            } catch (Exception e) {
                LOG.warn("Ignoring exception", e);
            }
        }
        closeAll();
        LOG.info("NIOServerCnxn factory exited run method");
    }

cnxnFactory.startup(zkServer) 回到第二个重要的方法

@Override
    public void startup(ZooKeeperServer zks) throws IOException,
            InterruptedException {
        start();
        setZooKeeperServer(zks);
        // 从快照 和事务日志中加载 数据
        zks.startdata();
        // 设置 请求处理链  非常重要***********************
        zks.startup();
    }

总结一下该方法。启动了线程,就是上面说的run()方法。去和客户端建立连接。从日志中加载数据。最重要的是设置请求处理链。进去看看

zks.startup();
public synchronized void startup() {
        if (sessionTracker == null) {
            createSessionTracker();
        }
        startSessionTracker();
        // 这里比较重要,这里设置请求处理器,包括请求前置处理器,和请求后置处理器
        // 注意,集群模式下,learner服务端都对调用这个方法,但是比如FollowerZookeeperServer和ObserverZooKeeperServer都会重写这个方法

        //  leader: 设置处理链、开启三个线程
        //  PrepRequestProcessor.next = ProposalRequestProcessor.next=CommitProcessor.next=ToBeAppliedRequestProcessor.next=FinalRequestProcessor
        //  PrepRequestProcessor.run
        //  CommitProcessor.run
        //  SynProcessorRequset.run


        //  follower: 设置处理链、开启三个线程
        //  FollowerRequestProcessor.next = CommitProcessor.next = FinalRequestProcessor
        //  SyncRequestProcessor.next = SendAckRequestProcessor
        //  FollowerRequestProcessor.run
        //  CommitProcessor.run
        //  SyncRequestProcessor.run

        //  observer:设置处理链、根据条件开启线程
        //  ObserverRequestProcessor.next = CommitProcessor.next= FinalRequestProcessor
        //  ObserverRequestProcessor.run
        //  CommitProcessor.run
        //   if (syncRequestProcessorEnabled) {
        //         SyncRequestProcessor.run
        //        }
        setupRequestProcessors();

        registerJMX();

        setState(State.RUNNING);
        notifyAll();
    }
    // 执行完 PrepRequestProcessor->SyncRequestProcessor->FinalRequestProcessor
    protected void setupRequestProcessors() {
        RequestProcessor finalProcessor = new FinalRequestProcessor(this);
        RequestProcessor syncProcessor = new SyncRequestProcessor(this,
                finalProcessor);
        ((SyncRequestProcessor)syncProcessor).start();
        firstProcessor = new PrepRequestProcessor(this, syncProcessor);
        ((PrepRequestProcessor)firstProcessor).start();
    }

请求处理链

上面可以看调用了start()方法那么肯定也是一个线程了。

1、当服务端接收到请求后,会调用doIO()方法,最后会放到PrepRequestProcessor的队列
2、PrepRequestProcessor:本身是一个线程,该线程循环从本类中的队列里取数据,封装成事务对象,处理完放到SyncRequestProcessor的队列里。
3、SyncRequestProcessor:本身也是一个线程,该线程从本类中的队列里取数据,将事务对象刷盘到磁盘,处理完后调用FinalRequestProcessor来处理。
4、FinalRequestProcessor它不是线程,它主要作用是,修改内存中的数据。并返回数据给客户端。触发事件等等。

具体代码比较多就不贴了。可以查看我的github上的我已经添加好注释的源码。
我的github链接:https://github.com/WangTingYeYe/