It is with immense excitement to write this blog, if there is assistance, please give me a praise to you
The necessity of the presence watcher
For especially easy to understand example: If my project is based on dubbo + zookeeper build a distributed project, I have three functions the same service provider, with a zookeeper as a registration center, I have three projects registered into the zookeeper to Foreign Exposing services, but the question is, how to write java code to register into the zookeeper? of course join dependence, write the configuration file and then start to become, then, these three service providers that zookeeper body of the client, zookeeper more than one client, which I chose to rely on, which is the client, the light has not ah service provider, to provide services, I have need to service consumers ah, so in the same way, the consumers registered into zookeeper, zookeeper on the existence of a 4 node, that is, four clients, service consumers subscribe zookeeper, pull it to the address of the service provider, and then address cached locally , and then you can remotely call services to consumers, so problem again, in case Which service provider hung up, how to do it? zookeeper must notify the consumer is not it? address in case the day of the service provider becomes Is not it also have to inform consumers? This is the meaning of existence watcher, it solves it
Experimental scene:
Suppose we have successfully launched a zookeeper service and client, and added in advance watcher, then use the console to modify the dynamic node of data, we will find watcher callback phenomenon
Hook function code is added as follows:
public class ZookepperClientTest {
public static void main(String[] args) throws Exception {
ZooKeeper client = new ZooKeeper("localhost", 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.err.println("连接,触发");
}
});
Stat stat = new Stat();
// todo 下面添加的事件监听器可是实现事件的消费订阅
String content = new String(client.getData("/node1", new Watcher() {
@Override
public void process(WatchedEvent event) {
// todo 任何连接上这个节点的客户端修改了这个节点的 data数据,都会引起process函数的回调
// todo 特点1: watch只能使用1次
if (event.getType().equals(Event.EventType.NodeDataChanged)){
System.err.println("当前节点数据发生了改变");
}
}
}, stat));
Look above code adds its own watcher
is, client.getData("/node1", new Watcher() {}
this is a callback hook function does not run when executed, will be executed, such as when certain conditions are met: node1 been deleted, node1 of the data has been modified
getData do what things?
Source follows: getData, the name suggests, the server returns the node data + stat, of course, is when the server node is changed after the call
Following several major mainstream work
- create
WatchRegistration wcb= new DataWatchRegistration(watcher, clientPath);
- It is actually a simple internal type, the path and watch encapsulated into an object
- Create a request, and initialize
request.head=getData=4
- Call ClientCnxn.submitRequest (...), the presence of these packages further information
- request.setWatch (watcher = null!); that he did not go in the watcher package, but only made a mark there watcher
public byte[] getData(final String path, Watcher watcher, Stat stat)
throws KeeperException, InterruptedException
{
// todo 校验path
final String clientPath = path;
PathUtils.validatePath(clientPath);
// the watch contains the un-chroot path
WatchRegistration wcb = null;
if (watcher != null) {
// todo DataWatchRegistration 继承了 WatchRegistration
// todo DataWatchRegistration 其实就是一个简单的内部类,将path 和 watch 封装进了一个对象
wcb = new DataWatchRegistration(watcher, clientPath);
}
final String serverPath = prependChroot(clientPath);
// todo 创建一个请求头
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.getData);
// todo 创建了一个GetDataRequest
GetDataRequest request = new GetDataRequest();
// todo 给这个请求初始化,path 是传递进来的path,但是 watcher不是!!! 如果我们给定了watcher , 这里面的条件就是 true
request.setPath(serverPath);
request.setWatch(watcher != null); // todo 可看看看服务端接收到请求是怎么办的
GetDataResponse response = new GetDataResponse();
// todo 同样由 clientCnxn 上下文进行提交请求, 这个操作应该同样是阻塞的
// todo EventThread 和 SendThread 同时使用一份 clientCnxn的 submitRequest()
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
if (r.getErr() != 0) {
throw KeeperException.create(KeeperException.Code.get(r.getErr()),
clientPath);
}
if (stat != null) {
DataTree.copyStat(response.getStat(), stat);
}
return response.getData();
}
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
I uninstalled the source code below, here came this method, one can see that it is still blocking style, and is further encapsulated packet requet
More importantly, queuePacket()
the last argument of the method, there is a path + watcher we just create wrapper classes
public ReplyHeader submitRequest(RequestHeader h, Record request, Record response, WatchRegistration watchRegistration)
throws InterruptedException {
ReplyHeader r = new ReplyHeader();
// todo 来到这个 queuePacket() 方法在下面, 这个方法就是将 用户输入-> string ->>> request ->>> packet 的过程
Packet packet = queuePacket(h, r, request, response, null, null, null,
null, watchRegistration);
// todo 使用同步代码块,在下面的进行 同步阻塞等待, 直到有了Response响应才会跳出这个循环, 这个finished状态就是在客户端接受到服务端的
// todo 的响应后, 将服务端的响应解析出来,然后放置到 pendingqueue里时,设置上去的
synchronized (packet) {
while (!packet.finished) {
// todo 这个等待是需要唤醒的
packet.wait();
}
}
// todo 直到上面的代码块被唤醒,才会这个方法才会返回
return r;
}
Similarly, in queuePacket () method will be submitted to the outgoingQueue packet, the final consumer is sent to the server seadThread
How to deal with server watchRegistration not empty packet
I am going to explain the follow-up process in standalone mode the server to process the request with an entire blog, so the only conclusion to say this blog
In the server, the user's request will eventually sequentially flows to three Processor
, are
- PrepRequestProcessor
- Modify responsible for some of the state
- SyncRequestProcessor
- The transaction log synchronized to disk
- Final RequestProcessor
- The user's request
We go directly to FinalRequestProcessor
the public void processRequest(Request request) {}方法
, look for him getData()
to make a request which actions the way. Let's a small climax, zks.getZKDatabase().getData(getDataRequest.getPath(), stat, getDataRequest.getWatch() ? cnxn : null);
whether to follow watcher of the server to add different Watcher
Really I have to draw the focus, and when I discovered this, I was super excited mood, as found in the New World
case OpCode.getData: {
lastOp = "GETD";
GetDataRequest getDataRequest = new GetDataRequest();
ByteBufferInputStream.byteBuffer2Record(request.request,
getDataRequest);
DataNode n = zks.getZKDatabase().getNode(getDataRequest.getPath());
if (n == null) {
throw new KeeperException.NoNodeException();
}
PrepRequestProcessor.checkACL(zks, zks.getZKDatabase().aclForNode(n),
ZooDefs.Perms.READ,
request.authInfo);
Stat stat = new Stat();
// todo 这里的操作 getDataRequest.getWatch() ? cnxn : null 对应可客户端的 跟进watcher有没有而决定往服务端传递 true 还是false 相关
// todo 跟进去 getData()
byte b[] = zks.getZKDatabase().getData(getDataRequest.getPath(), stat,
getDataRequest.getWatch() ? cnxn : null);
//todo cnxn的Processor()被回调, 往客户端发送数据 , 什么时候触发呢? 就是上面的 处理事务时的回调 第127行
// todo 构建了一个 rsp ,在本类的最后面将rsp 响应给client
rsp = new GetDataResponse(b, stat);
break;
}
Continue to follow up this getData()
on the server maintains a map of the path + watcher
public byte[] getData(String path, Stat stat, Watcher watcher)
throws KeeperException.NoNodeException {
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
synchronized (n) {
n.copyStat(stat);
if (watcher != null) {
// todo 将path 和 watcher 绑定在一起
dataWatches.addWatch(path, watcher);
}
return n.data;
}
}
Open a command line client, modify the server node status
Book connected to the back, when the client code to create a single ClientCnxn
time, have the following logic, it opens the two daemon thread, sendThread responsible for sending a heartbeat to the server, the user has the relevant IO exchange and server, EventThread and is responsible for processing logic txn matters related to the rising level for the node
// todo start就是启动了在构造方法中创建的线程
public void start() {
sendThread.start();
eventThread.start();
}
So far, clients have the following three threads
- Responsible for handling user input commands in the console main thread
- Daemon thread 1: seadThread
- Daemon thread 2: eventThread
Logic code processing the user input portion of the main thread as follow:
The following code main logic processing command is input by the user, when the branch selected by the user in the end if-else determines what command input
According to our hypothetical scenario, the command entered by the user this is set /path newValue
so, no doubt, after parsing code to perform the following stat = zk.setData(path, args[2].getBytes(),
section
// todo zookeeper客户端, 处理用户输入命令的具体逻辑
// todo 用大白话讲,下面其实就是把 从控制台获取的用户的输入信息转换成指定的字符, 然后发送到服务端
// todo MyCommandOptions 是处理命令行选项和shell脚本的工具类
protected boolean processZKCmd(MyCommandOptions co) throws KeeperException, IOException, InterruptedException {
// todo 在这个方法中可以看到很多的命令行所支持的命令
Stat stat = new Stat();
// todo 获取命令行输入中 0 1 2 3 ... 位置的内容, 比如 0 位置是命令 1 2 3 位置可能就是不同的参数
String[] args = co.getArgArray();
String cmd = co.getCommand();
if (args.length < 1) {
usage();
return false;
}
if (!commandMap.containsKey(cmd)) {
usage();
return false;
}
boolean watch = args.length > 2;
String path = null;
List<ACL> acl = Ids.OPEN_ACL_UNSAFE;
LOG.debug("Processing " + cmd);
if (cmd.equals("quit")) {
System.out.println("Quitting...");
zk.close();
System.exit(0);
} else if (cmd.equals("set") && args.length >= 3) {
path = args[1];
stat = zk.setData(path, args[2].getBytes(),
args.length > 3 ? Integer.parseInt(args[3]) : -1);
printStat(stat);
Continue to follow stat = zk.setData(path, args[2].getBytes(),
the following logic is very simple, is to request the user to input the package come by ClientCnxn
submission queue to submit a method of the class, to the consumer waiting sendThread
The purpose of submitRequest look at the last parameter is null, this parameter is WatchRegistration position, a start is set to null
public Stat setData(final String path, byte data[], int version)
throws KeeperException, InterruptedException
{
final String clientPath = path;
PathUtils.validatePath(clientPath);
final String serverPath = prependChroot(clientPath);
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.setData);
SetDataRequest request = new SetDataRequest();
request.setPath(serverPath);
request.setData(data);
request.setVersion(version);
SetDataResponse response = new SetDataResponse();
ReplyHeader r = cnxn.submitRequest(h, request, response, null);
if (r.getErr() != 0) {
throw KeeperException.create(KeeperException.Code.get(r.getErr()),
clientPath);
}
return response.getStat();
}
This follow-up submitRequest () method , the following source, not place material, it is likewise blocked live until the server in response to the
The current main logic of the code is to request encapsulated into the packet, and then add to ClintCnxn packet waiting in the queue maintained outgoingQueue consumption sendThread
The set came to this method is because we enter the console command triggered, the more important this packet is carried == null WatchRegistration , no doubt, taken out of service when the end FinalRequestProcessor reprocessing watcher == null, it will not save into the path + watcher in maptable
Important: sending transaction message
In FinalRequestProcessor
the public void processRequest(Request request) {}
process, the following codes
//todo 请求头不为空
if (request.hdr != null) {
// 获取请求头
TxnHeader hdr = request.hdr;
// 获取事务
Record txn = request.txn;
// todo 跟进这个方法-----<--!!!!!!-----处理事务的逻辑,在这里面有向客户端发送事件的逻辑, 回调客户端的watcher----!!!!!!-->
rc = zks.processTxn(hdr, txn);
}
Continue with the inside
// todo 处理事物日志
public ProcessTxnResult processTxn(TxnHeader hdr, Record txn) {
ProcessTxnResult rc;
int opCode = hdr.getType();
long sessionId = hdr.getClientId();
// todo 继续跟进去!!!!!!!!!
// todo 跟进 processTxn(hdr, txn)
rc = getZKDatabase().processTxn(hdr, txn);
Follow-up ZkDatabase.java
ofprocessTxn(hdr, txn)方法
public ProcessTxnResult processTxn(TxnHeader hdr, Record txn) {
// todo 跟进 processTxn
return dataTree.processTxn(hdr, txn);
}
To follow upDataTree.java
public ProcessTxnResult processTxn(TxnHeader header, Record txn)
{
ProcessTxnResult rc = new ProcessTxnResult();
try {
rc.clientId = header.getClientId();
rc.cxid = header.getCxid();
rc.zxid = header.getZxid();
rc.type = header.getType();
rc.err = 0;
rc.multiResult = null;
switch (header.getType()) { // todo 根据客客户端发送过来的type进行switch,
case OpCode.create:
CreateTxn createTxn = (CreateTxn) txn;
rc.path = createTxn.getPath();
// todo 跟进这个创建节点的方法
createNode(
createTxn.getPath(),
Based on the value of the request header, and then the judge went to switch branches, we are currently in the console triggered into setData branch as follows: follow this method you can see it is mainly made the following few things
- Using the passed value of the new data replace the old
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
Trigger the specified event watch, what events it? NodeDataChange, which triggered a watcher of it? Check with the inside
//todo setData
public Stat setData(String path, byte data[], int version, long zxid,
long time) throws KeeperException.NoNodeException {
Stat s = new Stat();
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
byte lastdata[] = null;
synchronized (n) {
// todo 修改内存的数据
lastdata = n.data;
n.data = data;
n.stat.setMtime(time);
n.stat.setMzxid(zxid);
n.stat.setVersion(version);
n.copyStat(s);
}
// now update if the path is in a quota subtree.
String lastPrefix;
if((lastPrefix = getMaxPrefixWithQuota(path)) != null) {
this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
- (lastdata == null ? 0 : lastdata.length));
}
// todo 终于 看到了 服务端 关于触发NodeDataChanged的事件
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}
With the inside dataWatches.triggerWatch(path, EventType.NodeDataChanged);
, the source code is as follows, deposit account when the main logic is taken is stored in the service side of the watch, then one by one callback their processor functions, the question is, in the end is what watcher it? I think we can add getData when the client starts () the wather, that is ServerCnxn
// todo 跟进去服务端的 触发事件, 但是吧, 很纳闷. 就是没有往客户端发送数据的逻辑
public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
WatchedEvent e = new WatchedEvent(type,
KeeperState.SyncConnected, path);
HashSet<Watcher> watchers;
synchronized (this) {
watchers = watchTable.remove(path);
if (watchers == null || watchers.isEmpty()) {
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG,
ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"No watchers for " + path);
}
return null;
}
for (Watcher w : watchers) {
HashSet<String> paths = watch2Paths.get(w);
if (paths != null) {
paths.remove(path);
}
}
}
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
// todo 继续跟进去, 看它如何回调的
w.process(e);
}
return watchers;
}
Excited to go and see ServerCnxn the process () method to do what?
To achieve class NIOServerCnxn ServerCnxn indeed very excited to see the server to the client sending transactional messages, and new ReplyHeader (-1, -1L, 0) on the first position parameter is -1, which is it is important, because the client after receiving the flag xid = -1, it will put this process in response to EventThread
@Override
synchronized public void process(WatchedEvent event) {
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"Deliver event " + event + " to 0x"
+ Long.toHexString(this.sessionId)
+ " through " + this);
}
// Convert WatchedEvent to a type that can be sent over the wire
WatcherEvent e = event.getWrapper();
// todo 往服务端发送了 e event类型消息
sendResponse(h, e, "notification");
}
Processing the response using callbacks callbacks watch
Into the SendThread
read ready source section as follows: It is based on header.xid = -1 know this is the type of transaction response
// todo 服务端抛出来的事件, 客户端将把他存在EventThread的 watingEvents 队列中
// todo 它的实现逻辑也是这样, 会有另外一个线程不断的消费这个队列
if (replyHdr.getXid() == -1) {
// -1 means notification
if (LOG.isDebugEnabled()) {
LOG.debug("Got notification sessionid:0x"
+ Long.toHexString(sessionId));
}
// todo 创建watcherEvent 并将服务端发送回来的数据,反序列化进这个对象中
WatcherEvent event = new WatcherEvent();
event.deserialize(bbia, "response");
// convert from a server path to a client path
// todo 将server path 反转成 client path
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.warn("Got server path " + event.getPath()
+ " which is too short for chroot path "
+ chrootPath);
}
WatchedEvent we = new WatchedEvent(event);
if (LOG.isDebugEnabled()) {
LOG.debug("Got " + we + " for sessionid 0x"
+ Long.toHexString(sessionId));
}
//todo 跟进去
eventThread.queueEvent(we);
return;
}
}
In the end, this will add the appropriate method of EventThread
consumption of the queue, follow-upeventThread.queueEvent(we);
// todo
public void queueEvent(WatchedEvent event) {
// todo 如果事件的类型是 none, 或者sessionState = 直接返回
/**
* todo 事件的类型被设计成 watcher 接口的枚举
* None (-1),
* NodeCreated (1),
* NodeDeleted (2),
* NodeDataChanged (3),
* NodeChildrenChanged (4);
*/
if (event.getType() == EventType.None
&& sessionState == event.getState()) {
return;
}
sessionState = event.getState();
// materialize the watchers based on the event
// todo 根据事件的具体类型,将观察者具体化, 跟进去
// todo 这个类是ClientCnxn的辅助类,作用就是将watcher 和它观察的事件封装在一起
WatcherSetEventPair pair = new WatcherSetEventPair(
//todo 跟进这个 materialize方法. 其实就是从map中取出了和当前client关联的全部 watcher set
watcher.materialize(event.getState(), event.getType(),
event.getPath()),
event);
// queue the pair (watch set & event) for later processing
// todo 将watch集合 和 event 进行排队(按顺序添加到队列里了), 以便后续处理 , 怎么处理呢? 就在EventThread的run循环中消费
// todo watingEvent ==> LinkedBlockingQueue<Object>
waitingEvents.add(pair);
}
The above code is mainly to do the following things:
- Remove all watcher and current events related to from the map
- Will be added to the watcher set waitingEvents queue, waiting EventThead consumption
Follow-up watcher.materialize(event.getState(), event.getType(),
will catch up with the following code
case NodeDataChanged: // todo node中的data改变和 nodeCreate 都会来到下面的分支
case NodeCreated:
synchronized (dataWatches) {
// todo dataWatches 就是刚才存放 path : watcher 的map
// todo dataWatches.remove(clientPath) 移除并返回clientPath对应的watcher , 放入 result 中
addTo(dataWatches.remove(clientPath), result);
}
The above dataWatches is saved path + watcher set of map, the above operation is to remove and return the designated watcher, which explains why zk native client will only add watcher callback time
EventThread how the consumer waitingEvents
EventThread is a daemon thread, so it has its own method of operation continues to run, it is to consume this queue in this run method
@Override
public void run() {
try {
isRunning = true;
// todo 同样是无限的循环
while (true) {
// todo 从watingEvnets 中取出一个 WatcherSetEventPair
Object event = waitingEvents.take();
if (event == eventOfDeath) {
wasKilled = true;
} else {
// todo 本类方法,处理这个事件,继续进入,方法就在下面
processEvent(event);
}
if (wasKilled)
synchronized (waitingEvents) {
if (waitingEvents.isEmpty()) {
isRunning = false;
break;
}
}
}
} catc
Continue to follow it processEvent(event)
, eventually calls the following code in this method, Watcher here's what I added to it at the beginning of this blog watcher, so far kick call it a day
watcher.process(pair.event);
It is with immense excitement to write this blog, if there is assistance, please give me a praise to you