紧接着上一篇博客:https://blog.csdn.net/Dongguabai/article/details/82970852
在输出内容中有这样两个结果:
在ZooKeeper中,接口类Watcher用于表示一个标准的事件处理器,其定义了事件通知相关的逻辑,包含KeeperState和EventType两个枚举类,分别代表了通知状态和事件类型,同时定义了事件的回调方法:process(WatchedEvent event)。
那么什么样的操作会产生什么类型的事件呢:
event For “/path” | event For “/path/child” | |
create(“/path”) | EventType.NodeCreated(exists/getData) | 无 |
delete(“/path”) | EventType.NodeDeleted(exists/getData) | 无 |
setData(“/path”) | EventType.NodeDataChanged(exists/getData) | 无 |
create(“/path/child”) | EventType.NodeChildrenChanged(getChildren) | EventType.NodeCreated |
delete(“/path/child”) | EventType.NodeChildrenChanged(getChildren) | EventType.NodeDeleted |
setData(“/path/child”) | 无 | EventType.NodeDataChanged |
这里可以简单测试一下:
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
/**
* @author Dongguabai
* @date 2018/10/9 19:18
*/
public class ZooKeeperWatcherDemo2 {
static final String CONNECT_ADDR = "192.168.220.135:2181,192.168.220.136:2181,192.168.220.137:2181";
static final int SESSION_OUTTIME = 2000;//ms
/**
* 阻塞程序执行,等待zookeeper连接成功
*/
static final CountDownLatch connectedSemaphore = new CountDownLatch(1);
static final String PATH = "/dongguabai";
static final String PATH_CHILD = PATH + "/child";
public static void main(String[] args) {
try {
//连接
ZooKeeper zk = new ZooKeeper(CONNECT_ADDR, SESSION_OUTTIME, event -> {
System.out.println("事件是:" + event.getType());
//如果收到了服务端的响应事件,连接成功
if (Watcher.Event.KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
});
connectedSemaphore.await();
System.out.println("连接成功!");
//binding
zk.exists(PATH,event -> {
System.out.println("create---"+event.getType());
});
//create
zk.create(PATH,"1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zk.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果:
再简单测试下:
import org.apache.zookeeper.*;
import java.util.concurrent.CountDownLatch;
/**
* @author Dongguabai
* @date 2018/10/9 19:18
*/
public class ZooKeeperWatcherDemo2 {
static final String CONNECT_ADDR = "192.168.220.135:2181,192.168.220.136:2181,192.168.220.137:2181";
static final int SESSION_OUTTIME = 2000;//ms
/**
* 阻塞程序执行,等待zookeeper连接成功
*/
static final CountDownLatch connectedSemaphore = new CountDownLatch(1);
static final String PATH = "/dongguabai";
static final String PATH_CHILD = PATH + "/child";
public static void main(String[] args) {
try {
//连接
ZooKeeper zk = new ZooKeeper(CONNECT_ADDR, SESSION_OUTTIME, event -> {
System.out.println("事件是:" + event.getType());
//如果收到了服务端的响应事件,连接成功
if (Watcher.Event.KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
});
connectedSemaphore.await();
System.out.println("连接成功!");
//binding
zk.exists(PATH_CHILD, event -> {
System.out.println("create path child---" + event.getType());
});
zk.exists(PATH, event -> {
System.out.println("create path---" + event.getType());
});
System.out.println("create path");
//create
zk.create(PATH, "1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zk.getChildren(PATH, event -> {
System.out.println("create getChild---" + event.getType());
});
System.out.println("create path child");
zk.create(PATH_CHILD, "1".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zk.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里测试的时候遇到了一个关于getChildren()的问题,具体可参看:
https://blog.csdn.net/Dongguabai/article/details/82987931
从网上其他博客中看到的比较好的总结的表格:
在上一篇博客也介绍过了,exists()方法是可以注册事件的。这里以exists()方法为例,查看org.apache.zookeeper.ZooKeeper#exists(java.lang.String, org.apache.zookeeper.Watcher):
构建了一个WatchRegistration对象,一个header,一个request和一个response,随后客户端会进行request提交,看看submitRequest()方法:
这个方法会组装一个Packet的数据包,随后会加锁,调用wait()方法进行一个阻塞操作。再来看看queuePacket()方法:
这个Packet是客户端和服务端进行通信的一个最小的数据单元。这里会把相关的参数再组织成为一个Packet对象。随后会将Packet输出队列中去:
最后会触发selector的执行:
简单流程如下:
但是明显这个流程是不完整的,因为这里只有发送,那怎么接收呢。一定会有某种机制对这个队列进行某种数据的处理,即肯定会有个东东去消费OutgoingQueue,入队就肯定有个出队。
先来看看ZooKeeper的构造方法:
经过一层层的调用,最终会到这里,注意这里的start()方法:
会将这个watcher赋值给一个全局的defaultWatcher:
后面会new一个ClientCnxn。之前也介绍过一个cnxn会调用submitRequest方法,那个cnxn就是在这里初始化的。来看看这个:
这里初始化了一些信息,但是也做了两个事情,构造并启动SendThread和EventThread两个线程。那么什么时候启动的呢,在上面提到过的start()方法就是用来启动的:
目前整体流程图为:
貌似没有什么关联,但是随便一想,启动两个线程肯定是要搞点事情的,接下来就看看这两个线程到底做了什么。看看SendThread:
一般我们启动一个线程肯定是调用它的run()方法,这个时候毫不犹豫的去看看这个方法,首先可以猜想这个方法肯定是做一些outgoingQueue出队的相关操作。
这个run()方法特别长,没必要过于关注细节,可以先看看判断:
这里是一个循环,即当状态是存活的时候会进入循环:
再会判断连接状态,如果没有开启连接,就开启连接,同时更新最后一次发送的Heard。
最后一定会生成一个to:
这个to相当于是客户端和服务端一个ping的心跳去维持连接。
如果to小于0的话,意味着会话已经超时了:
往下看,有一个很关键的传输方法,有两种实现,基于NIO或者基于Netty:
那到底走哪一个呢,其实只要把握一点,所有的构造初始化一定是在开始的时候做好的,可以再回到ZooKeeper的构造:
看看这个方法到底做了什么:
首先会从配置文件中读取:
如果是null就会是基于NIO的方式,否则会根据读取到的clientCnxnSocketName通过反射去实例化ClientCxnSocket。即默认是走NIO的方式。
可以看看如果是基于Netty的话是怎么做的,主要就是传输数据包:
可以发现一个很重要的方法:
会从之前介绍的outgoingQueue中取出数据包保存到head中。后面就是一些判断以及错误处理机制等。
再往下看会看到一个关键的方法:
也就是说当前从outgoingQueue中获取到的数据如果不为空的话,就会执行这个doWrite()方法:
又会调用一个sendPkt()方法:
先通过p.createBB(),创建缓冲字节,再通过Netty的Channel进行传输,先看看createBB()方法:
主要会序列两个参数:
然后会把bb(ByteBuffer)封装好。最后发送数据包,自此流程已经走通了:
刚刚只是客户端的数据包发送流程,那服务端是怎么接收数据包的呢,先看看这个类:NettyServerCnxn:
这是处理请求的一个类,里面有这样一个方法receiveMessage():
这里如果bb(ByteBuffer)读完了,会执行一个packetReceived()方法:
看看这个方法,记录了消息递增的一个次数:
接下来会进行消息的相关处理:
这是处理数据包的一个方法:
最后会构建一个服务端的Request,再submitRequest():
看看这个submitRequest()方法:
服务端会有很多的processor,它会通过一个链式的方式把这些processor组装起来。这里firstProcessor为空的那段代码看都不用看,肯定不为空啊,看后面那一段代码:
首先会验证数据包,如果数据包验证成功了,会通过firstProcessor.processRequest()做一个处理,而这里会有很多的实现:
具体是哪一个调用我们就要看firstProsessor对应的是哪个实例。可以看看ZooKeeperServer中firstProsessor是在哪里实例化的:
在PrepRequestProcessor中又传递了一个RequestProcessor:
总得来说,相当于是组装了一个链式的调用。
所以这个时候就先去找PrepRequestProcessor:
把当前的request加到了一个submittedRequests里面:
而这个submittedRequests就是一个队列:
可以发现在ZooKeeper中大量的通过多线程的方式去异步化流程,所以在PreRequestProcessor中一定有一个对应的线程去启动。PreRequestProcessor本身就是一个线程:
一定会有一个run()方法:
会从submittedRequests中去take()请求,随后会经过一系列的判断,循环去处理请求,看看pRequest()方法:
根据一系列的switch语句做一些判断。在这个方法的最后:
会发现有一个nextProcessor又去调用了processRequest,根据前面的规律,肯定回想,这个nextProcessor是在哪里实例化的呢:
这个nextProcessor是通过构造传入的,在刚刚也介绍过,这时候实际上是实例化的一个SyncRequestProcessor对应的processor:
也就是说下一个processor就是SyncRequestProcessor,再去看看SyncRequestProcessor的实现:
跟上面的很类似有木有,也是加到一个队列中:
再通过循环的方式去处理:
这个思想可以去借鉴,即链式调用Thread内部维护的一个Queue,通过循环的方式处理Queue里面的内容,后面会简单写一个小Demo演示这个思想。
在这个run()方法的最后:
又发现了nextProcessor,跟上面是类型的,再看看这个nextProcessor是从哪里构造的:
也是通过构造传入的,这里套路都是一样的,通过构造去传入下一个引用。在刚刚也介绍过,这时候实际上是实例化的一个FinalRequestProcessor对应的processor:
照葫芦画瓢,再看看FinalRequestProcessor的实现:
根据名字也可以知道,这是最后一个processor了。先找我们属性的那一段代码:
这个是处理exists请求的。这里通过socket,通过netty最后把request传到了processor,然后request反序列化后获取到了path。获得了path就好说了,剩下的常规操作,获取节点的Stat:
最后会设置成resp:
最后有一个sendResponse()的方法:
其实在刚刚的获取节点的Stat中还有一个关键点,即绑定Watcer:
注意这里传入的不是Watcher,而是cnxn:
传的是服务端的ServerCnxn。来看看这个statNode()方法:
这里做了一个向上转型,因为ServerCnxn实际上是实现了Watcher:
回过头来继续分析statNode()方法:
如果watcher不为空,就会将path和watcher(实际上是ServerCnxn)添加到dataWatches中,这个就是核心:绑定事件:
看看WatchManager中的addWatch()方法:
之前介绍过,在FinalRequestProcessor的最后会调用一个sendResponse()方法:
有多个实现:
来看看Netty的实现:
看看sendbuffer()方法:
也就是通过channel.write()去发送ByteBuffer,至此,服务端完成了响应。
总体流程图如下:
服务端完成了响应,必然客户端也会去接收,再来看看这个类ClientCnxnSocketNetty,有这样一个方法messageReceived():
这个方法是接收服务端的请求,也是一个异步的过程。
看看readResponse()方法,这个是读取服务端返回的header和response:
首先会反序列化header,然后会判断xid,这里有-1、-2和-4:
官方注解也说的很明白了,-2是一个ping的请求,-4是一个授权的请求,-1表示服务端触发事件的时候才会调用。
接着往后面看,会从pendingQueue中移除这个packet,为什么要移除呢,因为packet发送完成后它会加入到pendingQueue这个集合中,直到服务端返回response。
之后会进行一系列服务端的判断校验,并将服务端的信息设置到客户端拿到的packet中,同时会把服务端返回的response反序列化:
这里就是Stat信息:
比如在ZooKeeper的exists方法中,就是通过这个来获取Stat的:
之前也说过了,这些过程都是异步的,那么从response获取到的Stat会不会是空的,这个不必担心,在前面也介绍过了,在提交请求的时候实际上是有一个阻塞的过程的:
以上就是客户端接收响应的过程,到这里还没有完,因为这里还有一个finally:
看看finishPacket()方法,也比较简单,这里就不详细说明了:
客户端接收消息的主要流程如下:
事件注册后,那究竟是怎么触发的呢?在WatchManager中有一个triggerWatch()方法:
以setData()为例,我们知道setData()方法是可以触发Wather的,那就直接看DataTree的setData()方法:
补充说明下:DataTree类维护整个目录树结构,ConcurrentHashMap
这个dataWatches就是WatchManager:
在这里通过dataWatches触发节点(path)和事件类型(EventType.NodeDataChanged),再回过头来看这个truggerWatch()方法到底做了哪些事情:
又调用了一个重载的triggerWatch()方法,首先会封装一个WatchdEvent对象,参数是事件类型,同步状态和path。
这里有个关键点是,它首先会从watchTable中异常这个path(这也就是为什么事件只会触发一次的原因):
这个watchTable就是之前介绍过的那个HashMap:
接着triggerWatch()方法,,紧接着又会循环删除watch2Paths这个Map中的这个path,因为在WatchManager中还维护者一个key是Watcher,value是path的Map,即watch2Paths,这个之前也介绍过:
最后会循环出发Watch:
来看下这个process()方法,这里是不是有点小激动,多么关键的方法总算来了。
这个方法的实现有很多,那到底调用的是哪一个呢?
再反推这个w是从哪里来的,这个w是从watchers中来的:
那watchers是从哪里来的呢:
是从watchTable.remove(path)返回的。那watchTable是从哪里来的呢:
在DataTree的statNode()方法中:
再回想一下之前的调用过程,在FinalRequestProcessor中,我们以exists类型为例:
这里传递的是不是就是cnxn,之前也介绍过了,这里存在一个向上转型:
因此说明调用process()方法的是ServerCnxn,可以看看它的实现:
而这又是个模板的抽象方法,可以看看有哪些实现:
看看NettyServerCnxn的实现:
最终会调用sendResponse()方法,要注意的是这个h中的xid是-1,前面也介绍过了,-1是一个引导流程的非常重要的参数。
会对header和response做一个序列化,之后通过channel再去发送。至此服务端的事件触发已经打通,总的来说服务端会发送一个WatchedEvent给客户端。
客户端的接收流程就还是不变,毕竟代码是公共的嘛,先看看CientCnxnSocketNetty:
收到消息也是会调用整个readResponse方法,这里又回到之前介绍过的地方了:
这里用-1、-2、-4代表执行什么流程,这里也没有强制性的使用设计模式,如果服务端返回的是事件类型,也就是-1,就会进入下面的流程:
经过反序列化获取服务端传过来的WatchedEvent,最终会将WatchedEvent作为参数执行queueEvent():
这里materializedWatchers是空的,就会执行:
来看看materialize()方法:
看到了很熟悉的existWatches,客户端会把所有的事件放到existWatches中去:
简单点说,ZKWatchManager是客户端管理Watcher的,WatchManager是服务端管理Watcher的。
也就是说这里的这个方法返回的watchers的得到的是所有的exists的watcher事件:
最终在ClientCnxn中会添加到队列中:
而这个waitingEvents是在哪里被调用呢,是在EventThread里卖弄调用的,看看EventThread的run()方法:
的确也是从队列里面取到数据然后搞事情,搞什么事情呢,执行processEvent()方法:
会循环遍历调用process()方法,而真正执行的不就是我们在客户端实现Watcher重写的process()方法嘛。就这样最终完成了事件的回调。
更多相关资料:
https://blog.csdn.net/liu857279611/article/details/70495413
https://www.cnblogs.com/programlearning/archive/2017/05/10/6834963.html
https://blog.csdn.net/Dongguabai/article/details/82953133
https://blog.csdn.net/Dongguabai/article/details/82907095
https://www.jianshu.com/p/d6301f07ad39
http://www.cnblogs.com/sunddenly/articles/4087251.html