ZooKeeper客户端源码解读(网络I/O)

本文深入剖析ZooKeeper客户端的核心组件ClientCnxn,揭示其内部工作原理,包括Packet的数据封装、序列化过程、请求与响应处理流程,以及SendThread和EventThread的职责。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

org.apache.zookeeper.ClientCnxn是ZooKeeper客户端的核心工作类,负责维护客户端与服务端之间的网络连接并进行一系列网络通信。此处分析一下内部的工作原理。
首先上图:
在这里插入图片描述

Packet

Packet是ClientCnxn内部定义的一个对协议层的封装,作为ZooKeeper中请求与响应的载体。在这里插入图片描述
从上图可以看出,Packet中包含了请求头、响应头、请求体、响应体、节点路径和注册的Watcher等信息。

/**
 * This class allows us to pass the headers and the relevant records around.
 * 对协议层封装的内部类 作为ZooKeeper中请求与响应的载体
 */
static class Packet {
    /*
     * 请求头
     */
    RequestHeader requestHeader;
    /*
     * 响应头
     */
    ReplyHeader replyHeader;
    // 请求体
    Record request;
    // 响应体
    Record response;

    ByteBuffer bb;

    /** Client's view of the path (may differ due to chroot) **/
    String clientPath;
    /** Servers's view of the path (may differ due to chroot) **/
    String serverPath;

    boolean finished;

    AsyncCallback cb;

    Object ctx;
    // 注册的Watcher
    WatchRegistration watchRegistration;

    public boolean readOnly;

    WatchDeregistration watchDeregistration;
}

那么在客户端和服务端之间进行网络通讯时是否需要传送这个多属性呢?答案是否定的。Packet的createBB()方法负责对Packet对象进行序列化,最终生成可用于底层网络传输的ByteBuffer对象,在这个过程中,只会将requestHeader、request和readOnly三个属性进行序列化,其余属性都保存在客户端的上下文中,不会进行与服务端之间的网络传输。

public void createBB() {
    try {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
        boa.writeInt(-1, "len"); // We'll fill this in later
        if (requestHeader != null) {
            requestHeader.serialize(boa, "header");
        }
        if (request instanceof ConnectRequest) {
            request.serialize(boa, "connect");
            // append "am-I-allowed-to-be-readonly" flag
            boa.writeBool(readOnly, "readOnly");
        } else if (request != null) {
            request.serialize(boa, "request");
        }
        baos.close();
        this.bb = ByteBuffer.wrap(baos.toByteArray());
        this.bb.putInt(this.bb.capacity() - 4);
        this.bb.rewind();
    } catch (IOException e) {
        LOG.warn("Ignoring unexpected exception", e);
    }
}
outgoingQueue和pendingQueue
/**
 * These are the packets that have been sent and are waiting for a response.
 */
private final LinkedList<Packet> pendingQueue = new LinkedList<Packet>();

/**
 * These are the packets that need to be sent.
 */
private final LinkedBlockingDeque<Packet> outgoingQueue = new LinkedBlockingDeque<Packet>();

ClientCnxn中,有两个比较核心的队列outgoingQueue和pendingQueue,分别代表客户端的请求发送队列和服务端响应的等待队列。outgoingQueue队列是一个请求发送队列,专门用于存储那些需要发送到服务端的Packet集合。pendingQueue队列是为了存储那些已经从客户端发送到服务端的,但是需要等待服务端响应的Packet集合。

ClientCnxnSocket 底层Socket通信层

org.apache.zookeeper.ClientCnxnSocket定义了底层Socket通信的接口,默认使用的是org.apache.zookeeper.ClientCnxnSocketNIO,但是也支持其他的方式,比如Netty。org.apache.zookeeper.ClientCnxnSocketNetty。

private ClientCnxnSocket getClientCnxnSocket() throws IOException {
	String clientCnxnSocketName = getClientConfig()
			.getProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
	if (clientCnxnSocketName == null) {
		// 未设置要使用的客户端连接器 则使用默认的NIO模式 可以选择的还有Netty
		clientCnxnSocketName = ClientCnxnSocketNIO.class.getName();
	}
	try {
		Constructor<?> clientCxnConstructor = Class.forName(clientCnxnSocketName)
				.getDeclaredConstructor(ZKClientConfig.class);
		ClientCnxnSocket clientCxnSocket = (ClientCnxnSocket) clientCxnConstructor
				.newInstance(getClientConfig());
		return clientCxnSocket;
	} catch (Exception e) {
		IOException ioe = new IOException("Couldn't instantiate " + clientCnxnSocketName);
		ioe.initCause(e);
		throw ioe;
	}
}
请求发送

在正常情况下(即客户端与服务端之间的TCP连接正常且会话有效的情况下),会从outgoingQueue队列中提取一个可发送的Packet对象,同时生成一个客户端请求序号XID,并将其设置到Packet请求头中去,然后将其进行序列化后进行发送。这里提到的获取一个可发送的Packet对象指的哪些Packet呢?在outgoingQueue队列中的Packet整体上是按照先进先出的顺序被处理的,但是如果检测到客户端与服务端之间正在进行SASL权限的话,那么那些不含请求头(requestHeader)的Packet(例如会话创建请求)是可以被发送的,其余的都无法发送。
请求发送完毕后,会立即将该Packet保存到pendingQueue队列中,以便等待服务端响应返回后进行相应的处理。

public ReplyHeader submitRequest(RequestHeader h, Record request, Record response,
                                 WatchRegistration watchRegistration, WatchDeregistration watchDeregistration)
        throws InterruptedException {
    ReplyHeader r = new ReplyHeader();
    Packet packet = queuePacket(h, r, request, response, null, null, null, null, watchRegistration,
            watchDeregistration);
    synchronized (packet) {
        if (requestTimeout > 0) {
            // Wait for request completion with timeout
            waitForPacketFinish(r, packet);
        } else {
            // Wait for request completion infinitely
            while (!packet.finished) {
            	// 进行等待
                packet.wait();
            }
        }
    }
    if (r.getErr() == Code.REQUESTTIMEOUT.intValue()) {
        sendThread.cleanAndNotifyState();
    }
    return r;
}
响应接收

客户端获取到来自服务端的完整响应数据后,根据不同的客户端请求类型,会进行不同的处理

  • 如果检测到当前客户端还未进行初始化,那么说明当前客户端与服务端之间正在进行会话创建,那么就直接将接收到的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客户端的实际运行过程中,一方面,SendThead维护了客户端和服务端之间的会话生命周期,其通过在一定的周期频率内向服务器发送一个PING包来实现心跳检测,同时,在会话周期内,如果客户端和服务端之间出现TCP连接断开的情况,那么就会自动而且透明化完成重连操作。
另一方面,SendThread管理了客户端所有的请求发送和响应接收操作,其将上层客户端API操作转换成相应的请求协议并发送到服务端,并完成对同步调用的返回和异步调用的回调。同时,SendThread还负责将来自服务端的事件传递给EventThread去处理。

// @VisibleForTesting
protected void finishPacket(Packet p) {
    int err = p.replyHeader.getErr();
    if (p.watchRegistration != null) {
        p.watchRegistration.register(err);
    }
    // Add all the removed watch events to the event queue, so that the
    // clients will be notified with 'Data/Child WatchRemoved' event type.
    if (p.watchDeregistration != null) {
        Map<EventType, Set<Watcher>> materializedWatchers = null;
        try {
            materializedWatchers = p.watchDeregistration.unregister(err);
            for (Entry<EventType, Set<Watcher>> entry : materializedWatchers.entrySet()) {
                Set<Watcher> watchers = entry.getValue();
                if (watchers.size() > 0) {
                    queueEvent(p.watchDeregistration.getClientPath(), err, watchers, entry.getKey());
                    // ignore connectionloss when removing from local
                    // session
                    p.replyHeader.setErr(Code.OK.intValue());
                }
            }
        } catch (KeeperException.NoWatcherException nwe) {
            p.replyHeader.setErr(nwe.code().intValue());
        } catch (KeeperException ke) {
            p.replyHeader.setErr(ke.code().intValue());
        }
    }

    if (p.cb == null) {
    	// 同步唤醒Packet阻塞的线程
        synchronized (p) {
            p.finished = true;
            p.notifyAll();
        }
    } else {
    	// 异步 将packet加入到waitingEvents中
        p.finished = true;
        eventThread.queuePacket(p);
    }
}
EventThread

EventThread是客户端ClientCnxn内部的另一个核心线程,负责客户端的时间处理,并触发客户端注册的Watcher监听。EventThread中有一个waitingEvents队列,用于临时存放那么需要被触发的Object,包括那些客户点注册的Watcher和异步接口中注册的回到器AsyncCallBack。同时,EventThread会不断地从waitingEvents这个队列中取出Object,识别出其具体类型(Watcher或者AsynCallBack),并分别调用process和processResult接口方法来实现对事件的触发和回调

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值