文章目录

  • 一、概述
  • 二、 Zookeeper 中的 RPC 网络数据结构
  • 2.1 协议数据结构
  • 2.2 核心数据结构 Packet
  • 三、核心源码解析
  • 3.1 建立 Netty 网络连接
  • 3.2 SendThread 从 outgoingQueue 获取并发送 Packet
  • 3.3 同步版 RPC 调用流程(Create API)
  • 3.3.1 创建 Packet 并入队 outgoing (且 Packet.Wait)
  • 3.3.2 处理响应并出队 pendingQueue(且 Packet.notifyAll)
  • 3.4 异步版 RPC 调用流程(Create API)
  • 3.4.1 创建 Packet 并入队 outgoing
  • 3.4.2 入队 waitingEvents 并在出队时进行方法回调
  • 四、同步版 API 和异步版 API 总结与比较
  • 4.1 版本总结
  • 4.2 版本比较
  • 五、内容总结


一、概述

  在前面的博文中我们已经分析了 Zookeeper 中的 Zab 选举部分的代码实现,在这篇博文中我们将再通过源码分析一下 Zookeeper 客户端的网络通信实现。

  博客内所有文章均为 原创,所有示意图均为 原创,若转载请附原文链接。



二、 Zookeeper 中的 RPC 网络数据结构

2.1 协议数据结构

  Zookeeper 中的 RPC 网络协议数据结构概述即包括三个部分:

  1. 起始的 4 byte(int)用于记录实际数据长度(后接实际数据);
  2. RequestHeader 请求头 / ResponseHeader 响应头
  3. Request 请求体 / Response 响应体

  且 RequestHeader 主要包括 xidtype 两部分,其中 xid 代表请求的顺序号,用于保证请求的顺序发送和接收,而 type 代表请求的类型;而 ResponseHeader 主要包括 xidzxid 以及 err ,其中 xid 的作用与 RequestHeader 中的 xid 作用相同,zxid 表示分布事务 id ,而 err 为记录相关的错误信息的错误码。



2.2 核心数据结构 Packet

static class Packet {
	RequestHeader requestHeader;	// 请求头信息
	ReplyHeader replyHeader;		// 响应头信息

	Record request;		// 请求数据
	Record response;	// 响应数据

	AsyncCallback cb;	// 异步回调
    Object ctx;			// 异步回调所需使用的 context

	String clientPath;	// 客户端路径视图
    String serverPath;	// 服务器的路径视图
	boolean finished;	// 是否已经处理完成
    
    ByteBuffer bb;		
    public boolean readOnly;
    WatchRegistration watchRegistration;
    WatchDeregistration watchDeregistration;
	
	// 省略方法逻辑..
}



三、核心源码解析

3.1 建立 Netty 网络连接

// Zookeeper.java
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean canBeReadOnly, 
					HostProvider aHostProvider, ZKClientConfig clientConfig) throws IOException {
        
    // 创建连接管理器
	cnxn = createConnection(connectStringParser.getChrootPath(), hostProvider, sessionTimeout, 
				this, watchManager, getClientCnxnSocket(), canBeReadOnly);
	cnxn.start();
}

  首先当我们建立一个 Zookeeper 客户端时需要创建一个 Zookeeper 对象,且在这个 Zookeeper 对象创建的过程中会创建一个客户端连接管理器(ClientCnxn),接着在创建 ClientCnxn 的过程中又需要创建一个 ClientCnxnSocket 用于实现客户端间的通信,所以我们跟进这个 getClientCnxnSocket 方法。

// Zookeeper.java
private ClientCnxnSocket getClientCnxnSocket() throws IOException {
	// 从配置文件中获取 ClientCnxnSocket 配置信息
	String clientCnxnSocketName = getClientConfig().getProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
	// 如果配置文件中没有提供 ClientCnxnSocket 配置信息则默认使用 NIO
	if (clientCnxnSocketName == null) {
		clientCnxnSocketName = ClientCnxnSocketNIO.class.getName();
	}
	try {
		// 通过反射获取 ClientCnxnSocket 的构造方法
		Constructor<?> clientCxnConstructor = Class.forName(clientCnxnSocketName).getDeclaredConstructor(ZKClientConfig.class);
		// 通过以客户端配置为入参调用构造方法来创建一个 ClientCnxnSocket 实例
		ClientCnxnSocket clientCxnSocket = (ClientCnxnSocket) clientCxnConstructor.newInstance(getClientConfig());
		// 将创建完成的 ClientCnxnSocket 实例返回
		return clientCxnSocket;
	}
}

  在这个 getClientCnxnSocket 方法中会选择 ClientCnxnSocket 的实现版本,目前的 Zookeeper 中存在两个实现版本,一个是使用 Java JDK 中的 NIO 实现的 ClientCnxnSocketNIO ,另一个是使用 Netty 实现的 ClientCnxnSocketNetty ,而选择的方式优先根据配置文件中的配置进行选择,如果没有进行配置则默认选择 ClientCnxnSocketNIO 实现版本,之后再通过 反射 的方式创建其实例对象。

// ClientCnxnSocketNetty.java
ClientCnxnSocketNetty(ZKClientConfig clientConfig) throws IOException {
	this.clientConfig = clientConfig;
	
	// 创建一个 eventLoopGroup 用于后面对异步请求的处理
	// 且因为客户端只有一个 outgoing Socket 因此只需要一个 eventLoopGroup 即可
	eventLoopGroup = NettyUtils.newNioOrEpollEventLoopGroup(1 /* nThreads */);
	initProperties();
}

public static EventLoopGroup newNioOrEpollEventLoopGroup(int nThreads) {
	// 如果 Epoll 可用( Linux )则优先使用 EpollEventLoopGroup 否则使用 NioEventLoopGroup
	if (Epoll.isAvailable()) {
		return new EpollEventLoopGroup(nThreads);
	} else {
		return new NioEventLoopGroup(nThreads);
	}
}

  我们这里的分析以 Netty 实现为准,所以选择 ClientCnxnSocketNetty 实现版本,在 ClientCnxnSocketNetty 的构造方法中会选择具体的 EventLoopGroup 的实现,如果是在 Linux 优先选择使用性能更高的 EpollEventLoopGroup 实现,且这里配置的线程数目为一,因此这是典型的 单线程 Reactor 实现。

// Zookeeper.java
protected ClientCnxn createConnection(String chrootPath, HostProvider hostProvider, int sessionTimeout, ZooKeeper zooKeeper, ClientWatchManager watcher, ClientCnxnSocket clientCnxnSocket, boolean canBeReadOnly) throws IOException {
	// 将刚刚创建的 clientCnxnSocket 实例作为入参创建一个 ClientCnxn 对象
	return new ClientCnxn(chrootPath, hostProvider, sessionTimeout, this, watchManager, clientCnxnSocket, canBeReadOnly);
}

// ClientCnxn.java
public ClientCnxn(String chrootPath, HostProvider hostProvider, int sessionTimeout, ZooKeeper zooKeeper, ClientWatchManager watcher, ClientCnxnSocket clientCnxnSocket, long sessionId, byte[] sessionPasswd, boolean canBeReadOnly) {
	// 省略属性初始化...

	// 将刚刚创建的 clientCnxnSocket 实例作为入参创建一个 SendThread 实例
	// 这里的 SendThread 和 EventThread 均为 ClientCnxn 的内部类且本质均为一个线程
	sendThread = new SendThread(clientCnxnSocket);
	eventThread = new EventThread();
	this.clientConfig = zooKeeper.getClientConfig();
	initRequestTimeout();
}

  看完 getClientCnxnSocket 方法后我们再回头去看 Zookeeper 构造方法中的 createConnection 方法,可以看到该方法的实质就是创建了一个 ClientCnxn 对象,并在 ClientCnxn 的构造方法中创建了 SendThread 发送线程和 EventThread 事件处理线程。

// ClientCnxn.java
// 该方法在 Zookeeper 构造方法中被调用
public void start() {
	// 启动 sendThread 和 eventThread 既调用 SendThread 和 EventThread 线程的 run 方法
	sendThread.start();
    eventThread.start();
}

  当完成 SendThread 和 EventThread 这两个线程的创建和初始化后,在 Zookeeper 的构造方法中最后会通过 cnxn.start() 方法启动这两个线程。

// ClientCnxn.SendThread.java
public void run() {
	while (state.isAlive()) {
		try {
			// 当前尚未与服务端建立连接
			if (!clientCnxnSocket.isConnected()) {
         		// 如果正在关闭则不尝试重连
				if (closing) {
					break;
				}
				// 如果存在之前通过 pingRwServer 方法搜索到的可用服务器地址 rwServerAddress 则优先使用它尝试重连
				if (rwServerAddress != null) {
					serverAddress = rwServerAddress;
					rwServerAddress = null;
				} else {
					// 如果不存在 rwServerAddress 则直接更换服务器地址后尝试重连
					serverAddress = hostProvider.next(1000);
				}
				// 传入服务器地址建立连接
				startConnect(serverAddress);
				clientCnxnSocket.updateLastSendAndHeard();
			}
			// 省略已连接逻辑...
		}
	}
}

// ClientCnxn.SendThread.java
private void startConnect(InetSocketAddress addr) throws IOException {
    if(!isFirstConnect){
	    try {
	    	// 如果不是第一次连接则先让线程睡眠 1000ms 以内的随机时间,防止短时间内过快的不断重连
			Thread.sleep(r.nextInt(1000));
		} 
	}
	// 设置状态为 CONNECTING
	state = States.CONNECTING;
	// 调用 ClientCnxnSocket 的 connect 方法尝试连接
	clientCnxnSocket.connect(addr);
}

  在 SendThread 的 run 方法中会启动初始化连接的流程,并且最终会调用到 ClientCnxnSocketNetty 的 connect 方法来建立客户端网络通信的连接,而 connect 方法中的代码逻辑注释已经描述的比较清楚,所以不做赘述。

// ClientCnxnSocketNetty.java
void connect(InetSocketAddress addr) throws IOException {
	firstConnect = new CountDownLatch(1);

	// 初始化 Netty 逻辑
	Bootstrap bootstrap = new Bootstrap()
			.group(eventLoopGroup)	// 设置 eventLoopGroup
            .channel(NettyUtils.nioOrEpollSocketChannel()) // 选择合适的 SocketChannel
            .option(ChannelOption.SO_LINGER, -1) // 对应套接字选项SO_LINGER 
            .option(ChannelOption.TCP_NODELAY, true) // 对应套接字选项 TCP_NODELAY
            .handler(new ZKClientPipelineFactory(addr.getHostString(), addr.getPort())); // 设置处理器
	bootstrap = configureBootstrapAllocator(bootstrap);
    bootstrap.validate();

    connectLock.lock();
    try {
    	// Netty 异步调用
    	connectFuture = bootstrap.connect(addr);
    	// 监听并处理返回结果
        connectFuture.addListener(new ChannelFutureListener() {
        	@Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                boolean connected = false;
                connectLock.lock();
                try {
                	if (!channelFuture.isSuccess()) {
                		// 连接失败则直接返回
                    	return;
                    } else if (connectFuture == null) {
                        // 如果 connectFuture 为空则证明尝试连接被取消
                        // 但是因为可能已经连接成功了,所以应当确保 channel 被正常关闭
                        channelFuture.channel().close();
						return;
                    }
                    
                    // lenBuffer 仅用于读取传入消息的长度(该 Buffer 长度为 4 byte),具体描述见 RPC 网络数据结构
                    lenBuffer.clear();
                    incomingBuffer = lenBuffer;
                        
                    // 设置 Session、之前的观察者和身份验证
                    sendThread.primeConnection();
                    
                    connected = true;
                } finally {
                    connectFuture = null;
                    connectLock.unlock();
                    // 唤醒发送线程中的发送逻辑(向 outgoingQueue 中添加一个 WakeupPacket 空包)
                    wakeupCnxn();
                    // 避免 ClientCnxn 中的 SendThread 在 doTransport() 中等待第一次连接而被阻塞并最终超时
                    firstConnect.countDown();
                }
            }
        });
    } finally {
        connectLock.unlock();
    }
}

  最后,在 connect 方法中需要注意下面的几点:

  • 设置 Bootstrap 的处理器为 ZKClientPipelineFactory,准确来说是 ZKClientHandler(如下图);
  • 初始化设置 incomingBuffer = lenBuffer,保证第一次读入的是数据包中真实数据的长度(首部 4byte ByteBuffer);
  • 完成连接后需要通过 wakeupCnxn() 方法来唤醒发送线程的发送逻辑(实质是发送一个空的 WakeupPacket);
// ZKClientPipelineFactory.java
protected void initChannel(SocketChannel ch) throws Exception {
	ChannelPipeline pipeline = ch.pipeline();
    if (clientConfig.getBoolean(ZKClientConfig.SECURE_CLIENT)) {
    	initSSL(pipeline);
    }
    // 配置 Pipeline
    pipeline.addLast("handler", new ZKClientHandler());
}



3.2 SendThread 从 outgoingQueue 获取并发送 Packet

// ClientCnxn.SendThread.java
public void run() {
	while (state.isAlive()) {
    	try {
			if (!clientCnxnSocket.isConnected()) {
            	// 连接服务端逻辑..
			}
			// 省略部分代码..

			// 发送 outgoingQueue 中的数据包并将已发送的数据包转移到 PendingQueue 中
        	clientCnxnSocket.doTransport(to, pendingQueue, ClientCnxn.this);
        	
        	// 省略部分代码..
		} 
	}

  当完成了客户端连接后即可进入到请求发送的逻辑中,客户端发送请求的逻辑主要位于 ClientCnxn 的 SendThread 线程中,在初始化时我们已经启动了该线程,所以当连接建立完成后会通过调用 clientCnxnSocket ( ClientCnxnSocketNetty )的 doTransport 方法发送位于 outgoingQueue 中的 Packet 请求。

// ClientCnxnSocketNetty.java
void doTransport(int waitTimeOut, List<Packet> pendingQueue, ClientCnxn cnxn) throws IOException, InterruptedException {
	try {
		// 该线程方法会等待连接的建立且超时即返回
		if (!firstConnect.await(waitTimeOut, TimeUnit.MILLISECONDS)) {
			return;
		}
		Packet head = null;
		if (needSasl.get()) {
			if (!waitSasl.tryAcquire(waitTimeOut, TimeUnit.MILLISECONDS)) {
				return;
			}
		} else {
			// 从 outgoingQueue 队列中获取要发送的 Packet
			head = outgoingQueue.poll(waitTimeOut, TimeUnit.MILLISECONDS);
		}
		// 检查当前是否正处于关闭流程中
		if (!sendThread.getZkState().isAlive()) {
			// adding back the packet to notify of failure in conLossPacket().添加回数据包以通知conLossPacket()中的失败
			addBack(head);
			return;
		}
		// 当通道断开时
		if (disconnected.get()) {
			addBack(head);
			throw new EndOfStreamException("channel for sessionid 0x" + Long.toHexString(sessionId) + " is lost");
		}
		if (head != null) {
			// 调用 doWrite 方法执行实际的发送数据操作
			doWrite(pendingQueue, head, cnxn);
		}
	} finally {
		updateNow();
	}
}

  在 doTransport 方法中首先会 await 等待连接建立,并且在超时后会立即返回(因此在连接建立后需要第一时间唤醒该线程以避免其超时返回),之后会从 outgoingQueue 中取出待发送的 Packet ,并在进行一系列验证后通过 doWrite 方法来实际发送该 Packet 。

private void doWrite(List<Packet> pendingQueue, Packet p, ClientCnxn cnxn) {
	updateNow();
    boolean anyPacketsSent = false;
    while (true) {
    	// 跳过处理 WakeupPacket 数据包
    	if (p != WakeupPacket.getInstance()) {
        	if ((p.requestHeader != null) &&
            		(p.requestHeader.getType() != ZooDefs.OpCode.ping) &&
                	(p.requestHeader.getType() != ZooDefs.OpCode.auth)) {
				p.requestHeader.setXid(cnxn.getXid());
				
                synchronized (pendingQueue) {
                    // 将该 Packet 添加到 pendingQueue 队列中
                	pendingQueue.add(p);
                }
            }
            // 只发送数据包到通道,而不刷新通道
            sendPktOnly(p);
            // 记录当前迭代存在需要被发送的数据
            anyPacketsSent = true;
        }
        if (outgoingQueue.isEmpty()) {
            break;
        }
        // 将该 Packet 从 outgoingQueue 队列中出队
        p = outgoingQueue.remove();
    }

    if (anyPacketsSent) {
    	// 如果本次迭代存在需要被发送的数据,则调用 flush 刷新 Netty 通道
    	channel.flush();
    }
}

  在 doWrite 方法中会在验证该 Packet 非 WakeupPacket 后为其设置请求头中的 xid ,并将其添加到 pendingQueue 中,最后通过 sendPktOnly 方法将其发送到通道中(暂不刷新通道,方法代码如下),然后如果 outgoingQueue 中仍存在待发送的 Packet 则继续重复执行添加 pendingQueue 并发送的逻辑,当 outgoingQueue 中的 Packet 全部处理完成后调用 channel.flush() 刷新通道,将本轮数据一起发送。

// ClientCnxnSocketNetty.java
private ChannelFuture sendPktOnly(Packet p) {
	// 仅发送数据包到通道,而不调用 flush() 方法刷新通道
    return sendPkt(p, false);
}

// ClientCnxnSocketNetty.java
private ChannelFuture sendPkt(Packet p, boolean doFlush) {
	// 创建 ByteBuffer
    p.createBB();
    updateLastSend();
    // 将 ByteBuffer 转化为 Netty 的 ByteBuf
    final ByteBuf writeBuffer = Unpooled.wrappedBuffer(p.bb);
    final ChannelFuture result = doFlush
            ? channel.writeAndFlush(writeBuffer)
            : channel.write(writeBuffer);
    result.addListener(onSendPktDoneListener);
    return result;
}



3.3 同步版 RPC 调用流程(Create API)

3.3.1 创建 Packet 并入队 outgoing (且 Packet.Wait)
// Zookeeper.java
public String create(final String path, byte data[], List<ACL> acl, CreateMode createMode) throws KeeperException, InterruptedException {
	final String clientPath = path;
	// 相关信息验证
    PathUtils.validatePath(clientPath, createMode.isSequential());
    EphemeralType.validateTTL(createMode, -1);

    final String serverPath = prependChroot(clientPath);

	// 创建 Request 请求包和 Response 响应包
    RequestHeader h = new RequestHeader();
    h.setType(createMode.isContainer() ? ZooDefs.OpCode.createContainer : ZooDefs.OpCode.create);
    CreateRequest request = new CreateRequest();
    CreateResponse response = new CreateResponse();
    request.setData(data);
    request.setFlags(createMode.toFlag());
    request.setPath(serverPath);
    if (acl != null && acl.size() == 0) {
    	throw new KeeperException.InvalidACLException();
    }
    request.setAcl(acl);

	// 提交请求并接收返回头
    ReplyHeader r = cnxn.submitRequest(h, request, response, null);
    // 处理异常
    if (r.getErr() != 0) {
    	throw KeeperException.create(KeeperException.Code.get(r.getErr()), clientPath);
    }
    // 返回结果
    if (cnxn.chrootPath == null) {
        return response.getPath();
    } else {
        return response.getPath().substring(cnxn.chrootPath.length());
    }
}

  当我们在客户端使用 Create 命令创建节点时,实际会调用到 Zookeeper 的 Create 方法,而 Create 方法存在两个版本的实现,首先就是同步版本的实现,在同步版本中 Create 方法的逻辑可以概括为以下五步:

  1. 验证相关信息的有效性(包括验证客户端的路径以及创建模式的选择);
  2. 创建 Request 请求包和 Response 响应包,并为 Request 请求包填充数据;
  3. 通过调用 ClientCnxnsubmitRequest 方法提交请求并接收请求结果;
  4. 处理请求结果中的异常;
  5. 将请求结果处理后返回;
// ClientCnxn.java
public ReplyHeader submitRequest(RequestHeader h, Record request, Record response, WatchRegistration watchRegistration, WatchDeregistration watchDeregistration) throws InterruptedException {

	ReplyHeader r = new ReplyHeader();
	// 根据 Request 数据和 Response 数据打包创建 Packet
    Packet packet = queuePacket(h, r, request, response, null, null, null, null, watchRegistration, watchDeregistration);
    
    synchronized (packet) {
        if (requestTimeout > 0) {
            // 等待请求完成超时
            waitForPacketFinish(r, packet);
        } else {
            // 无限等待请求完成
            while (!packet.finished) {
                packet.wait();
            }
        }
    }
    if (r.getErr() == Code.REQUESTTIMEOUT.intValue()) {
        // 如果请求超时则清空 outgoingQueue 和 pendingQueue
        sendThread.cleanAndNotifyState();
    }
    return r;
}

  submitRequest 方法中的逻辑相对比较清晰,该方法首先会调用 queuePacket 方法创建 Packet 并将其入队 outgoing ,之后同步该 Packet,最后根据是否设置了超时时间来选择是否使用超时逻辑,如果设置了超时时间,当请求在超时时间内未完成即返回并清空相关队列(outgoingQueue 和 pendingQueue),而如果未设置超时时间则该线程无限期的 wait 在该 Packet 直至接收到该请求的响应(在接收到请求的响应后该线程会被 notify)。

// 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 {
                // 如果客户端要求关闭会话,则将其状态标记为正在关闭(Closing)
                if (h.getType() == OpCode.closeSession) {
                    closing = true;
                }
                // 将 Packet 添加到 outgoing 队列中等待发送
                outgoingQueue.add(packet);
            }
        }
        
        // 唤醒发送线程发送 outgoing 队列中的 Packet(Netty 中为空实现)
        sendThread.getClientCnxnSocket().packetAdded();
        return packet;
    }

  在 queuePacket 方法中会创建一个 Packet 实例,并将入参中的 request 请求包和 response 响应包等数据填充到该 Packet 中。但需要注意的是,到目前为止还没有为包生成 Xid ,它是在稍后发送时由 ClientCnxnSocket::doIO() 实现生成的,因为 Packet 实际上是在那里被发送的。然后会同步在状态值 state 上,这里之所以要同步在该状态上的原因有两点:

  1. 同步 SendThread.run() 中的 cleanup() 操作以避免竞争;
  2. 通过对每个包进行同步,如果一个 closeSession 包被添加,后面的包都会被通知;

  在同步后会首先判断当前连接的状态,如果当前连接已断开或者正在关闭则直接返回相应的错误信息,而当连接状态正常时,首先会判断该客户端请求的类型是否为 closeSession ,如果为该类型则意味着客户端需要关闭该连接,所以设置当前的状态为正在关闭(closing),然后将该 Packet 入队 outgoingQueue 等待发送,最后唤醒发送逻辑(在 Netty 实现版本中添加一个 Packet 将会唤醒一个网络连接,所以我们不需要向队列添加一个虚拟包来触发唤醒,因此 NettyClientCnxnSocket 中的 packetAdded 方法为空实现)。

  到这里为止( Packet 入队 outgoingQueue 且线程 wait )同步版本的 Create API 实现第一部分已经分析完成,下面我们来大概总结一下主要的流程:

  1. 首先在 Zookeeper 的 Create() 方法中根据入参创建 Request 请求包和 Response 响应包;
  2. 调用 ClientCnxn 的 submitRequest() 方法提交该请求,在 submitRequest() 方法中会调用 queuePacket() 方法;
  3. 在 queuePacket() 方法中将入参中的 Request 请求包和 Response 响应包以及其它信息封装为 Packet ,然后将其入队 outgoingQueue 并唤醒发送逻辑;
  4. 返回到 ClientCnxn 的 submitRequest() 方法中,根据是否设置超时时间选择不同的逻辑来将该线程 wait ;


3.3.2 处理响应并出队 pendingQueue(且 Packet.notifyAll)
// ClientCnxnSocketNetty.ZKClientHandler.java
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
	updateNow();
    while (buf.isReadable()) {
    
    	if (incomingBuffer.remaining() > buf.readableBytes()) {
    		// 如果 incomingBuffer 中剩余的空间大于 ByteBuf 中可读数据长度
    		// 重置 incomingBuffer 中的 limit 为 incomingBuffer 当前的 position 加上 ByteBuf 中可读的数据长度
        	int newLimit = incomingBuffer.position() + buf.readableBytes();
			incomingBuffer.limit(newLimit);
        }

		// 将 ByteBuf 中的数据读入到 incomingBuffer( ByteBuffer)中
		buf.readBytes(incomingBuffer);
        incomingBuffer.limit(incomingBuffer.capacity());

        if (!incomingBuffer.hasRemaining()) {
        	incomingBuffer.flip();
        	if (incomingBuffer == lenBuffer) {
        		recvCount.getAndIncrement();
        		// 当 incomingBuffer 等于 lenBuffer 时首先读取数据包中数据的长度 4byte
            	readLength();
        	} else if (!initialized) {
        		// 如果未进行初始化则首先读取连接结果
        		readConnectResult();
        		// 重置 lenBuffer 并重新初始化 incomingBuffer 为 lenBuffer 来读取数据包中真正数据的长度
            	lenBuffer.clear();
            	incomingBuffer = lenBuffer;
            	initialized = true;
            	updateLastHeard();
        	} else {
        		// 读取数据包中真正的数据
        		sendThread.readResponse(incomingBuffer);
        		// 重置 lenBuffer 并重新初始化 incomingBuffer 为 lenBuffer 来读取数据包中真正数据的长度
            	lenBuffer.clear();
            	incomingBuffer = lenBuffer;
            	updateLastHeard();
        	}
    	}
	}
	
	// 唤醒发送逻辑
    wakeupCnxn();
}

// ClientCnxnSocket.java
void readLength() throws IOException {
	// 读取数据包中数据的长度 len
	int len = incomingBuffer.getInt();
    if (len < 0 || len >= packetLen) {
    	throw new IOException("Packet len" + len + " is out of range!");
    }
    // 重新申请长度为 len 的 ByteBuffer 来读取真正的数据
    incomingBuffer = ByteBuffer.allocate(len);
}

  因为我们整体的分析以 Netty 实现为准,因此当接收到响应后会调用我们在创建 Netty Bootstrap 时所设置的 ZKClientHandler 中的 channelRead0 方法来处理数据。因为在 channelRead0 方法中是将 Netty 的 ByteBuf 转换为了 NIO 的 ByteBuffer 来进行处理,所以我们先大概明确几个比较重要的 NIO ByteBuffer 方法的作用:

  1. ByteBuffer.remaining() :limit - position;
  2. ByteBuffer.hasRemaining() :position < limit;
  3. ByteBuffer.clear() :position = 0; limit = capacity; mark = -1;
  4. ByteBuffer.flip() :position = 0; limit = position; mark = -1;

  首先在初始化连接的过程中(connect 方法中)我们就已将 incomingBuffer 设置为 lenBuffer(4 byte ByteBuffer),因此当数据到达时 incomingBuffer 会先将 ByteBuf 中的前 4byte 数据读入,然后调用 readLength 方法来获取这 4byte 所代表的 int 数值(真实数据的长度),之后再创建一个新的长度为真实数据长度的 ByteBuffer 赋值给 incomingBuffer,这样在下一轮的读取过程中 incomingBuffer 就可以从 ByteBuf 中一次性完整的读出所有的真实数据,最后调用 readResponse 方法来处理读取到的真实数据。

// ClientCnxn.SendThread.java
void readResponse(ByteBuffer incomingBuffer) throws IOException {
	ByteBufferInputStream bbis = new ByteBufferInputStream(incomingBuffer);
	BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
	//创建临时响应头
	ReplyHeader replyHdr = new ReplyHeader();

	// 解析数据包中的数据填充临时响应头,然后根据临时响应头中的 xid 进行分类处理
	replyHdr.deserialize(bbia, "header");
	if (replyHdr.getXid() == -2) {
		// xid == -2 为心跳包
		// 心跳包不做处理直接返回
		return;
	}
	if (replyHdr.getXid() == -4) {
    	// xid == -4 为认证包               
        // 省略处理错误逻辑...
        return;
	}
    if (replyHdr.getXid() == -1) {
    	// xid == -1 为通知包
        // 省略通知处理逻辑...
        return;
    }

	Packet packet;
    synchronized (pendingQueue) {
    	// 如果 pendingQueue 为空则直接抛异常,否则出队一个 Packet
    	if (pendingQueue.size() == 0) {
        	throw new IOException("Nothing in the queue, but got " + replyHdr.getXid());
        }
        packet = pendingQueue.remove();
    }

	// 由于请求是按顺序处理的,所以获得对第一个请求的响应
	try {
		// 如果 pendingQueue 刚刚出队的 Packet 不是当前响应所对应的 Packet 则证明出现异常
		if (packet.requestHeader.getXid() != replyHdr.getXid()) {
			packet.replyHeader.setErr( KeeperException.Code.CONNECTIONLOSS.intValue());
		}

		// 填充 Packet 响应头数据
        packet.replyHeader.setXid(replyHdr.getXid());
        packet.replyHeader.setErr(replyHdr.getErr());
        packet.replyHeader.setZxid(replyHdr.getZxid());
        // 更新最后处理的 zxid
		if (replyHdr.getZxid() > 0) {
			lastZxid = replyHdr.getZxid();
		}
		// 如果 Packet 中存在 Response 响应包(数据为空)且响应头解析未出现错误则继续解析响应体数据
		if (packet.response != null && replyHdr.getErr() == 0) {
			packet.response.deserialize(bbia, "response");
		}
    } finally {
    	// 完整响应数据解析后调用
    	finishPacket(packet);
	}
}

  readResponse 方法首先从刚刚读入数据的 ByteBuffer 中解析出一个临时响应头,然后根据这个临时响应头中的 xid 来进行分类处理, 当处理完成后会从 pendingQueue 中出队一个 Packet,这个 Packet 正常来说应当是我们之前发送最后一个请求后入队的那个 Packet (请求顺序性),因此判断这个出队的 Packet 的 xid 是否等于当前正在处理的这个请求中的 Packet 的 xid ,如果不是则证明出现了丢包或断连等问题,所以向临时响应头中添加一个错误信息,然后将临时响应头中的数据填充到刚刚出队的那个 Packet 的 ReplyHeader 响应头中并更新最后处理的 zxid 属性值( lastZxid ),最终如果确认该 Packet 中存在 Response(需要返回响应信息)并且在解析响应头的过程中未发现错误,则开始从 ByteBuffer 中解析出响应体并赋给 Packet 的 Response 属性,当全部处理完成时,最终调用 finishPacket 方法完成 ByteBuffer 的 Response 解析。

// ClientCnxn.java
    protected void finishPacket(Packet p) {
        // 省略 watch 事件处理逻辑...
        if (p.cb == null) {
        	// 如果 Packet 中不存在方法回调(同步 API)
            synchronized (p) {
            	// 设置 Packet 处理完成
                p.finished = true;
                // 唤醒所有 wait 在该 Packet 上的线程
                p.notifyAll();
            }
        } else {
        	// 如果 Packet 中不存在方法回调(异步 API),先设置 Packet 处理完成
            p.finished = true;
            // 进入异步 Packet 的处理逻辑
            eventThread.queuePacket(p);
        }
    }

  finishPacket 方法在响应处理完成后就会被调用,在这个方法中首先会对 Watch 事件进行处理,然后判断当前 Packet 中是否存在回调方法(本次调用是同步还是异步),如果不存在回调方法则证明本次调用为同步调用,因此更新 Packet 的 finished 状态后通过 Packet 的 notifyAll 方法唤醒所有 wait 在该 Packet 上的线程(wait 逻辑位于 ClientCnxn 的 submitRequest 方法),而如果存在回调方法,则应通过调用 EventThread 的 queuePacket 方法进入对于异步回调的处理逻辑中。



3.4 异步版 RPC 调用流程(Create API)

3.4.1 创建 Packet 并入队 outgoing
public void create(final String path, byte data[], List<ACL> acl, CreateMode createMode, StringCallback cb, Object ctx){

        final String clientPath = path;
        // 相关信息验证(与同步版相同)
        PathUtils.validatePath(clientPath, createMode.isSequential());
        EphemeralType.validateTTL(createMode, -1);

        final String serverPath = prependChroot(clientPath);

		// 创建 Request 请求包和 Response 响应包(与同步版相同)
        RequestHeader h = new RequestHeader();
        h.setType(createMode.isContainer() ? ZooDefs.OpCode.createContainer : ZooDefs.OpCode.create);
        CreateRequest request = new CreateRequest();
        CreateResponse response = new CreateResponse();
        ReplyHeader r = new ReplyHeader();
        request.setData(data);
        request.setFlags(createMode.toFlag());
        request.setPath(serverPath);
        request.setAcl(acl);

		// 直接调用 queuePacket 方法创建 Packet 并入队 outgoingQueue
        cnxn.queuePacket(h, r, request, response, cb, clientPath, serverPath, ctx, null);
    }

  其实异步版的 API 和同步版 API 是大体相同的,只不过在异步版本的 API 中仅需要调用 queuePacket 方法创建 Packet 并入队 outgoingQueue ,然后直接返回即可,而不需要再在 submitRequest 方法中 wait 等待请求响应的返回。



3.4.2 入队 waitingEvents 并在出队时进行方法回调

  在接收并处理 Response 的过程中同步版和异步版的 API 前面的处理逻辑都是完全相同的,差异之处在于在 finishPacket 方法中同步版调用会直接唤醒所有 wait 在该 Packet 上面的线程然后返回,而对于异步版本则会调用到 queuePacket 方法来对响应做进一步的处理。

// ClientCnxn.EventThread.java
public void queuePacket(Packet packet) {
	if (wasKilled) {
		// EventThread 在接收到 eventOfDeath 后 wasKilled 将被设为 true
        synchronized (waitingEvents) {
        	// 如果 EventThread 仍在运行(isRunning == true)则将 Packet 入队 waitingEvents
            if (isRunning) waitingEvents.add(packet);
            // 否则直接调用 processEvent 方法处理该 Packet
        	else processEvent(packet);
    	}
	} else {
		// 如果 EventThread 线程正常运行则直接将 Packet 入队 waitingEvents
    	waitingEvents.add(packet);
	}
}

  queuePacket 方法主要执行的就是将 Packet 入队 waitingEvents 的逻辑,但是需要注意的是对于 EventThread 存在两个标志量(下文会细讲),且当 wasKilled 为 true 时并不是意味着 EventThread 已经完全不能处理 Packet 了,还需要再次判断 isRunning 来确定当前 EventThread 是否真的已经停止运行了,如果当前 wasKilled 为 true 且 isRunning 为 false 则证明 EventThread 已经真正的结束了,所以该方法会自己调用 processEvent 方法来处理该 Packet 。
  

// ClientCnxn.EventThread.java
public void run() {
	try {
    	isRunning = true;
        while (true) {
        // 从 waitingEvents 队列中获取事件
        Object event = waitingEvents.take();
        if (event == eventOfDeath) {
        	// 如果事件类型为 eventOfDeath 则修改 wasKilled 标志值
        	wasKilled = true;
        } else {
        	// 否则通过调用 processEvent 处理该事件
        	processEvent(event);
        }
        if (wasKilled)
        synchronized (waitingEvents) {
            // 如果当前状态为 wasKilled == true 则判断当前 waitingEvents 是否已空
        	if (waitingEvents.isEmpty()) {
        		// 如果 waitingEvents 已空则设置 isRunning 为 false 来终止当前线程的运行
            	isRunning = false;
                break;
            }
        }
    }
}

  当我们在 queuePacket 方法中将 Packet 入队 waitingEvents 后,在 ClientCnxn 的内部类(线程)EventThread 中会通过 run 方法不断取出队列中的 Packet ,然后调用 processEvent 方法进行处理。

  这里设计很巧妙的就是对于关闭该线程时的操作,在该线程中使用了两个标志量 wasKilledisRunning ,当外部将要关闭它时会通过发送类型为 eventOfDeath 的 Packet 先设置 wasKilled 为 true,此时进入关闭的第一阶段。EventThread 得到该关闭消息后开始进行扫尾工作,在每次处理完一个 Packet 后就会判断 waitingEvents 中是否还存在未处理的 Packet ,如果存在就继续处理,如果不存在就将 isRunning 设置为 false 并跳出循环,此时标志着 EventThread 已经完成了扫尾工作,可以正常关闭了。因此,EventThread 可以安全的进入到最后一个线程的关闭阶段。

  这样的三阶段关闭流程保证了数据的安全性,保证了 EventThread 不会在 waitingEvents 还存在数据时就关闭而导致数据丢失,同时也正是因为这样,当通过 queuePacket 方法向 waitingEvents 中添加元素时,就算 wasKilled 已经为 true 了,但只要 isRunning 还为 true 就证明 waitingEvents 中还存在数据既 EventThread 还可以处理数据,所以仍然可以放心的将 Packet 入队 waitingEvents 来交给 EventThread 处理,且不会发生数据丢失的情况。

  

// ClientCnxn.EventThread.java
private void processEvent(Object event) {
	try {
		// 重构后的代码,省略巨多各种事件类型的判断和处理逻辑...
		// 当进行异步 Create 时,事件类型为 CreateResponse 
    	if (p.response instanceof CreateResponse) {
    		// 获取 Packet 中的回调信息
        	StringCallback cb = (StringCallback) p.cb;
        	// 获取 Packet 中的响应体
            CreateResponse rsp = (CreateResponse) p.response;
            if (rc == 0) {
            	cb.processResult(rc, clientPath, p.ctx,
                	(chrootPath == null ? rsp.getPath() : rsp.getPath().substring(chrootPath.length())));
			} else {
				// 进行方法回调
				cb.processResult(rc, clientPath, p.ctx, null);
			}
		}
	}
}

  最后,在 processEvent 方法中会根据传入 Packet 的类型来选择不同的处理逻辑对 Packet 进行处理,因为我们这里分析的是 Create API ,而其对应的响应类型为 CreateResponse ,所以会进入到如上图代码的逻辑中,具体的处理方式也就是获取到 Packet 中所保存的回调方法,然后对其进行回调即可,至此也就完成了整个异步版本的方法调用。

四、同步版 API 和异步版 API 总结与比较

4.1 版本总结

  首先经过上面的源码分析我们先总结一下几个比较重要的数据结构和属性:

  • outgoingQueue :保存待发送的 Packet 的队列;
  • pendingQueue :保存已发送但还未接收到响应的 Packet 的队列;
  • waitingEvents :保存已接收到响应待回调的 Packet 的队列;
  • EventThread.wasKilled :外部发送信号终止 EventThread ,但此时可能尚未真正停止;
  • EventThread.isRunning :标志着 EventThread 尚在运行(waitingEvents 中还存在未处理的 Packet),当该属性为 false 时证明线程进入终止状态;

  下面总结 同步版 API 主流程:

  1. 创建 Packet 并入队 outgoingQueue ,然后线程 wait 在该 Packet 进行等待;
  2. SendThread 从 outgoingQueue 中取出 Packet 后进行发送,并入队 pendingQueue ;
  3. 接收响应后从 pendingQueue 中出队 Packet ,然后将响应数据解析到 Packet中;
  4. 解析完成后调用 Packet.notifyAll 方法唤醒所有阻塞在该 Packet 上的线程;

  下面总结 异步版 API 主流程:

  1. 创建 Packet 并入队 outgoingQueue ,然后方法直接返回;
  2. SendThread 从 outgoingQueue 中取出 Packet 后进行发送,并入队 pendingQueue ;
  3. 接收响应后从 pendingQueue 中出队 Packet ,然后将响应数据解析到 Packet中;
  4. 解析完成后将该 Packet 入队 waitingEvents ,然后 EventThread 会从 waitingEvents 中取出 Packet 并调用其回调方法;


4.2 版本比较

  最后总结一下,其实异步版本的 API 与同步版的 API 大体上都是一致的,差别之处在于同步版本中在将 Packet 入队 outgoingQueue 后会 wait 等待,而异步版本则会在入队后直接返回。其次在接收到请求的响应后,同步版本会唤醒所有 wait 在该 Packet 上的线程然后返回,而异步版本则会继续将 Packet 入队 waitingEvents ,然后在 EventThread 中对其进行出队后再对其 Packet 中的回调方法进行调用。



五、内容总结

  这篇博文从 Zookeeper 源码的角度分析了客户端中网络连接建立的流程,并且分析了同步版和异步版的 Create API 的具体代码实现,并通过对两种实现的方式的梳理和总结得到了 Zookeeper 客户端网络通信的普适逻辑。