ZooKeeper源码分析之NettyServerCnxn

2021SC@SDUSC

本章将讲述基于Netty实现ServerCnxn服务器连接。使用Netty框架来高效处理服务器与客户端之间的通信。

源码分析

(1)属性,与NIO类似。NettyServerCnxn维护了服务器与客户端之间的通道缓冲、缓冲区以及会话等属性。

    //获取基于Netty框架服务连接的日志
    private static final Logger LOG = LoggerFactory.getLogger(NettyServerCnxn.class);
    //连接通道,客户端与服务器连接通道
    private final Channel channel;
    //通道缓存
    private CompositeByteBuf queuedBuffer;
    private final AtomicBoolean throttled = new AtomicBoolean(false);
    //字节缓冲区
    private ByteBuffer bb;
    //设置缓冲区为4个字节
    private final ByteBuffer bbLen = ByteBuffer.allocate(4);
    //客户端与服务器之间会话的ID
    private long sessionId;
    //客户端与服务器之间会话超时时间,当会话超时时,会自动释放资源结束会话
    private int sessionTimeout;
    private Certificate[] clientChain;
    //关闭连接通道
    private volatile boolean closingChannel;
    //NettyServerCnxn工厂
    private final NettyServerCnxnFactory factory;
    //初始化标志
    private boolean initialized;
    
    public int readIssuedAfterReadComplete;
    //握手状态,即连接状态默认为未连接
    private volatile HandshakeState handshakeState = HandshakeState.NONE;
    //连接状态分为未连接、开始连接、完成连接
    public enum HandshakeState {
        NONE,
        STARTED,
        FINISHED
    }
    

(2)构造函数,与NIOServerCnxn区别在于Netty的构造函数需要用户进行登录验证。其余部位对NIOServerCnxn的部分重要属性进行赋值。

    //参数需求存在连接通道,服务器以及已经初始化的NettyServerCnxn工厂
    NettyServerCnxn(Channel channel, ZooKeeperServer zks, NettyServerCnxnFactory factory) {
        super(zks);
        this.channel = channel;
        this.closingChannel = false;
        this.factory = factory;
        //用户登录
        if (this.factory.login != null) {
            this.zooKeeperSaslServer = new ZooKeeperSaslServer(factory.login);
        }
        //获取IP地址
        InetAddress addr = ((InetSocketAddress) channel.remoteAddress()).getAddress();
        //认证信息中添加IP地址
        addAuthInfo(new Id("ip", addr.getHostAddress()));
    }
    

(3)核心函数(一)——close(),关闭服务器与客户端之间的连接通道。

    //关闭连接通道
    public void close() {
        //关闭连接通道标志为true
        closingChannel = true;
        //关闭会话的ID
        LOG.debug("close called for session id: 0x{}", Long.toHexString(sessionId));

        setStale();

        // 关闭时始终取消注册连接,以防止在某些竞争条件下连接bean泄漏。
        factory.unregisterConnection(this);

        // 如果这连接通道不在cnxns中,那么它已经关闭了
        if (!factory.cnxns.remove(this)) {
            LOG.debug("cnxns size:{}", factory.cnxns.size());
            if (channel.isOpen()) {
                channel.close();
            }
            return;
        }

        LOG.debug("close in progress for session id: 0x{}", Long.toHexString(sessionId));

        factory.removeCnxnFromSessionMap(this);

        factory.removeCnxnFromIpMap(this, ((InetSocketAddress) channel.remoteAddress()).getAddress());

        if (zkServer != null) {
            zkServer.removeCnxn(this);
        }
        //如果连接通道没有关闭
        if (channel.isOpen()) {
            /**由于不检查通过对channel complete的写调用创建的未来,因此需要确保
             * 在关闭通道之前,所有写入都已完成,否则我们将面临数据丢失的风险
             */
            channel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) {
                    future.channel().close().addListener(f -> releaseQueuedBuffer());
                }
            });
        } else {
            ServerMetrics.getMetrics().CONNECTION_DROP_COUNT.add(1);
            channel.eventLoop().execute(this::releaseQueuedBuffer);
        }
    }

核心函数(二)——process(),处理被NettyServerCnxn监听的事件。首先创建ReplyHeader,然后再调用sendResponse来发送响应,最后调用close函数进行后续关闭处理。

    public void process(WatchedEvent event) {
        // 创建响应头
        ReplyHeader h = new ReplyHeader(ClientCnxn.NOTIFICATION_XID, -1L, 0);
        if (LOG.isTraceEnabled()) {
            ZooTrace.logTraceMessage(
                LOG,
                ZooTrace.EVENT_DELIVERY_TRACE_MASK,
                "Deliver event " + event + " to 0x" + Long.toHexString(this.sessionId) + " through " + this);
        }

        //将WatchedEvent转换为可以通过线路发送的类型
        WatcherEvent e = event.getWrapper();

        try {
            // 发送响应
            sendResponse(h, e, "notification");
        } catch (IOException e1) {
            LOG.debug("Problem sending to {}", getRemoteSocketAddress(), e1);
            close();
        }
    }

核心函数(三)——sendResponse(),给客户端发送响应。

    public void sendResponse(ReplyHeader h, Record r, String tag,
                             String cacheKey, Stat stat, int opCode) throws IOException {
        // cacheKey和stat用于缓存,而不是在这里实现。实现示例可以在NIOServerCnxn中找到。
        if (closingChannel || !channel.isOpen()) {
            return;
        }
        sendBuffer(serialize(h, r, tag, cacheKey, stat, opCode));
        decrOutstandingAndCheckThrottle(h);
    }

核心函数(四)——receiveMessage()。该函数用于接收ChannelBuffer中的数据,函数在while循环体中,当writerIndex大于readerIndex(表示ChannelBuffer中还有可读内容)且throttled为false时执行while循环体,该函数大致可以分为两部分:
首先是当bb不为空时,表示已经准备好读取ChannelBuffer中的内容。其中主要的部分是判断bb的剩余空间是否大于message中的内容,简单而言,就是判断bb是否还有足够空间存储message内容,然后设置bb的limit,之后将message内容读入bb缓冲中,之后再次确定时候已经读完message内容,统计接收信息,再根据是否已经初始化来处理包或者是连接请求,其中的请求内容都存储在bb中。
当bb为空时,表示还没有给bb分配足够的内存空间来读取message,首先还是将message内容(后续内容的长度)读入bbLen中,然后再确定读入的内容代表后续真正内容的长度len,然后再根据len来为bb分配存储空间,方便后续读取真正的内容。

    private void receiveMessage(ByteBuf message) {
        //检查消息是否在实践队列中
        checkIsInEventLoop("receiveMessage");
        try {
            // 当writerIndex > readerIndex,并且不节流时,满足条件
            while (message.isReadable() && !throttled.get()) {
                // bb不为空
                if (bb != null) {
                    if (LOG.isTraceEnabled()) {
                        LOG.trace("message readable {} bb len {} {}", message.readableBytes(), bb.remaining(), bb);
                        ByteBuffer dat = bb.duplicate();
                        dat.flip();
                        LOG.trace("0x{} bb {}", Long.toHexString(sessionId), ByteBufUtil.hexDump(Unpooled.wrappedBuffer(dat)));
                    }
                    // bb剩余空间大于message中可读字节大小
                    if (bb.remaining() > message.readableBytes()) {
                        // 确定新的limit
                        int newLimit = bb.position() + message.readableBytes();
                        bb.limit(newLimit);
                    }
                    // 将message写入bb中
                    message.readBytes(bb);
                    // 重置bb的limit
                    bb.limit(bb.capacity());

                    if (LOG.isTraceEnabled()) {
                        LOG.trace("after readBytes message readable {} bb len {} {}", message.readableBytes(), bb.remaining(), bb);
                        ByteBuffer dat = bb.duplicate();
                        dat.flip();
                        LOG.trace("after readbytes 0x{} bb {}",
                                  Long.toHexString(sessionId),
                                  ByteBufUtil.hexDump(Unpooled.wrappedBuffer(dat)));
                    }
                    // 已经读完message,表示内容已经全部接收
                    if (bb.remaining() == 0) {
                        // 翻转,可读
                        bb.flip();
                        // 统计接收信息
                        packetReceived(4 + bb.remaining());

                        ZooKeeperServer zks = this.zkServer;
                        // Zookeeper服务器为空
                        if (zks == null || !zks.isRunning()) {
                            throw new IOException("ZK down");
                        }

                        if (initialized) {// 未被初始化
                            // TODO: if zks.processPacket() is changed to take a ByteBuffer[],
                            // we could implement zero-copy queueing.
                            // 处理bb中包含的包信息
                            zks.processPacket(this, bb);
                        } else {// 已经初始化
                            LOG.debug("got conn req request from {}", getRemoteSocketAddress());
                            // 处理连接请求
                            zks.processConnectRequest(this, bb);
                            initialized = true;
                        }
                        bb = null;
                    }
                } else {// bb为null
                    if (LOG.isTraceEnabled()) {
                        LOG.trace("message readable {} bblenrem {}", message.readableBytes(), bbLen.remaining());
                        // 复制bbLen缓冲
                        ByteBuffer dat = bbLen.duplicate();
                        // 翻转
                        dat.flip();
                        LOG.trace("0x{} bbLen {}", Long.toHexString(sessionId), ByteBufUtil.hexDump(Unpooled.wrappedBuffer(dat)));
                    }
                    // bb剩余空间大于message中可读字节大小
                    if (message.readableBytes() < bbLen.remaining()) {
                        // 重设bbLen的limit
                        bbLen.limit(bbLen.position() + message.readableBytes());
                    }
                    // 将message内容写入bbLen中
                    message.readBytes(bbLen);
                    // 重置bbLen的limit
                    bbLen.limit(bbLen.capacity());
                    // 已经读完message,表示内容已经全部接收
                    if (bbLen.remaining() == 0) {
                        // 翻转
                        bbLen.flip();

                        if (LOG.isTraceEnabled()) {
                            LOG.trace("0x{} bbLen {}", Long.toHexString(sessionId), ByteBufUtil.hexDump(Unpooled.wrappedBuffer(bbLen)));
                        }
                        // 读取position后四个字节
                        int len = bbLen.getInt();
                        if (LOG.isTraceEnabled()) {
                            LOG.trace("0x{} bbLen len is {}", Long.toHexString(sessionId), len);
                        }
                        // 清除缓存
                        bbLen.clear();
                        if (!initialized) {// 未被初始化
                            if (checkFourLetterWord(channel, message, len)) {// 是否是四个字母的命令
                                return;
                            }
                        }
                        if (len < 0 || len > BinaryInputArchive.maxBuffer) {
                            throw new IOException("Len error " + len);
                        }
                        ZooKeeperServer zks = this.zkServer;
                        if (zks == null || !zks.isRunning()) {
                            throw new IOException("ZK down");
                        }
                        // 根据len重新分配缓冲,以便接收内容
                        zks.checkRequestSizeWhenReceivingMessage(len);
                        bb = ByteBuffer.allocate(len);
                    }
                }
            }
        } catch (IOException e) {
            LOG.warn("Closing connection to {}", getRemoteSocketAddress(), e);
            close(DisconnectReason.IO_EXCEPTION);
        } catch (ClientCnxnLimitException e) {
            // Common case exception, print at debug level
            ServerMetrics.getMetrics().CONNECTION_REJECTED.add(1);

            LOG.debug("Closing connection to {}", getRemoteSocketAddress(), e);
            close(DisconnectReason.CLIENT_RATE_LIMIT);
        }
    }

总结

ZooKeeper中网络通信不仅有基于NIO完成服务端与客户端之间的通信,也有基于Netty完成服务端与客户端之间的通信。本章讲述了基于Netty的通信,其通信效率相对于ZooKeeper默认的NIO的通信来说是高效的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值