文章目录
本文只代表笔者一人的理解和叙述,笔者功力尚浅,如有错误,还请各位大神斧正。
阅读本篇文章前需阅读:
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
来接收数据,这样就可以在保证数据能完整接收的同时,还能减少资源的消耗。先来看下该类的继承关系:
![](https://suyeq.oss-cn-shenzhen.aliyuncs.com/CloudNotes/netty/Snipaste_2021-10-17_19-55-26.png)
从继承关系上来看,它实现了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事件才会再次进行读取数据:
maybeMoreDataSupplier
提供的值为true或者respectMaybeMoreData
为false。- 配置里自动读取打开。
- 读取的总次数小于最大读取次数(16次)。
- 读取的总字节数大于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
调用的是SocketUtils
的accept
方法:
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();
}
}
可以看出,创建SocketChannel
是ServerSocketChannel
的accept
方法,其内部调用的是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
结构如图:
![](https://suyeq.oss-cn-shenzhen.aliyuncs.com/CloudNotes/netty/Snipaste_2021-09-01_22-24-17.png)
也就是在此处会将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);
}
}
关于EventLoop
和SocketChannel
如何绑定的可以阅读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
一致,最终调用的是AbstractNioChannel
的deBeginRead
方法,只不过此处的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 构建连接总结
- 利用
Selector
的select
方法监听到OP_ACCEPT
事件,调用AbstractNioMessageChannel
类里的内部类NioMessageUnfafe
类的read方法来处理该OP_ACCEPT
事件。 - 采用
ServerSocketChannel
的accept
方法创建一个SocketChannel
。 - 通过
ServerSocketChannel
的pipline
广播channelRead
事件将SocketChannel
传递至ServerBootstrapAcceptor
类中channelRead
方法中进行初始化和EventLoop
绑定。 - 初始化的时候调用NIO的
register
方法将SocketChannel
注册进Selector
,注意此处设置SocketChannel
的感兴趣事件集为0。 - 最终注册
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
内部类实例HandleImpl
的readComplete
方法,也就是在这一步调用了计算nextReceiveBufferSize
值的record方法,并同时利用
pipline的
ChannelReadComplete事件通知本次
OP_READ`处理已完成:
public void readComplete() {
record(totalBytesRead());
}
总结一下,对于接收数据的过程,其实就是分析动态分配ByteBuf
的过程。在初始化ByteBuf
时,利用guess
方法来指定ByteBuf
的大小,guess
方法里的值nextReceiveBufferSize
在每次OP_READ
事件完成后,调用record
方法计算下一次nextReceiveBufferSize
的大小。在缩容时,需要确认两次才会减小nextReceiveBufferSize
的大小,而在扩容时则不需要。