ClientCnxn是ZooKeeper客户端的核心工作类,负责维护客户端与服务端之间的网络连接并进行一系列网络通信。下面是ClientCnxn内部的工作原理。
Packet
Packet是ClientCnxn内部定义的一个对协议层的封装,作为ZooKeeper中请求与响应的载体,其数据结构如下图所示。
从上图中可以看到,Packet中包含了最基本的请求头(requestHeader)、响应头(replyHeader)、请求体(request)、响应体(response)、节点路径(clientPath/serverPath)和注册的Watcher(watchRegistration)等信息。
针对Packet中这么多的属性,读者可能会疑惑他们是否都会在客户端和服务端之间进行网络传输?答案是否定的。Packet的createBB()方法负责对Packet对象进行序列化,最终生成可用于底层网络传输的ByteBuffer对象。在这个过程中,只会将requestHeader、request和readOnly三个属性进行序列化,其余属性都保存在客户端的上下文,不会进行与服务端之间的网络传输。
outgoingQueue和pendingQueue
ClientCnxn中,有两个比较核心的队列outgoingQueue和pendingQueue,分别代表客户端的请求发送队列和服务端响应的等待队列。Outgoing队列是一个请求发送队列,专门用于存储那些需要发送到服务端的Packet集合。Pending队列是为了存储那些从客户端发送到服务端的,但是需要等待服务端响应的Packet集合。
ClientCnxnSocket:底层Socket通信层
ClientCnxnSocket定义了底层Socket通信的接口。在ZooKeeper3.4.0以前的版本中,客户端的这个底层通信层并没有被独立出来,而是混合在了ClientCnxn代码中。但后来为了使客户端代码结构更为清晰,同时也是为了便于对底层Socket层进行扩展(例如使用Netty来实现),因此3.4.0版本开始,抽取出了这个接口类。在使用ZooKeeper客户端的时候,可以通过在zookeeper.clientCnxnSocket这个系统变量中配置ClientCnxnSocket实现类的全类名,以指定底层Socket通信层的自定义实现,例如,-Dzookeeper.clientCnxnSocket=org.apache.zookeeper.ClientCnxnSocketNIO。在ZooKeeper中,其默认的实现是ClientCnxnSocketNIO。该实现类使用Java原生的NIO接口,其核心是doIO逻辑,主要负责对请求的发送和响应接收过程。
请求发送
在正常情况下(即客户端与服务端之间的TCP连接正常且会话有效地情况下),会从outgoingQueue队列中提取出一个可发送的Packet对象,同时生成一个客户端请求序号XID并将其设置到Packet请求头中去,然后将其序列化后进行发送。这里提到了“获取一个可发送的Packet对象”,那么什么样的Packet是可发送的呢?在outgoingQueue队列中的Packet整体上是按照先进先出的顺序被处理的,但是如果检测到客户端与服务端之间正在处理SASL权限的话,那么那些不含请求头(requestHeader)的Packet(例如会话创建请求)是可以被发送的,其余的都无法被发送。
请求发送完毕后,会立即将该Packet保存到pendingQueue队列中,以便等待服务端响应返回后进行相应的处理,如下图所示。
响应接收
客户端获取到来自服务器的完整响应数据后,根据不同的客户端请求类型,会进行不同的处理。
- 如果检测到当前客户端还尚未进行初始化,那么说明当前客户端与服务端之间正在进行会话创建,那么就直接接收到的ByteBuffer(incomingBuffer)序列化成ConnectResponse对象。
- 如果当前客户端已经处于正常的会话周期,并且接收到的服务端响应是一个事件,那么ZooKeeper客户端会将接收到的ByteBuffer(incomingBuffer)序列化成WatcherEvent对象,并将该事件放入待处理队列中。
- 如果是一个常规的请求响应(指的是Create、GetData和Exist等操作请求),那么会从pendingQueue队列中取出一个Packet来进行相应的处理。ZooKeeper客户端首先会通过检验服务端响应中包含的XID值来确保请求处理的顺序性,然后再将接收到的ByteBuffer(incomingBuffer)序列化成相应的Response对象。
最后,会在finishPacket方法中处理Watcher注册等逻辑。
SendThread
SendThread是客户端ClientCnxn内部一个核心的I/O调度线程,用于管理客户端和服务端之间的所有网络I/O操作。在ZooKeeper客户端的实际运行过程中,一方面,SendThread维护了客户端与服务端之间的会话生命周期,其通过在一定的周期频率内向服务端发送一个PING包来实现心跳检测。同时,在会话周期内,如果客户端与服务端之间出现TCP连接断开的情况,那么就会自动且透明化的完成重连操作。
另一方面,SendThread管理了客户端所有的请求发送和响应接收操作,其将上层客户端API操作转换成相应的请求协议并发送到服务端,并完成对同步调用的返回和异步调用的回调。同时,SendThread还负责将来自服务端的事件传递给EventThread去处理。
EventThread
EventThread是客户端ClientCnxn内部的另一个核心线程,负责客户端的事件处理,并触发客户端注册的Watcher监听。EventThread中有一个waitingEvents队列,用于临时存放那些需要被触发的Object,包括那些客户端注册的Watcher和异步接口中注册的回调器AsyncCallback。同时,EventThread会不断地从waitingEvents这个队列中取出Object,识别出其具体类型(Watcher或者AsyncCallback),并分别调用process和processResult接口方法来实现对事件的触发和回调。