1.引入
前面介绍了客户端创建流程,最终调用了ClientCnxn.start方法启动了两个重要的线程SendThread和eventThread,这两个就是客户端处理请求的关键所在,所以在本篇中会逐一介绍这两个线程的作用。
为了方便介绍客户端请求处理过程,我们接着前面介绍的cli解析分析,当前以delete操作为例,最终介绍到了
DeleteRequest request = new DeleteRequest();
request.setPath(serverPath);
request.setVersion(version);
ReplyHeader r = cnxn.submitRequest(h, request, null, null);
其中cnxn.submitRequest就会通过客户端的请求机制访问zk服务端。
2.客户端请求
提交请求的处理流程:
- 将请求封装成packet等待线程处理(后面会介绍)
- 如果通过requestTimeout变量控制是否等待处理结果,如果小于0表示一直等到处理结束。
- 处理请求异常,主要是清除socket连接,清除处理队列等。
ClientCnxn.java
-------------------
public ReplyHeader submitRequest(RequestHeader h, Record request,
Record response, WatchRegistration watchRegistration,
WatchDeregistration watchDeregistration)
throws InterruptedException {
ReplyHeader r = new ReplyHeader();
// 将packet放入队列中
Packet packet = queuePacket(h, r, request, response, null, null, null,
null, watchRegistration, watchDeregistration);
synchronized (packet) {
// 如果reequestTimeout<=0表示一直等待处理结束(同步)
if (requestTimeout > 0) {
// 在超时时间内等待处理结果
waitForPacketFinish(r, packet);
} else {
// Wait for request completion infinitely
while (!packet.finished) {
packet.wait();
}
}
}
// 如果处理超时
if (r.getErr() == Code.REQUESTTIMEOUT.intValue()) {
sendThread.cleanAndNotifyState();
}
return r;
}
客户端将请求封装成packet之后,如果校验状态没有问题之后,就会将packet添加到outgoingQueue队列中。
ClientCnxn.java
------------------
public Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration,
WatchDeregistration watchDeregistration) {
Packet packet = null;
// 组装packet对象
packet = new Packet(h, r, request, response, watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
packet.watchDeregistration = watchDeregistration;
synchronized (state) {
// 服务状态不可用
if (!state.isAlive() || closing) {
conLossPacket(packet);
} else {
if (h.getType() == OpCode.closeSession) {
closing = true;
}
// 将有效的请求(packet)放入到outgoingQueue队列中
outgoingQueue.add(packet);
}
}
// 通知有新的请求,对于NIO连接来说,唤醒阻塞在selector.select上的线程
sendThread.getClientCnxnSocket().packetAdded();
return packet;
}
从上面可以看出,一个正常的请求最终会放到outgoingQueue队列中,然后就不进行任何操作,那它是如何偷偷地发送到服务端地呢?这个就要看前面介绍的SendThread(ClientCnxn内部类)了,我们来看看这个线程究竟在干什么吧。
- 判断当前客户端和zk服务端是否建立连接,如果还没有建立连接,就创建
- 如果是安全连接,就先校验客户端连接
- 如果客户端请求超时,抛出异常
- 客户端和服务端连接正常,需要定期发送心跳信息到服务端,并更新客户端连接状态信息
- 如果连接的服务端处理仅读模式,需要重新寻找一个可以支持读写的服务端连接,并更新服务连接信息
- 处理客户端请求(核心逻辑)
ClientCnxn.java
-----------------
public void run() {
..........
while (state.isAlive()) {
try {
if (!clientCnxnSocket.isConnected()) {
if (closing) {
break;
}
if (rwServerAddress != null) {
serverAddress = rwServerAddress;
rwServerAddress = null;
} else {
serverAddress = hostProvider.next(1000);
}
// 如果没有连接服务端,重新连接
startConnect(serverAddress);
clientCnxnSocket.updateLastSendAndHeard();
}
if (state.isConnected()) {
// 安全连接配置...省略
// 发送认证事件
if (sendAuthEvent) {
eventThread.queueEvent(new WatchedEvent(
Watcher.Event.EventType.None,
authState,null));
if (state == States.AUTH_FAILED) {
eventThread.queueEventOfDeath();
}
}
}
to = readTimeout - clientCnxnSocket.getIdleRecv();
} else {
to = connectTimeout - clientCnxnSocket.getIdleRecv();
}
// 如果session已经超时
if (to <= 0) {
String warnInfo;
warnInfo = "Client session timed out, have not heard from server in "
+ clientCnxnSocket.getIdleRecv()
+ "ms"
+ " for sessionid 0x"
+ Long.toHexString(sessionId);
LOG.warn(warnInfo);
throw new SessionTimeoutException(warnInfo);
}
// ------------------------发送心跳------------------------
if (state.isConnected()) {
int timeToNextPing = readTimeout / 2 - clientCnxnSocket.getIdleSend() -
((clientCnxnSocket.getIdleSend() > 1000) ? 1000 : 0);
if (timeToNextPing <= 0 || clientCnxnSocket.getIdleSend() > MAX_SEND_PING_INTERVAL) {
sendPing();
clientCnxnSocket.updateLastSend();
} else {
if (timeToNextPing < to) {
to = timeToNextPing;
}
}
}
// ------------------------发送心跳------------------------
// 如果是仅读模式,寻找读写服务器
if (state == States.CONNECTEDREADONLY) {
long now = Time.currentElapsedTime();
int idlePingRwServer = (int) (now - lastPingRwServer);
if (idlePingRwServer >= pingRwTimeout) {
lastPingRwServer = now;
idlePingRwServer = 0;
pingRwTimeout =
Math.min(2*pingRwTimeout, maxPingRwTimeout);
pingRwServer();
}
to = Math.min(to, pingRwTimeout - idlePingRwServer);
}
// 处理请求的关键逻辑,详情看实现类。
clientCnxnSocket.doTransport(to, pendingQueue, ClientCnxn.this);
clientCnxnSocket.doTransport将真正看到客户端是如何处理请求的,由于它有NIO和Netty两种实现方式,这里介绍默认的实现方式,也就是NIO(对应ClientCnxnSocketNIO)。
- 如果是客户端和服务端连接行为,就添加认证信息,watcher等信息到outgoingQueue队列队首等待处理。
- 如果是读写行为,调用doIO方法处理。
ClientCnxnSocketNIO.java
----------------------
@Override
void doTransport(int waitTimeOut, List<Packet> pendingQueue, ClientCnxn cnxn)
throws IOException, InterruptedException {
selector.select(waitTimeOut);
Set<SelectionKey> selected;
synchronized (this) {
selected = selector.selectedKeys();
}
for (SelectionKey k : selected) {
SocketChannel sc = ((SocketChannel) k.channel());
if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {
// 处理客户端连接行为
if (sc.finishConnect()) {
updateLastSendAndHeard();
updateSocketAddresses();
sendThread.primeConnection();
}
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
// 处理读写行为(核心)
doIO(pendingQueue, cnxn);
}
}
.....
}
doIO这里我们分成写流程和读流程。
客户端请求流程(写流程):
- 取出outgoingQueue队首的packet,并序列化
- 向zk服务端发送序列化之后的packet
- outgoingQueue移除该packet,pendingQueue添加该packet表明这个packet等待处理结束。
if (sockKey.isWritable()) {
// 从outgoingQueue中获取队列的第一个元素
Packet p = findSendablePacket(outgoingQueue,
sendThread.tunnelAuthInProgress());
if (p != null) {
updateLastSend();
if (p.bb == null) {
if ((p.requestHeader != null) &&
(p.requestHeader.getType() != OpCode.ping) &&
(p.requestHeader.getType() != OpCode.auth)) {
// 设置客户端请求序号
p.requestHeader.setXid(cnxn.getXid());
}
// 设置packet的序列化请求对象
p.createBB();
}
// 向zookeeper的服务端发送请求
sock.write(p.bb);
if (!p.bb.hasRemaining()) {
sentCount.getAndIncrement();
// 移除packet
outgoingQueue.removeFirstOccurrence(p);
if (p.requestHeader != null
&& p.requestHeader.getType() != OpCode.ping
&& p.requestHeader.getType() != OpCode.auth) {
synchronized (pendingQueue) {
// 等待队列中添加packet
pendingQueue.add(p);
}
}
}
}
客户端读流程(服务端响应),主要通过sendThread.readResponse实现,由于实现代码比较多直接,在这里直接描述实现过程。
- 解析响应的header信息,反序列化成ReplyHeader对象(该对象主要需要关注err属性,err=0表示响应没有出现问题)
- 处理replyHeader中的xid属性:
- -2: 客户端ping响应结果,直接忽略
- -4: 服务端认证请求失败,将调用eventThread.queueEvent添加一个客户端需要处理的认证失败的WatcherEvent对象
- -1: 说明根据客户端的请求路径,触发了watcher需要将响应反序列成WatcherEvent对象,等待本客户端处理。
if (sockKey.isReadable()) {
// 读取服务端返回结果
int rc = sock.read(incomingBuffer);
if (rc < 0) {
throw new EndOfStreamException(
"Unable to read additional data from server sessionid 0x"
+ Long.toHexString(sessionId)
+ ", likely server has closed socket");
}
if (!incomingBuffer.hasRemaining()) {
incomingBuffer.flip();
if (incomingBuffer == lenBuffer) {
recvCount.getAndIncrement();
readLength();
} else if (!initialized) {
// 读取初始化结果
readConnectResult();
enableRead();
if (findSendablePacket(outgoingQueue,
sendThread.tunnelAuthInProgress()) != null) {
enableWrite();
}
lenBuffer.clear();
incomingBuffer = lenBuffer;
updateLastHeard();
initialized = true;
} else {
sendThread.readResponse(incomingBuffer);
lenBuffer.clear();
incomingBuffer = lenBuffer;
updateLastHeard();
}
}
}
在上面介绍了,处理服务端的响应时主要是会调用eventThread.queueEvent方法,最终将客户端需要触发的watcher列表和该watcherEvent对象添加到waitingEvents,即客户端等待处理响应的队列中。那这个队列的数据是如何进行消费的呢?这个时候就需要看看EventThread线程执行过程了。
// eventThread线程执行
public void run() {
try {
isRunning = true;
while (true) {
// 从等待线程中获取事件(先进先出)
Object event = waitingEvents.take();
if (event == eventOfDeath) {
wasKilled = true;
} else {
// 如果不是结束事件,处理事件
processEvent(event);
}
if (wasKilled)
synchronized (waitingEvents) {
if (waitingEvents.isEmpty()) {
isRunning = false;
break;
}
}
}
......
// 处理事件的核心逻辑
private void processEvent(Object event) {
try {
if (event instanceof WatcherSetEventPair) {
// 存在多个watcher
// each watcher will process the event
WatcherSetEventPair pair = (WatcherSetEventPair) event;
for (Watcher watcher : pair.watchers) {
try {
// 多个watcher处理该事件
watcher.process(pair.event);
} catch (Throwable t) {
LOG.error("Error while calling watcher ", t);
}
}
所以eventThread主要就是做了两件事情:
- 监听waitingEvents队列,并取出等待客户端处理的事件
- 如果存在事件,会调用对应的watcher执行(客户端在请求服务端时会添加一个watcher对象同时指定watcher的处理方式)
本文详细解析了ZooKeeper客户端处理请求的过程,包括SendThread和EventThread的工作原理,客户端如何封装请求并发送到服务端,以及服务端响应的处理流程。


被折叠的 条评论
为什么被折叠?



