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

本文深入剖析Zookeeper中EventThread的工作原理,揭示客户端如何处理来自服务端的事件通知,包括事件监听器的注册、事件返回后的触发机制及EventThread运行流程。

前情提要

上一篇Zookeeper 源码解读系列, 单机模式(四)主要讲解了单机模式下Zookeeper的处理器链的工作原理和具体到代码中是怎么实现的,包括每个处理器链做了什么工作、各个处理器是怎么连接的、事务的存储、快照是如何打的等等。那么回顾一下,到目前为止我们已经讲了单机模式下Zookeeper客户端和服务端的启动、交互、SendThread和处理器链等等,我们还剩下了一个很重要的线程类EventThread,我想应该会有同学应该还记得EventThreadSendThread是同时启动的,但是我们一直把EventThread束之高阁,在之前的讲解中也是碰到了就忽略掉,今天我们这一篇就来说说EventThread这个重要的线程做了什么。本篇也会被收录到【Zookeeper 源码解读系列目录】中。

EventThread客户端流程

在讲解之前,我们先问一个问题,服务端除了接受客户端的请求以外,会不会做一些别的事情呢,比如主动推送一些内容给客户端呢?当然是有的,最明显的就是Watch事件,比如客户端1在NodeA上绑定了一个监听器,然后客户端2改变了NodeA,那么客户端1怎么知道NodeA改变了呢,这里就需要服务端推送这个change给客户端1,其实这个推送就是和EventThread有关系。

所以要探究EventThread,我们还是要看接受请求的这里,因为不管是一个正常的结果返回还是一个监听事件,都是通过一个结果返回出去的,我们之前已经将结果,客户端接受服务端返回的逻辑和发送消息给服务端的逻辑其实是在一个地方的,就是在SendThread这个线程里,所以说我们的入口还是SendThread.run(),这些内容已经在【单机模式二】详细说过,就不再赘述。这里为了方便大家对照源码找到对应的位置,只会贴上必要的代码,供大家检索使用,真正到了需要详解的地方,才会去贴上相对完整的代码,那我们还是先进入到SendThread.run()。这里特别要提醒一点SendThreadClientCnxn的内部类,我们又回到了客户端的代码,上两篇我连续讲了服务端的代码,大家不要找错了地方:

public void run() {
	/**略**/
  while (state.isAlive()) {
      try {
      	/**略**/ //跳到这里
        clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this);
      } catch (Throwable e) {/**略**/}
      /**略**/
  }
}

这里找到clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this);继续进入这个方法:

void doTransport(int waitTimeOut, List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn) throws ***Exception {
    /**略**/
    for (SelectionKey k : selected) {
        /**略**/
        if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {
            /**略**/
        } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
            doIO(pendingQueue, outgoingQueue, cnxn);//跳到这里
        }
    }
   /**略**/
}

接着我们找到doIO(pendingQueue, outgoingQueue, cnxn);并且进入这个方法:

void doIO(List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn) throws ***Exception {
    /**略**/
    if (sockKey.isReadable()) {
        /**略**/
        if (!incomingBuffer.hasRemaining()) {
            /**略**/
            if (incomingBuffer == lenBuffer) {
                /**略**/
            } else if (!initialized) {//连接有没有初始化
                /**略**/
            } else {//如果初始化已经完成
                sendThread.readResponse(incomingBuffer);//跳到这里
                /**略**/
            }
        }
    }
    /**略**/
}

找到读就绪if (sockKey.isReadable()),一直走到sendThread.readResponse(incomingBuffer)然后继续:

void readResponse(ByteBuffer incomingBuffer) throws IOException {
    ByteBufferInputStream bbis = new ByteBufferInputStream(incomingBuffer);//序列化服务端的返回结果
    BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
    ReplyHeader replyHdr = new ReplyHeader();
    replyHdr.deserialize(bbia, "header");//反序列化构建ReplyHeader
    /**xid=-2,ping命令;xid=-4,auth命令,验证命令;无关本次内容略过**/
    if (replyHdr.getXid() == -1) {//xid=-1,WatcherEvent的逻辑,执行事件
        /**打印Log**/
        WatcherEvent event = new WatcherEvent();//创建WatcherEvent对象
        event.deserialize(bbia, "response");//event反序列化response
        if (chrootPath != null) {
            String serverPath = event.getPath();//拿到节点
            if(serverPath.compareTo(chrootPath)==0)
                event.setPath("/");
            else if (serverPath.length() > chrootPath.length())
                event.setPath(serverPath.substring(chrootPath.length()));
            else { /**打印Log**/ }
        }
        WatchedEvent we = new WatchedEvent(event);  //包装成WatchedEvent
        /**打印Log**/
        eventThread.queueEvent( we );//eventThread调用
        return;
    }
	/**以下略,有需要再贴**/
}

进入以后直接找到if (replyHdr.getXid() == -1),当xid=-1的时候,就是WatcherEvent的逻辑分支,也就到了执行事件的逻辑了。此处笔者要提醒大家EventThread线程是和SendThread线程一起调用start()方法启动的,尽管我们讲EventThread比较晚,但是此时EventThread早已经启动了。那么我们进入这个if语句的逻辑,就看见一个WatcherEvent的实例对象event = new WatcherEvent();被创建出来了,这个event其实就是接收监听事件的结果的,比如某个客户端修改了数据,其他客户端要感知到就是通过这个WatcherEvent接收的。然后就是拿数据嘛,所以反序列化event.deserialize(bbia, "response");服务器返回来的response,没错这里取出返回的事件。下面一个if (chrootPath != null)这里是处理节点路径的先不关心。再往下看到了把服务端返回的event又包装成了一个WatchedEvent对象we = new WatchedEvent(event);,注意这里的WatchedEvent和上面的WatcherEvent很像但不是一个类,WatchedEvent这个类就是事件,哪个节点(path)触发了某个事件等等都是用这个类处理的。再后面终于使用到了eventThread线程,调用了方法queueEvent( we )并且把事件WatchedEvent对象作为参数传递进入了,那就看看进去做了什么:

public void queueEvent(WatchedEvent event) {
    if (event.getType() == EventType.None
            && sessionState == event.getState()) {
        return;
    }
    sessionState = event.getState();
    WatcherSetEventPair pair = new WatcherSetEventPair(
            watcher.materialize(event.getState(), event.getType(),
                    event.getPath()),
                    event);//创建pair
    waitingEvents.add(pair);//pair加入队列
}

一直读这一系列的文章的同学一定对这里异常的眼熟对不对,没错,这里又是NIO的思想实现的,这里几乎没做什么事情,先用event创建了一个WatcherSetEventPair对象pair,然后放到waitingEvents队列里。我们这里先不管什么是pair。一句话总结来说EventThread在这里做了什么事情呢?当SendThread接收服务器发送的事件以后,EventThread就把服务端告诉客户端的这个事件存到EventThread线程的一个队列里。有点绕但是一定要理清楚这个逻辑。

事件监听器的注册

了解到这里以后我们回头看下什么是pair,分析这一句构造WatcherSetEventPair的代码WatcherSetEventPair pair = new WatcherSetEventPair(watcher.materialize(event.getState(), event.getType(),event.getPath()), event);。很明显这里就是把我们从客户端传来的event绑定到了WatcherSetEventPair上。在哪里绑定的呢,就是在构造方法这里调用的watcher.materialize(***),那么这个watch是什么意思呢,这个时候我们就得看下我们怎么去绑定的。我们这里拿getData()方法为例子看下,事件是怎么绑定的,我们用一个示例代码解释:

public static void main(String[] args) throws ***Exception {
    ZooKeeper client=new ZooKeeper("localhost:2181,localhost:2182", 5000, new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            System.out.println("连接测试"+event.toString());
        }
    });
    Stat stat=new Stat();
    client.getData("/data", new Watcher() { //拿"/data"的数据的时候就绑定了一个监听器
        @Override
        public void process(WatchedEvent event) {
            if(Event.EventType.NodeDataChanged.equals(event.getType())){
                System.out.println("数据发生了改变");
            }
        }
    },stat);
}

这里是笔者写的一个示例代码,我在拿“/data”数据的时候绑定了一个节点改变NodeDataChanged的监听器。当Node的数据发生修改的时候,这一行"数据发生了改变"就会打印出来。那么我们就去getData(final String path, Watcher watcher, Stat stat)里面看下是怎么写的:

public byte[] getData(final String path, Watcher watcher, Stat stat) throws ***Exception
 {
    final String clientPath = path;
    PathUtils.validatePath(clientPath);
    WatchRegistration wcb = null;  //watch注册器
    if (watcher != null) {
        wcb = new DataWatchRegistration(watcher, clientPath);//创建watch和path绑定
    }
    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); //判断了一下是不是有watcher
    GetDataResponse response = new GetDataResponse();
    //传入submitRequest,在这里封装成packet传给服务器
    ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
    /**if (r.getErr() != 0) KeeperException**/
    if (stat != null) {
        DataTree.copyStat(response.getStat(), stat);
    }
    return response.getData();
}

看代码之前我们先想一个问题:假设我给nodeA绑定一个事件,记作nodeA:new Watch(),客户端在绑定代码之前需不需要把nodeA:new Watch()这整个事件告诉给服务端呢?换句话说需不需要把某个节点对应的某个watcher告诉给服务端呢?

答案是不需要的,因为监听器watcher只要客户端存起来就可以了,服务端只关心哪个节点发生了什么事件,然后抛出并不需要知道watcher里面的具体内容是什么,而客户端只要接收到这些事件以后,检查自己有没有定义监听这个事件。如果定义了那么就触发自己定义的事件,所以信息只要客户端保存即可。那么我们看代码是不是和我们分析的一致。我们看这个方法传入了pathwatcher,我们先看到WatchRegistration进行了一个注册,因为我们是getData所以这里就是wcb = new DataWatchRegistration(watcher, clientPath);,然后传入的watcherclientPath绑定在wcb上,接着走看到构建了一个GetDataRequest对象request,接着request.setPath(serverPath);把路径绑定到request上,下面request.setWatch(watcher != null);判断一下有没有watcher,请大家牢牢记住这里,我们讲解服务端的时候还会用到。最终所有的数据都传入到了cnxn.submitRequest(h, request, response, wcb);这个方法里构建了一个ReplyHeader,那么我们就进入cnxn.submitRequest()看下里面写了什么:

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);//包装成packet对象
    synchronized (packet) {
        while (!packet.finished) {
            packet.wait(); //packet等待返回值

        }
    }
    return r; //返回ReplyHeader对象
}

进入以后看到里面没有什么东西,首先把我们所有的数据构造成为一个packet,然后packet.wait();等待服务器返回的结果,这种类似的逻辑我们也见过,不多说。我们知道最终客户端传送给服务端的数据都会封装成为Packet,然后我们的watchRegistration也传入进去了。这里是不是有一个疑问,刚才明明说服务端不需要知道Watcher的内容,怎么把它也包装成了packet了,这和刚才解释的不就冲突了吗?其实没有冲突,刚才那句话只watcher != null判断了一下,真正传进去的是true或者false,并没有传入实际的watcher对象。所以为什么要把有没有watcher告诉服务端呢?这里要结合服务端去看,我们先放在这里,一会儿再说。所以先进入queuePacket(***)看看这里做了什么:

Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
        Record response, AsyncCallback cb, String clientPath,
        String serverPath, Object ctx, WatchRegistration watchRegistration)
{
    Packet packet = null;
    synchronized (outgoingQueue) {
        packet = new Packet(h, r, request, response, watchRegistration);
        packet.cb = cb;
        packet.ctx = ctx;
        packet.clientPath = clientPath;
        packet.serverPath = serverPath;
        if (!state.isAlive() || closing) {
            conLossPacket(packet);
        } else {
            if (h.getType() == OpCode.closeSession) {
                closing = true;
            }
            outgoingQueue.add(packet);//把命令加入queue
        }
    }
    sendThread.getClientCnxnSocket().wakeupCnxn();
    return packet;
}

这里面没有什么逻辑只有一句话值得我们注意:outgoingQueue.add(packet);。我们又发现了这个queue。回想一下是谁再用这个queueSendThread对不对,SendThread接受结果的时候最终还是要使用packet。这里再次提醒我们目前一直是在分析SendThread里面,还没有涉及到EventThread的分析。以前说过SendThread线程走到了最后都会走到finally块里面的finishPacket(packet)这个方法里,所以我们先跳出当前方法,然后在外面的阻塞语句packet.wait();这里停下,转去ClientCnxn.SendThread.readResponse(***)里面找到finishPacket(packet)这个方法:

private void finishPacket(Packet p) {
    if (p.watchRegistration != null) {
        p.watchRegistration.register(p.replyHeader.getErr());//进入register
    }
    if (p.cb == null) {
        synchronized (p) {
            p.finished = true;
            p.notifyAll();//这里唤醒之前的等待
        }
    } else {
        p.finished = true;
        eventThread.queuePacket(p);
    }
}

从外面传入的packet,就已经是服务端传回来的packet,所以这里面的p就是服务端返回的数据。如果说事件注册器不是空的if (p.watchRegistration != null),那么就进行注册register(p.replyHeader.getErr());

public void register(int rc) {
    if (shouldAddWatch(rc)) {
        Map<String, Set<Watcher>> watches = getWatches(rc);//初始化watch map
        synchronized(watches) {
            Set<Watcher> watchers = watches.get(clientPath);
            if (watchers == null) {
                watchers = new HashSet<Watcher>();
                watches.put(clientPath, watchers);//把节点和其注册的watch对应起来
            }
            watchers.add(watcher);//添加到watches这个map中
        }
    }
}

进入以后我们发现,刚刚设置的所有的watch都被存到了一个mapMap<String, Set<Watcher>> watches,这个map又把节点和其注册的watch对应起来watches.put(clientPath, watchers);,为什么是watchers因为你一个节点可能注册了好几个事件,这些事件就是在这里接受结果并注册的。如果说外面的getdata()方法报错了,那么这里就不需要再注册监听器了,这就保证了只有getdata()的操作能够成功,成功了才会去重新拿出watch再去注册事件。总结下事件监听器注册的流程就是,我们的操作首先成功了以后,客户端接收到服务端返回的response以后才会注册这个事件。注册以后呢,就有有一个判断if (p.cb == null),这个cb是个什么东西呢?转到声明的地方发现是一个异步回调的类AsyncCallback,那么我们进入eventThread.queuePacket(p);看看里面做了什么操作:

       public void queuePacket(Packet packet) {
          if (wasKilled) {
             synchronized (waitingEvents) {
                if (isRunning) waitingEvents.add(packet);
                else processEvent(packet);
             }
          } else {
             waitingEvents.add(packet);
          }
       }

进入以后我们发现这里也是什么都没有做,只是把返回来的packet放到了waitingEvents这个队列里,那么我们可以有以下结论,EventThread如果收到的是事件通知,就会直接唤醒执行run()方法,如果收到的是异步通知,也会放到队列waitingEvents里面去等待调用执行,本节我们主要的精力还是放在watch上。

客户端收到事件返回后的触发机制

我们说过事件的监听和注册以后,就要开始看这些返回的事件是怎么触发的了。这个时候我们要去哪里呢?还是SendThread,到目前为止我们都是在说SendThread里面的内容,看起来有点不务正业,没错就是这样。既然是接收到请求那就必须再回去SendThread.readResponse(***)这个方法里面:

void readResponse(ByteBuffer incomingBuffer) throws IOException {
    ByteBufferInputStream bbis = new ByteBufferInputStream(incomingBuffer);//序列化服务端的返回结果
    BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
    ReplyHeader replyHdr = new ReplyHeader();
    replyHdr.deserialize(bbia, "header");//反序列化构建ReplyHeader
    /**xid=-2,ping命令;xid=-4,auth命令,验证命令;无关本次内容略过**/
    if (replyHdr.getXid() == -1) {//xid=-1,WatcherEvent的逻辑,执行事件
        /**读出来事件,已经说国,略**/
        eventThread.queueEvent( we );//eventThread调用
        return;
    }
	/**以下略,有需要再贴**/
}

我们上面已经贴过这个方法的代码了,这里就贴重点的地方,还进入eventThread.queueEvent( we )里面:

public void queueEvent(WatchedEvent event) {
    /**已经说过,略**/
    WatcherSetEventPair pair = new WatcherSetEventPair(
            watcher.materialize(event.getState(), event.getType(),
                    event.getPath()),
                    event);//创建pair
    waitingEvents.add(pair);//pair加入队列
}

这次我们主要讲解 watcher.materialize(event.getState(), event.getType(), event.getPath()), event);这个方法。现在就已经可以看到传入这个方法的参数是事件的状态、事件的类型、事件的节点等等,重点就在于事件的路径Path,事件的类型Type都传进去了。点进去这是一个接口,所以最后我们看到的是ZooKeeper这个类的实现方法,方法很长,老规矩:

public Set<Watcher> materialize(Watcher.Event.KeeperState state, Watcher.Event.EventType type, String clientPath)
{
    Set<Watcher> result = new HashSet<Watcher>();
    switch (type) {//区分是事件
    case None:
        	/**空事件,略**/
    case NodeDataChanged:
    case NodeCreated: //NodeCreated,NodeDataChanged事件,进入逻辑
        synchronized (dataWatches) {
            addTo(dataWatches.remove(clientPath), result);//添加到节点上
        }
        /**暂时不相关,略**/
        break;
    case NodeChildrenChanged:
        /**孩子节点变更,略**/
    case NodeDeleted:
        /**节点删除,略**/
    default:
   		 /**抛出RuntimeException,略**/
    }
    return result;//返回result
}

首先还是一个switch语句区分事件类型是什么,我们这里还是用刚才写的示例代码中的NodeDataChanged作为例子。代码里NodeDataChangedNodeCreated,公用一套逻辑,所以这里就会走到case NodeCreated:。这里有一个map:dataWatches,这个map就是刚刚我们看的那个注册的map。往里面走从watchersmap中移除并拿到node(clientPath)上面绑定的事件addTo(dataWatches.remove(clientPath), result);并且存到result这个set中,而这里调用的是remove()方法,所以也是为什么原生的客户端的watcher只能被触发一次,就是因为用了以后就被remove掉了。然后我们往下走,找下直接返回了result

那么现在我们已经把result返回出去了,那么WatcherSetEventPair pair是什么呢?这个pair就是封装了这个结果result而已。出来以后就又只剩下一句话了waitingEvents.add(pair);,把pair加入队列。啰嗦到这里,我们终于要进入今天的主题了,EventThread的run()方法。

EventThread.run()

那么通过以上分析代码,我们已经形成了这样一个共识:服务端抛出来的事件,客户端会存在EventThread的队列waitingEvent里面。那么下一步就是一定是取出来用,这个机制我们已经见过很多了,后面还会有,在碰见就不会再多说了。所以我们找到EventThread.run()方法:

public void run() {
   try {
      isRunning = true;
      while (true) {
         Object event = waitingEvents.take();//取出pair
         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**/
}

在run()里面,进入while (true)的第一个事情就把上面的pair取出来了,然后判断是不是已经无效的事件,其实一切正常的情况下会走到processEvent(event);这个执行事件的方法里:

private void processEvent(Object event) {
  try {
      if (event instanceof WatcherSetEventPair) {//如果是WatcherSetEventPair
          WatcherSetEventPair pair = (WatcherSetEventPair) event;//取出pair
          for (Watcher watcher : pair.watchers) {//再去取返回的pair中所有的watch
              try {
                  watcher.process(pair.event);//执行event方法
              } catch (Throwable t) {
                  LOG.error("Error while calling watcher ", t);
              }
          }
      } else {
          /**暂时无关***/
      }
  } catch (Throwable t) {
      LOG.error("Caught unexpected throwable", t);
  }
}

这里的代码也非常的长,贴出来的还是已经过滤的了。进入第一个if(event instanceof WatcherSetEventPair)判断,如果我们传入的event正好就是WatcherSetEventPair,那么就把这个event在转化为WatcherSetEventPair,紧接着的for循环就是取出返回的pair中所有的watcher,然后调用watcher.process(pair.event);方法执行我们的逻辑。那么这个process()方法在哪里了?这个其实就是咱们自己写代码注册watcher的时候实现的process()方法,为了更直观的展示出来,有请我们的示例代码出场:

public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
        ZooKeeper client=new ZooKeeper("localhost:2181", 5000, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                System.out.println("连接测试"+event.toString());
            }
        });
        Stat stat=new Stat();
        client.getData("/data", new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if(Event.EventType.NodeDataChanged.equals(event.getType())){
                    System.out.println("数据发生了改变");
                }
            }
        },stat);
    }

看到上面的两个process()方法了吗,我在里面各做了一个输出,这里实现的就是watcher.process(pair.event);

总结

总结一下事件的机制:
客户端定义了watcher,这个类中定义的有create,delete,change等等事件类型。先发送请求,如果请求成功了,再注册Watcher到我们使用的客户端里。服务端发生改变以后会抛出事件给客户端,比如说当使用getData()的时候,客户端先发送请求,如果这个请求在服务端成功了,再返回来注册客户端event,保存起来。然后客户端拿到事件后,就会查看自己这里定义的事件都有什么类型,有没有定义,如果定义了那么触发这个定义好了的事件就可以了。

到这里EventThread流程基本就结束了,其实也是客户端处理watcher的流程的结束,但是Event的流程还没有结束,因为事件发送给服务器以后,服务器也会进行处理。比如我们的create,set,delete这些命令传递给服务端以后,服务端也会进行一系列的操作,比如至少等经过处理器链对不对。所以下一篇【Zookeeper 源码解读系列, 单机模式(六)】我们就会专门讲解一下:当set命令到服务端以后,服务端是怎么把事件抛出来的。

EventThread流程图

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值