Zookeeper 源码解读系列, 单机模式(三)

本文深入解析Zookeeper单机模式下服务器的启动过程,包括配置文件解析、ZookeeperServer实例化、NIO服务端启动、加载数据、Session追踪器启动等关键步骤。

前情提要

上一篇Zookeeper 源码解读系列, 单机模式(二)我们基本上已经把客户端主要的逻辑代码讲解完了,虽然溜了一些散碎得知识点,这些点以后笔者会作为小点做一些单独篇章的更新。所以这一系列的文章还是以Zookeeper的主逻辑主题继续更新,上一篇中我们详细介绍了一个很重要的线程SendThread、两个很重要的queue:outgoingQueuependingQueue、客户端时怎么处理输入的命令的、客户端时如何发送数据给服务器的、客户端的注册连接以及重试机制,NIO思想是怎么在客户端实现的等等。纵观Zookeeper的代码,NIO思想是贯穿Zookeeper整个设计理念的,可以说Zookeeper是一个非常好的NIO的实践例子。关于NIO笔者以后会写一篇帖子重点介绍,这里如果还有不懂的同学,还请移步百度做一下深度理解,这里我们不做赘述。本篇也会被收录到【Zookeeper 源码解读系列目录】中。

单机模式:服务器的启动

既然要说到服务器是接收数据的,那就绕不开服务器是怎么连接以及初始化的。和客户端一样,我们也得找到服务器端的入口才行,那么我们去bin目录找到zkServer.cmd并且打开,这里的代码也不多重点要找的还是入口在哪里:

setlocal
call "%~dp0zkEnv.cmd"
set ZOOMAIN=org.apache.zookeeper.server.quorum.QuorumPeerMain
echo on
call %JAVA% "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%" -cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %*
endlocal

和客户端一样我们要找的入口就是ZOOMAIN这个变量所写的类或者.java文件,这个文件的路径如下..\zookeeper-server\src\main\java\org\apache\zookeeper\server\quorum,如果你的Zookeeper源码已经安装好了,双击shift搜索就可以。那我们还是找到这个类的main()方法:

public static void main(String[] args) {
    QuorumPeerMain main = new QuorumPeerMain();
    try {
        main.initializeAndRun(args);
    } catch (***Exception e) {
        /**多个异常捕获,略**/
    }
    LOG.info("Exiting normally");
    System.exit(0);
}

这里的代码非常简单,有一点要说的就是,main()方法的参数args,这个参数接收到的就是配置文件zoo.cfg的路径,所以我们直接看main.initializeAndRun(args);

protected void initializeAndRun(String[] args)
    throws ConfigException, IOException
    QuorumPeerConfig config = new QuorumPeerConfig(); //服务端配置类
    if (args.length == 1) {
        config.parse(args[0]);//解析配置文件
    }
    /**略**/
}

配置参数的解析

既然我们把配置文件路径传入进来了,那么就一定有一个类要处理配置文件,就是QuorumPeerConfig这个类中的属性也都是和我们配置文件中的参数是对应的,有条件的同学可以点进去看一下,既然配置内容传递进来了,那后面肯定就要到解析config.parse(args[0]);

public void parse(String path) throws ConfigException {
    File configFile = new File(path);//拿出文件对象
    /**log略**/
    try {
        /**验证如果configFile不存在,抛出IllegalArgumentException,略**/
        Properties cfg = new Properties(); //生成Properties对象
        FileInputStream in = new FileInputStream(configFile); //变成文件流对象
        try {
            cfg.load(in);//装载成文件流
        } finally {
            in.close();
        }
        parseProperties(cfg);   //解析内容到Properties类中
    } catch (***Exception e) {
        /**多个异常捕获,略**/
    }
}

首先就是根据路径把文件对象File configFile = new File(path);拿出来,然后立刻生成Properties对象,并且转化文件对象为文件流对象,并且把流对象装载cfg.load(in)Properties对象中,最后在parseProperties(cfg)解析出来,这个方法里面,逻辑不复杂但是代码很长,我们拆分开来分析:

public void parseProperties(Properties zkProp) throws IOException, ConfigException {
    int clientPort = 0;
    String clientPortAddress = null;
    //解析参数
    for (Entry<Object, Object> entry : zkProp.entrySet()) {
        String key = entry.getKey().toString().trim();
        String value = entry.getValue().toString().trim();
        if (key.equals("dataDir")) {
            dataDir = value;
        } else if (key.equals("dataLogDir")) {
            dataLogDir = value;
        } else if (key.equals("clientPort")) {//客户端使用的端口
            clientPort = Integer.parseInt(value);
        } else if (key.equals("clientPortAddress")) {
            clientPortAddress = value.trim();
        }else if (key.equals("****")){
        /**赋值+解析参数tickTime,maxClientCnxns,minSessionTimeout,maxSessionTimeout,
        	initLimit,syncLimit,electionAlg,quorumListenOnAllIPs,peerType等等**/
        } else if (key.startsWith("server.")) {//这里会识别到server.开头的字符串
            int dot = key.indexOf('.');
            long sid = Long.parseLong(key.substring(dot + 1));
            String parts[] = splitWithLeadingHostname(value);//把内容分割开
            if ((parts.length != 2) && (parts.length != 3) && (parts.length !=4)) {
                /**LOG**/
            }
            LearnerType type = null;
            String hostname = parts[0];//拿到hostname
            Integer port = Integer.parseInt(parts[1]);//拿到传输port
            Integer electionPort = null;
            if (parts.length > 2){
            	electionPort=Integer.parseInt(parts[2]);//拿到选举port
            }
            if (parts.length > 3){
                if (parts[3].toLowerCase().equals("observer")) {
                //识别observer字段
                    type = LearnerType.OBSERVER;
                } else if (parts[3].toLowerCase().equals("participant")) {
                //识别participant字段
                    type = LearnerType.PARTICIPANT;
                } else {
                    throw new ConfigException("Unrecognised peertype: " + value);
                }
            }
            if (type == LearnerType.OBSERVER){
                //是观察者,加入到observers里面
                observers.put(Long.valueOf(sid), new QuorumServer(sid, hostname, port, electionPort, type));
            } else {
                //不是观察者,加入到servers里面
                servers.put(Long.valueOf(sid), new QuorumServer(sid, hostname, port, electionPort, type));
            }
        } else if (key.startsWith("****")) {
            /**略,group,weight,SASL_AUTH**/
        } else {
            System.setProperty("zookeeper." + key, value);
        }
    }
    /**集群,SASL相关,暂时略**/
}

首先我们先看第一部分,这里是一个for循环,这里是做什么的呢?这里其实就是在解析我们配置文件中的参数,并且根据我们的参数进行初步的处理和赋值。比如这里就用if判断并且拿数据dataDirdataLogDir这些我们配置的路径,还有客户端使用的端口clientPort等等,因为太多了所以略过大部分,我们找一些重点的来讲。这里的重点就在else if (key.startsWith("server."))这句话,这里的条件是不是很熟悉,还记得咋配置文件中我们怎么写的吗(server.*=hostname:2887:3887)?这里就要开始截取并且初始化我们配置的servers了,当然这里是指集群模式下的配置,我们在这里先进行一个讲解,到集群模式下就不再叙述这部分内容了,因为集群模式更加的难懂,我希望当介绍到集群模式的时候尽量减少干扰。当识别到server.开头的字符串以后,就分割成了一个字符串数组parts[] = splitWithLeadingHostname(value);,然后parts[0]就是配置的hostnameparts[1]就是我们用来传输的port也就是上面写的2887正常传输数据用的就是这位置配置的port,当然你可以随意配置,parts[2]就是用来选举port也就是上面写的3887选举传输数据用的就是这个端口,这里也可以在文件中自定义。parts[3]就是有没有配置observer字段,如果没有配置就是participant也就是这台机器是我们的follower之一。再往下如果是观察者就加到observers里面,如果不是就加入到servers里面,那么我们再往后看for循环后面是什么:

public void parseProperties(Properties zkProp)
throws IOException, ConfigException {
    int clientPort = 0;
    String clientPortAddress = null;
    for (Entry<Object, Object> entry : zkProp.entrySet()) {
        /**for略**/
    }
    /**集群和sasl相关,暂时略**/
    if (dataDir == null) {
        throw new IllegalArgumentException("dataDir is not set");
    }
    if (dataLogDir == null) {
        dataLogDir = dataDir;
    }
    /**同样处理参数clientPort clientPortAddress tickTime minSessionTimeout maxSessionTimeout,略过**/
    if (servers.size() == 0) {//如果没有可用的服务器直接返回
        if (observers.size() > 0) {//如果只有观察者服务器,抛出异常
            throw new IllegalArgumentException("Observers w/o participants is an invalid configuration");
        }
        return;
    } else if (servers.size() == 1) {//如果只有一台可用的服务器说明不是集群,清空servers队列
        if (observers.size() > 0) {//如果这台还是观察者服务器,抛出异常
            throw new IllegalArgumentException("Observers w/o quorum is an invalid configuration");
        }
        LOG.error("Invalid configuration, only one server specified (ignoring)");
        servers.clear();
    } else if (servers.size() > 1) {//如果大于1太可用服务器逻辑
        if (servers.size() == 2) {//如果等于2,报警最小3台才能用嘛
            LOG.warn("No server failure will be tolerated. " +
                "You need at least 3 servers.");
        } else if (servers.size() % 2 == 0) {//取模是偶数逻辑,报警因为过半机制偶数肯定不安全
            LOG.warn("Non-optimial configuration, consider an odd number of servers.");
        }
		/**暂时无关,略过**/
        if(serverGroup.size() > 0){
			/**serverGroup暂时无关,略过**/
        } else {//如果只有一个集群,就会走到这里
            LOG.info("Defaulting to majority quorums");
            quorumVerifier = new QuorumMaj(servers.size()); //集群验证器
        }
        //最终把observers加到servers里面
        servers.putAll(observers);
        File myIdFile = new File(dataDir, "myid");
        if (!myIdFile.exists()) {
            throw new IllegalArgumentException(myIdFile.toString() + " file is missing");
        }
        BufferedReader br = new BufferedReader(new FileReader(myIdFile));
        String myIdString;
        try {
            myIdString = br.readLine();
        } finally {
            br.close();
        }
        try {//serverId取的就是创建的myid
            serverId = Long.parseLong(myIdString);
            MDC.put("myid", myIdString);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("serverid " + myIdString  + " is not a number");
        }
        //再次验证observer
        LearnerType roleByServersList = observers.containsKey(serverId) ? LearnerType.OBSERVER
                : LearnerType.PARTICIPANT;
        //这里我们可以看到会更新peerType
        if (roleByServersList != peerType) {
            LOG.warn("Peer type from servers list (" + roleByServersList
                    + ") doesn't match peerType (" + peerType
                    + "). Defaulting to servers list.");

            peerType = roleByServersList;
        }
    }
}

过了for循环就开始正式对内容赋值了,如果发现没有设置dataDir报错,如果dataLogDir没有配置呢,就把dataDir的路径给它,诸如此类。接着往下就开始判断是不是有多少个服务器是可用的,如果说servers.size() == 0没有可用的服务器直接返回,如果只有一台可用的服务器说明不是集群,清空servers队列。接着往下serverGroup.size()这个概念先不说,如果我们是只有一个集群,一般就会走到else里面。如果单机模式那么我们就会进入servers.size() == 1这个if逻辑里面,就不会到这个serverGroup的判断面面去。我们再多说一句quorumVerifier = new QuorumMaj(servers.size());这个方法其实就是判断集群是不是过半用的,点进去里面只有this.half = n/2;,Zookeeper传说中的过半机制就是用的QuorumMaj这个类里面的方法,当然我们以后还会在这个系列专门讲解集群选举,这里只是点一下。再往后就把观察者机器的map也存到servers中,servers.putAll(observers);由于前面在验证的时候只会处理servers,而在这句话执行之前observers并没有包含在servers这个map中,可以看到过半的时候就没有observers参与进去, 所以observers才不会被算在过半的选举中,这也是验证了observer和过半机制是没有关系的。

再继续下去就是serverId,这个id也是比较重要的一个id,它取的就是创建的myid,后面也会简称为sid。后面又一个小点LearnerType roleByServersList = observers.containsKey(serverId) ? LearnerType.OBSERVER : LearnerType.PARTICIPANT;这里又一次验证了一下哪台机器才是观察者observer。比如我们配置了这样一句server.1=localhost:2887:3887:observer,那么到这里就会发现myid=1对应的后面有关键字observer那么就把这个myid所对应的机器重新标识为OBSERVER,并且会更新peerType=OBSERVER。这里就说明哪一台机器是观察者(observer),是由在配置文件中配置server.*=localhost:2887:3887:observer最后的:observer决定的,而不是你在配置文件中指定peerType=observer所决定的,即便在文件里对某个服务器配置了peerType=participant,后面hostname只要配置了observer,还是会被刷新掉的。到这里解析配置就已经结束了,那么我们跳出去,回到initializeAndRun(String[] args)方法中,接着走主流程:

protected void initializeAndRun(String[] args)
    throws ConfigException, IOException
{
   /**解析配置,已经讲过**/
   /**DatadirCleanupManager无关流程,略过**/
    //判断是集群还是单机
    if (args.length == 1 && config.servers.size() > 0) {
        runFromConfig(config);
    } else {
        //如果不是,there is only server in the quorum -- run as standalone
        LOG.warn("Either no config or no quorum defined in config, running " + " in standalone mode");
        ZooKeeperServerMain.main(args);
    }
}

经过读取配置文件后,就要去启动服务器了,走到下面的if这里,如果发现config.servers.size() > 0,说明是我们配置了server*这些集群服务器,那就运行runFromConfig(config);这个类。反之如果没有配置,那就是单机模式,则需要使用这个类ZooKeeperServerMain.main(args);,其实ZooKeeperServerMain这个类就是我们单机模式的启动类,所以我们先要看这个类是怎么启动的。

ZooKeeperServerMain的启动

一样的模式,我们先找到main()方法:

    public static void main(String[] args) {
        ZooKeeperServerMain main = new ZooKeeperServerMain();
        try {
            main.initializeAndRun(args); //传配置文件的路径进去
        } catch (IllegalArgumentException e) {
            /**多个异常捕获,略**/
        }
        LOG.info("Exiting normally");
        System.exit(0);
    }

接着到initializeAndRun(args)里面去:

protected void initializeAndRun(String[] args) throws ConfigException, IOException
{
    /**加载Log4j,略**/
    ServerConfig config = new ServerConfig();    //初始化配置
    if (args.length == 1) {
        config.parse(args[0]);    //解析配置存到配置类
    } else {
        config.parse(args);
    }
    runFromConfig(config);    //从配置中运行
}

简单的看一眼,其实套路都是一样的,无非都是先初始化配置,然后解析,这些没什么好说的,因为这里就相当于使用一台机器,所以里面的代码也非常的简单,parse(args)方法几乎也没什么代码,好奇的同学可以点击去看下,我们直接跳过,重点就在于runFromConfig(config)运行:

public void runFromConfig(ServerConfig config) throws IOException {
    LOG.info("Starting server");
    FileTxnSnapLog txnLog = null;
    try {
        final ZooKeeperServer zkServer = new ZooKeeperServer();//zk的主要Server类
        final CountDownLatch shutdownLatch = new CountDownLatch(1);
        zkServer.registerServerShutdownHandler(
                new ZooKeeperServerShutdownHandler(shutdownLatch));
        //FileTxnSnapLog工具类
        txnLog = new FileTxnSnapLog(new File(config.dataLogDir), new File(
                config.dataDir));
        //初始化server属性
        txnLog.setServerStats(zkServer.serverStats());
        zkServer.setTxnLogFactory(txnLog);
        zkServer.setTickTime(config.tickTime);
        zkServer.setMinSessionTimeout(config.minSessionTimeout);
        zkServer.setMaxSessionTimeout(config.maxSessionTimeout);
        //创建一个Socket工厂,工厂模式ServerCnxnFactory
        cnxnFactory = ServerCnxnFactory.createFactory();
        //建立socket,默认是NIOServerCnxnFactory
        cnxnFactory.configure(config.getClientPortAddress(),config.getMaxClientCnxns());
        //开始运行
        cnxnFactory.startup(zkServer);
        shutdownLatch.await();
        shutdown();
        cnxnFactory.join();
        if (zkServer.canShutdown()) {
            zkServer.shutdown(true);
        }
    } catch (InterruptedException e) {
        LOG.warn("Server interrupted", e);
    } finally {
        if (txnLog != null) {
            txnLog.close();
        }
    }
}

进入以后首先就看到new了一个ZooKeeperServer的实例,这个类就是Zookeeper主要的Server类。我们先讲解一下流程,我们单机模式启动ZooKeeperServer以后,和客户端一样,对应的服务端也会有一个NIO的server端的启动去接收客户端发送的请求。后面会运行一个NIO的线程,用来处理后续的逻辑。在介绍这些逻辑之前,必须先解释清楚另一个点:服务端启动的步骤。

服务端启动的步骤

我们知道Zookeeper的服务端是有事务日志和快照的概念的。比如说我们运行一个create命令,Zookeeper肯定会有一个持久化的动作。同时内存中也有数据,这个数据叫DataBase,其中包含了DataTreeDataTree又包含了DataNode,这个DataNode就是我们最终创建的节点。这三个类都是我们在内存中用的,那事务日志是什么时候用的呢?比如说我们发送了一个create命令到服务端,就会有一条日志给记录到服务器上,这点和数据库是类似的。所以当Zookeeper服务端接收到任何一个命令的时候(此处的前提是连接等等一切正常)大致会做4件事情:

  1. 创建事务日志,保存到我们指定的目录下面;
  2. 在某一个点打快照,这个快照就是创建DataBase.DataTree.DataNode这种层级结构到文件; 服务器累计多个事务才会打一个快照;
  3. 更新内存,操作更新内存中的DataTree
  4. 以上都执行成功了,返回错误或者正确的信息出去。

这是服务端接收请求的4步,但是当我们启动服务器的时候这些数据是要被加入到内存中去的,不可能我们每次get命令都从文件里读取内容,所以服务器启动的时候也有一个逻辑,就是从快照文件里面直接取出数据加载到内存里(DataBase),这样我们使用get命令就是从内存中读取了。既然事务日志、打快照都是和文件、Log相关的,所以Zookeeper也给我们提供了一个工具类FileTxnSnapLog,并且把Log的目录和数据的目录传递进去了txnLog = new FileTxnSnapLog(new File(config.dataLogDir), new File(config.dataDir));,然后初始化Zookeeper的server属性,再往下就是创建一个Socket工厂cnxnFactory = ServerCnxnFactory.createFactory();在这里使用createFactory()方法构造出来了一个NIOServerCnxnFactory的对象用来实现ServerCnxnFactory

static public ServerCnxnFactory createFactory() throws IOException {
	//取默认属性"zookeeper.serverCnxnFactory"
    String serverCnxnFactoryName = System.getProperty(ZOOKEEPER_SERVER_CNXN_FACTORY);
    if (serverCnxnFactoryName == null) {//如果没有配置,那么也是默认取NIOServer这个类
        serverCnxnFactoryName = NIOServerCnxnFactory.class.getName();
    }
    try {//构造一个NIOServerCnxnFactory
        ServerCnxnFactory serverCnxnFactory = 
        (ServerCnxnFactory) Class.forName(serverCnxnFactoryName)
                .getDeclaredConstructor().newInstance();
        LOG.info("Using {} as server connection factory", serverCnxnFactoryName);
        return serverCnxnFactory;
    } catch (Exception e) {
        /**异常**/
    }
}

这里和客户端构造NIO实例差不多,也是先取默认属性,没有拿到就把NIOServerCnxnFactory这个工厂类的名字传递过去,然后new了一个NIOServerCnxnFactory的实例,包装成为ServerCnxnFactory对象,最后就返回了一个ServerCnxnFactory的NIO实例。完了以后跳出回到runFromConfig(***)方法里去。

既然说已经初始化结束了,那么后面就该创建Socket连接了吧。所以紧接着的configure(***)方法cnxnFactory.configure(config.getClientPortAddress(),config.getMaxClientCnxns());就是做这件事情的。这是一个抽象方法,在NIOServerCxnFactory类中实现的。

public void configure(InetSocketAddress addr, int maxcc) throws IOException {
    configureSaslLogin();
    //这里new了一下线程,先记住这里,这里传递的this,就是把当前类作为线程
    thread = new ZooKeeperThread(this, "NIOServerCxn.Factory:" + addr);
    thread.setDaemon(true);//守护线程
    maxClientCnxns = maxcc;
    this.ss = ServerSocketChannel.open();//打开一个socket连接channel
    ss.socket().setReuseAddress(true);
    LOG.info("binding to port " + addr);
    ss.socket().bind(addr);//绑定地址
    ss.configureBlocking(false);
    ss.register(selector, SelectionKey.OP_ACCEPT);
}

果然我们在这个方法里能够看到先ServerSocketChannel.open()打开了一个socket channel,再后面就绑定了addr这个地址,现在就可以接收别人发来的请求了。过了这里回到runFromConfig(***)继续走,终于我们就到了执行的地方了cnxnFactory.startup(zkServer);。这个方法同样也是一个抽象方法,在NIOServerCxnFactory类中实现:

public void startup(ZooKeeperServer zks) throws ***Exception {
    start();
    setZooKeeperServer(zks);
    zks.startdata();
    zks.startup();
}

这里的代码很简单,但是这些逻辑很绕,我们一个一个来讲。我们先来看start():

public void start() {
    if (thread.getState() == Thread.State.NEW) {
        thread.start();
    }
}

我们发现里面只有一个thread启动,那么这thread是哪里来呢?这个其实就是刚才让大家在configure(***)方法里特别关照的那个地方,我在这里贴上thread = new ZooKeeperThread(this, "NIOServerCxn.Factory:" + addr);,这里构造的时候传递的是this,这个this其实传递的就是NIOServerCxnFactory这个类,为什么能传递这个类进来呢,因为NIOServerCxnFactory实现了Runnable接口也是一个线程类。所以这就捋清楚了thread.start();其实真正执行的方法就是NIOServerCxnFactory.run()方法,先放在这里我们一会儿再讲。

启动时加载数据

然后setZooKeeperServer(zks);把ZookeeperServer的实例set进去,再接着zks.startdata();就是就是我们刚刚说的加载数据了。

public void startdata() throws ***Exception {
    if (zkDb == null) {//如果dataBase是空的就会new一个database
        zkDb = new ZKDatabase(this.txnLogFactory);
    }  
    if (!zkDb.isInitialized()) {
        loadData();//没有初始化则加载数据
    }
}

如果说zkDB是空的就会new一个新的ZKDatabase对象,如果发现这个zkDb还没有初始化,就会进入if (!zkDb.isInitialized())调用方法加载数据loadData();

public void loadData() throws ***Exception {
    if(zkDb.isInitialized()){
        setZxid(zkDb.getDataTreeLastProcessedZxid());
    } else {
        //没有初始化
        setZxid(zkDb.loadDataBase());
    }
    /**不相关,略**/
}

它从哪里加载数据呢,如果说zkDb.isInitialized()==false没有加载数据,那么就调用loadDataBase()这个方法加载数据:

public long loadDataBase() throws IOException {  
    long zxid = snapLog.restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener);
    initialized = true;
    return zxid;
}

这里snapLog就是刚才说的工具类FileTxnSnapLog,我们这里传入的dataTree就是我们打的快照的内容,所以我们要看restore(dataTree, sessionsWithTimeouts, commitProposalPlaybackListener)

public long restore(DataTree dt, Map<Long, Integer> sessions, PlayBackListener listener) throws IOException {
    snapLog.deserialize(dt, sessions);//把数据加载到DataTree中
    return fastForwardFromEdits(dt, sessions, listener);//找事务并取出
}

这里面的snapLog还是快照的日志,然后snapLog.deserialize(dt, sessions);把快照的日志反序列化出来数据,加载到DataTree中,deserialize里就是具体反序列化的内容,那么后面return的是个什么东西呢?return的这个方法就是找事务并取出来用,我们后面【加载快照数据到内存】专门会详细讲到,这里加载数据到此为止。

ZookeeperServer的启动

那么startup()方法里面就剩一个方法了zks.startup();,那么我们继续走:

public synchronized void startup() {
    //sessionTracker:Session追踪器
    if (sessionTracker == null) {
        createSessionTracker();  //如果没有,创建一个出来
    }
    startSessionTracker(); //开始运行session跟踪器
    setupRequestProcessors(); //请求处理器
    registerJMX();
    setState(State.RUNNING);
    notifyAll();
}

我们知道网络传输在客户端和服务端之间是有session机制的,session一般会在服务端有一个倒计时,用来追踪客户端连接用的,这个跟踪器做了什么事情呢?那就得看下startSessionTracker();里面做了什么,进入以后里面就一句话((SessionTrackerImpl)sessionTracker).start();说明这也是一个线程类,所以我们稍微去SessionTrackerImpl.run()方法看一下:

synchronized public void run() {
    try {
        while (running) {
            currentTime = Time.currentElapsedTime();
            if (nextExpirationTime > currentTime) {
                this.wait(nextExpirationTime - currentTime);//不断地倒计时
                continue;
            }
            SessionSet set;
            set = sessionSets.remove(nextExpirationTime);//如果有过期的,把过期的移除
            if (set != null) {
                for (SessionImpl s : set.sessions) {
                    setSessionClosing(s.sessionId);
                    expirer.expire(s);//然后通知客户端,关闭session
                }
            }
            nextExpirationTime += expirationInterval;
        }
    } catch (InterruptedException e) {
        handleException(this.getName(), e);
    }
    LOG.info("SessionTrackerImpl exited loop!");
}

看里面的代码没有什么难理解的地方,无非就是不断地做减法作为倒计时,然后发现过期的就从集合sessionSets中拿出来并移除,如果if (set != null)那就必然是这个session过期了,于是调用expirer.expire(s);通知客户端关闭session。具体是怎么关闭的,本篇不做详解,后续会详细说。

Zookeeper服务器的启动解读到这里先停一停,后面还有好几个大块的内容:请求处理器链的概念是什么,NIOServerCnxnFactory线程有什么用,处理器链的流程是什么样的,以及启动服务器的流程图等等。全部容纳在一起,篇幅是在太长,所以笔者拆分成两篇文章阅读。想要继续探究后续Zookeeper是怎么继续的请移步【Zookeeper 源码解读系列, 单机模式(四)】觉得疲倦的同学,也可以休息一下,感谢大家读到这里。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值