Netty4.1源码分析—— 服务端构建连接和接收数据

本文只代表笔者一人的理解和叙述,笔者功力尚浅,如有错误,还请各位大神斧正。
阅读本篇文章前需阅读:
Netty4.1源码分析—— 服务端启动

一、引言与结论

Netty4.1源码分析—— 服务端启动分析得出,启动过程的实质是创建ServerSocketChannel并将该channel注册上OP_ACCEPT事件,说到底就是为即将到来的socket连接做好准备。

不同的socketChannel承担着不同的责任,我们在Netty中常说的连接则是指的是服务端和客户端的SocketChannel,客户端和服务端的SocketChannel是一对一的关系,所以ServerSocketChannel所做的准备实质上是为了创建和初始化SocketChannel所做的准备。

当我们分析某项事物所做的一系列动作的时候,不妨先来了解下它的最终目的。客户端和服务端构建完连接后,下一步的目的是开始接收/发送数据了,所以服务端构建连接其实也是在为下一步做准备——为接收数据做准备,也就是创建SocketChannel并将其注册OP_READ事件,而接收数据则实质就是处理OP_READ事件了。

二、AdaptiveRecvByteBufAllocator类

在分析构建连接过程和接收数据过程时,要着重了解一下·AdaptiveRecvByteBufAllocator·类。该类是两者中的重点,都依赖着该类的实现。

AdaptiveRecvByteBufAllocator类从名字上可以看出,它是用于Netty中的一个接收缓冲区ByteBuf的分配器,但是它是特殊的存在,它可以根据接收数据的不同来动态的分配ByteBuf来接收数据,这样就可以在保证数据能完整接收的同时,还能减少资源的消耗。先来看下该类的继承关系:


从继承关系上来看,它实现了DefaultMaxMessagesRecvByteBufAllocator抽象类,而下面也着重分析DefaultMaxMessagesRecvByteBufAllocator类和其实现类AdaptiveRecvByteBufAllocator。两者的具体实现是交由内部的Handle类来处理的,先分析DefaultMaxMessagesRecvByteBufAllocator类中的MaxMessageHandle类:

public abstract class MaxMessageHandle implements ExtendedHandle {
        private ChannelConfig config;
        private int maxMessagePerRead;
        private int totalMessages;
        private int totalBytesRead;
        private int attemptedBytesRead;
        private int lastBytesRead;
        private final boolean respectMaybeMoreData = DefaultMaxMessagesRecvByteBufAllocator.this.respectMaybeMoreData;
        private final UncheckedBooleanSupplier defaultMaybeMoreSupplier = new UncheckedBooleanSupplier() {
            @Override
            public boolean get() {
                return attemptedBytesRead == lastBytesRead;
            }
        };
        // ....
        @Override
        public ByteBuf allocate(ByteBufAllocator alloc) {
            return alloc.ioBuffer(guess());
        }

        @Override
        public final void incMessagesRead(int amt) {
            // 记录读取次数
            totalMessages += amt;
        }

        @Override
        public boolean continueReading() {
            return continueReading(defaultMaybeMoreSupplier);
        }

        @Override
        public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {
            return config.isAutoRead() &&
                    // respectMaybeMoreData为false时表示不“慎重”对待读取更多数据,只要有数据就一直读16次,默认true
                    // maybeMoreDataSupplier是判断有更多数据的可能性
                   (!respectMaybeMoreData || maybeMoreDataSupplier.get()) &&
                   totalMessages < maxMessagePerRead &&
                   totalBytesRead > 0;
        }
        // ...
    }

2.1 参数解释

其内部几个重要的参数解释如下:

  • maxMessagePerRead: 最大连续读的次数,默认是16次。maxMessagePerRead表示一次处理OP_READ事件中可以读取的最大次数。
  • attemptedBytesRead:表示一个状态,状态的值是当前分配接收缓冲区ByteBuf的可写入空间的大小,用于判断接收缓冲区分配大小的一个条件。与当前读取的字节数进行对比(lastBytesRead),如果不等则表示接收缓冲区需要改变大小的一个“意愿”,但是接收缓冲区ByteBuf不一定会改变。
  • lastBytesRead:当前一次读取的字节数。
  • respectMaybeMoreData:表示读取的一种状态,为false时表示不“慎重”对待读取更多数据,只要有数据就一直读16次,默认true。
  • totalMessages:一次处理OP_READ事件中读取的次数。
  • totalBytesRead:一次处理OP_READ事件读取的总字节数。

2.2 continueReading方法

了解完参数之后,来理解其中最重要的方法continueReading,该方法实现了一个判断,用于决定本次OP_READ事件是否需要再次读取数据。它有4个判断条件,当4个条件全部满足时,本次处理OP_READ事件才会再次进行读取数据:

  1. maybeMoreDataSupplier提供的值为true或者respectMaybeMoreData为false。
  2. 配置里自动读取打开。
  3. 读取的总次数小于最大读取次数(16次)。
  4. 读取的总字节数大于0。

详细解释下第一点,maybeMoreDataSupplier提供的值是否为true其实指的是attemptedBytesRead的值是否等于lastBytesRead。当前者等于后者时,则表示本次读取接收缓冲区ByteBuf已被写满,需要进行下一次读取才能将本次OP_READ或者OP_ACCEPT事件带来的数据读取完,而前者大于后者的话,则表示本次读取ByteBuf没有被装满,下一次可以把ByteBuf的大小缩小来减少资源的消耗。(注:不会小于的情况,因为attemptedBytesRead的大小是ByteBuf可写入空间的大小)

2.3 guess方法

另外一个重要的方法是guess方法,它在allocate方法分配ByteBuf大小时被调用:

public ByteBuf allocate(ByteBufAllocator alloc) {
    return alloc.ioBuffer(guess());
}

其实现是在AdaptiveRecvByteBufAllocator类里:

public int guess() {
    return nextReceiveBufferSize;
}

从这个方法里的可以得到一个新的变量nextReceiveBufferSize,这个变量才是决定下一次ByteBuf会分配多大的空间来接收数据。查看其调用链,会发现其在在readComplete方法里被调用:

// decreaseNow是决定是否缩容或者扩容的条件
// 缩容需要确认一次后再缩容,扩容则不需要,actualReadBytes参数为实际本次读取的字节数
private void record(int actualReadBytes) {
    if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
        if (decreaseNow) {
            index = max(index - INDEX_DECREMENT, minIndex);
            nextReceiveBufferSize = SIZE_TABLE[index];
            decreaseNow = false;
        } else {
            decreaseNow = true;
        }
    } else if (actualReadBytes >= nextReceiveBufferSize) {
        index = min(index + INDEX_INCREMENT, maxIndex);
        nextReceiveBufferSize = SIZE_TABLE[index];
        decreaseNow = false;
    }
}

@Override
public void readComplete() {
    record(totalBytesRead());
}

SIZE_TABLE是一个存储一系列升序数字的数组,用来控制扩缩容nextReceiveBufferSize的大小,也就是说nextReceiveBufferSize的大小只能是SIZE_TABLE里的值。而INDEX_DECREMENT缩容移动的索引大小与INDEX_INCREMENT扩容移动的索引大小的值分别为1和4。

record方法中,decreaseNow是一个决定是否缩容或者扩容的条件,它的值默认是false。当实际读取的字节数(actualReadBytes)小于等于数字表(SIZE_TABLE)中当前索引值前一位的值时,将decreaseNow置为true,如果下次读取还是满足小于的情况,那么就会减少nextReceiveBufferSize的大小,这意味着每次缩容ByteBuf的大小需要确认两次才会进行缩容。而如果大于时,则会增加nextReceiveBufferSize的大小,以此为机制来实现动态的一个扩容。

从上述描述中,可以得出一个结论,Netty动态分配ByteBuf的空间时对缩容比较谨慎,对扩容比较大胆。

三、NioUnsafe类

NioUnsafe类是Netty中专门处理NIO相关操作的一些系列类,它主要有两个实现类:NioByteUnsafe类和NioMessageUnsafe类,其中前者对应接收数据的逻辑处理,后者对应构建连接的逻辑处理。

之所以要在谈论构建连接过程和接收数据过程前谈到这个系列类,是因为在处理两者时都是走的EventLoop事件处理中同一个逻辑,也就是处理OP_ACCEPT事件和处理OP_READ事件的入口是一样的,都调用了NioUnsafe类的read方法:

/ 处理OP_READ or OP_ACCEPT
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
    unsafe.read();
}

如果对EventLoop事件循环处理不熟悉的同学可以参照Netty4.1源码分析—— 服务端启动一文。

四、构建连接过程

处理OP_ACCEPT事件对应的NioUnsafe类指的是AbstractNioMessageChannel类里的内部类NioMessageUnfafe类,因为其read方法比较长,所以我们分步骤来分析。

4.1 如何创建SocketChannel?

在read方法中其doReadMessages方法是用来创建SocketChannel的,其源码如下:

protected int doReadMessages(List<Object> buf) throws Exception {
    SocketChannel ch = SocketUtils.accept(javaChannel());
    try {
        if (ch != null) {
            buf.add(new NioSocketChannel(this, ch));
            return 1;
        }
    } catch (Throwable t) {
        //...
    }

    return 0;
}

从代码中看出,该方法接收一个buf数组,返回1代表创建成功,返回0代表失败。buf数组在这一步是用来存放已创建好的SocketChannel的,buf数组在后续广播read事件时会用到。而创建SocketChannel调用的是SocketUtilsaccept方法:

public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException {
    try {
        return AccessController.doPrivileged(new PrivilegedExceptionAction<SocketChannel>() {
            @Override
            public SocketChannel run() throws IOException {
                return serverSocketChannel.accept();
            }
        });
    } catch (PrivilegedActionException e) {
        throw (IOException) e.getCause();
    }
}

可以看出,创建SocketChannelServerSocketChannelaccept方法,其内部调用的是NIO的NATIVE方法来构建SocketChannel实例。

在了解完上述知识后,再总体看一下NioMessageUnfafe类的read方法前半段:

private final List<Object> readBuf = new ArrayList<Object>();

@Override
public void read() {
    assert eventLoop().inEventLoop();
    final ChannelConfig config = config();
    //得到ServerSocketChannel的pipline
    final ChannelPipeline pipeline = pipeline();
    // 新建一个AdaptiveRecvByteBufAllocator类
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    allocHandle.reset(config);

    boolean closed = false;
    Throwable exception = null;
    try {
        try {
            do {
                // 生成一个SocketChannel
                int localRead = doReadMessages(readBuf);
                if (localRead == 0) {
                    break;
                }
                if (localRead < 0) {
                    closed = true;
                    break;
                }
                // 记录读取的次数
                allocHandle.incMessagesRead(localRead);
            } while (continueReading(allocHandle));
        } catch (Throwable t) {
            exception = t;
        }
    } finally {
        //.....
    }
}

在构建连接的情况下,continueReading中的读取字节数总是会等于0,所以不会再继续循环下去。

4.2 如何初始化SocketChannel?

Netty4.1源码分析—— 服务端启动一文中,可以了解到ServerSocketChannel所绑定的handler类中有一个ServerBootstrapAcceptor类专门来初始化SocketChannel,那么怎么将SocketChannel传递到ServerBootstrapAcceptor类中呢?这就要分析一下NioMessageUnfafe类的read方法的后半段:

int size = readBuf.size();
for (int i = 0; i < size; i ++) {
    readPending = false;
    // 在这一步将SocketChannel传播出去,让Acceptor接收到进行初始化
    pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
pipeline.fireChannelReadComplete();

在此处,通过ServerSocketChannel所绑定的pipline通过广播channelRead事件,将SocketChannel广播出去,此时的pipline结构如图:


也就是在此处会将SocketChannel通过channelRead事件广播到ServerBootstrapAcceptor类中的channelRead方法里:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;

    child.pipeline().addLast(childHandler);

    setChannelOptions(child, childOptions, logger);
    setAttributes(child, childAttrs);

    try {
        // next 一个EventLoop绑定
        childGroup.register(child).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    forceClose(child, future.cause());
                }
            }
        });
    } catch (Throwable t) {
        forceClose(child, t);
    }
}

关于EventLoopSocketChannel如何绑定的可以阅读Netty4.1源码分析—— 服务端启动一文中的3.1.4章节,其内容和ServerSocketChannel绑定过程一致。区别的是,虽然两者都是最终调用了io.netty.channel.AbstractChannel.AbstractUnsafe类的register0方法,也是先利用doRegister方法只注册上该channel感兴趣的事件集为0。区别在于ServerSocketChannel是利用doBin0方法来完成注册OP_ACCEPT事件的,而此时SocketChannel在该方法执行时就已经被激活了,所以直接走的pipline.channelActive事件来完成注册OP_READ事件:

private void register0(ChannelPromise promise) {
    try {
        //...
        boolean firstRegistration = neverRegistered;
        doRegister();
        neverRegistered = false;
        registered = true;
        
        pipeline.invokeHandlerAddedIfNeeded();
        // 设置promise success, 注册ServerSocketChannel那么这里就是调用doBind0
        safeSetSuccess(promise);
        pipeline.fireChannelRegistered();
        // 注册ServerSocketChannel在这步没有激活,不会往下走
        // 注册socketChannel时已被激活,所以继续执行
        if (isActive()) {
            if (firstRegistration) {
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                beginRead();
            }
        }
    } 
    //....
}

而在最后注册OP_READ事件时,也是和ServerSocketChannel一致,最终调用的是AbstractNioChanneldeBeginRead方法,只不过此处的interestOps值为1,即代表着OP_READ事件:

protected void doBeginRead() throws Exception {
    // Channel.read() or ChannelHandlerContext.read() was called
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    readPending = true;

    final int interestOps = selectionKey.interestOps();
    // OP_ACCEPT = 1 << 4 = 16/OP_READ = 1 <<0 = 1
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

4.3 构建连接总结

  1. 利用Selectorselect方法监听到OP_ACCEPT事件,调用AbstractNioMessageChannel类里的内部类NioMessageUnfafe类的read方法来处理该OP_ACCEPT事件。
  2. 采用ServerSocketChannelaccept方法创建一个SocketChannel
  3. 通过ServerSocketChannelpipline广播channelRead事件将SocketChannel传递至ServerBootstrapAcceptor类中channelRead方法中进行初始化和EventLoop绑定。
  4. 初始化的时候调用NIO的register方法将SocketChannel注册进Selector,注意此处设置SocketChannel的感兴趣事件集为0。
  5. 最终注册OP_READ事件集是通过pipline广播channelActive消息来完成的,OP_READ事件代表值为1。

五、接收数据过程

接收数据过程采用的是NioByteUnsafe类的read方法,我们来直接看源码:

public final void read() {
    //....
    final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
    allocHandle.reset(config);

    ByteBuf byteBuf = null;
    boolean close = false;
    try {
        do {
            //利用guess方法初始化一个bytebuf,guess猜测的字节大小是动态变化的
            byteBuf = allocHandle.allocate(allocator);
            // 将channel的数据读取到bytebuf里面,并记录本次读取的字节数
            allocHandle.lastBytesRead(doReadBytes(byteBuf));
            // 本次读取的字节数≤0
            if (allocHandle.lastBytesRead() <= 0) {
                //...
                break;
            }
            // 记录读取的次数,用于计算一次OP_READ事件读取次数是否超过16次
            allocHandle.incMessagesRead(1);
            readPending = false;
            // 将读取的数据传播到handler上处理
            pipeline.fireChannelRead(byteBuf);
            byteBuf = null;
        } while (allocHandle.continueReading());
        //记录本次读取事件的读取数据的总大小,并计算出用于下次读取分配适合大小的ByteBuff(guss方法用到)
        allocHandle.readComplete();
        // 这一步表示一次OP_READ事件完成
        pipeline.fireChannelReadComplete();
        //...
    } 
    //...
}

recvBufAllocHandle方法中,对每一个SocketChannel都会只拥有一个AdaptiveRecvByteBufAllocator实例:

public RecvByteBufAllocator.Handle recvBufAllocHandle() {
    if (recvHandle == null) {
        recvHandle = config().getRecvByteBufAllocator().newHandle();
    }
    return recvHandle;
}

doReadBytes方法中,将Bytebuf可写入空间的大小赋值给attemptedBytesRead,并且将channel上的数据移到ByteBuf里面:

protected int doReadBytes(ByteBuf byteBuf) throws Exception {
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    // 赋值给attemptedBytesRead
    allocHandle.attemptedBytesRead(byteBuf.writableBytes());
    //读取数据
    return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

从代码中看出,每次一个OP_READ事件读取循环完成后,都会调用AdaptiveRecvByteBufAllocator内部类实例HandleImplreadComplete方法,也就是在这一步调用了计算nextReceiveBufferSize值的record方法,并同时利用piplineChannelReadComplete事件通知本次OP_READ`处理已完成:

public void readComplete() {
    record(totalBytesRead());
}

总结一下,对于接收数据的过程,其实就是分析动态分配ByteBuf的过程。在初始化ByteBuf时,利用guess方法来指定ByteBuf的大小,guess方法里的值nextReceiveBufferSize在每次OP_READ事件完成后,调用record方法计算下一次nextReceiveBufferSize的大小。在缩容时,需要确认两次才会减小nextReceiveBufferSize的大小,而在扩容时则不需要。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值