上篇文章主要为大家分析了NioEventLoop相关源码实现,其中提到在processSelectedKey方法中处理OP_WRITE、OP_READ、OP_ACCEPT、OP_CONNECT等事件,今天就让我们继续深入研究下Netty究竟是如何处理这些事件的。本篇文章内容较长,希望大家可以认真阅读,很多知识点都能和前面的文章形成闭环,比如分析ServerBootStrap提到过channel得初始化、NioEventLoop源码解析提到的重写execute方法和taskQueue等。
一、温故知新
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
try {
int readyOps = k.readyOps();
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
以上代码调用路径为NioEventLoop-run -> processSelectedKeys -> processSelectedKeysOptimized -> processSelectedKey,不熟悉NioEventLoop原理的同学可以查看我上一篇文章。通过上述源码可以看到,最终调用的是AbstractNioChannel.NioUnsafe类来处理各种nio事件。
二、类图
老规矩,有图先看图,以上就是Netty Channel相关类图。Channel是Netty抽象出来对网络I/O进行读写的相关接口,主要功能有发起连接、处理连接、关闭连接等。分层很多,不同层负责不同的功能,特定的功能交给特定的子类去实现,那就让我们自上而下开始阅读Channel相关设计吧
三、AbstractChannel解读
查看其构造函数如下:
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
一共初始化了几个属性
- parent由于channel有父子关系,这里记录下父信息
- id,channel的唯一标识,有兴趣的同学可以研究下相关实现,由几部分组成,大致就是机器id+自增id+时间戳+随机数组成
- unsafe类,实现具体的连接与读写,命名为unsafe表示不对外提供使用
- pipeline,类型为DefaultChannelPipeline,handler容器,主要处理数据的编解码以及执行自定义的业务逻辑
3.1. newUnsafe方法
查看newUnsafe源码如下:
protected abstract AbstractUnsafe newUnsafe();
方法很简单,抽象方法返回Unsafe类的实现类,不同的Channel实现类可以自定于自己的AbstractUnsafe,此处的AbstractUnsafe类则是AbstractChannel中提供的实现,大部分方法都是模版方法,具体的实现细节由子类完成,例如Bind方法:
@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
boolean wasActive = isActive();
try {
// 抽象方法,由子类AbstractNioChannel去实现
doBind(localAddress);
} catch (Throwable t) {
safeSetFailure(promise, t);
closeIfClosed();
return;
}
if (!wasActive && isActive()) {
invokeLater(new Runnable() {
@Override
public void run() {
pipeline.fireChannelActive();
}
});
}
safeSetSuccess(promise);
}
其中调用的doBind则由子类NioServerSocketChannel来实现,还记得前文分析ServerBootStrap启动流程-bind方法分析吗?时序图里曾提到过,底层调用的就是NioServerSocketChannel的doBind方法,当时其实没太理解为什么这么设计,觉得冗余封装,这次自顶向下看才理解,其实每层都有每层的职责。
3.2. EventLoop属性
每一个channel都会对应一个EventLoop线程,EventLoop属性不是在初始化channel的时候创建的,而且调用register方法时候,传递进来的,查看AbstractUnsafe-register方法源码如下:
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
ObjectUtil.checkNotNull(eventLoop, "eventLoop");
.. ...
AbstractChannel.this.eventLoop = eventLoop;
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
}
可以看到,在register方法中,通过eventLoop线程执行register0方法,从而注册channel。这里的execute方法,还记得前文分析NioEventLoop时候,提到过其父类重写了execute方法,此处调用就是其父类SingleThreadEventExecutor的execute方法,把任务放到taskQueue中,NioEventLoop线程启动后,会轮询taskQueue,通过runAllTask取出对应的任务,执行对应任务的run方法,对应到这里就是执行register0方法。通过debug也可以验证我们的想法
至于register方法的调用链路,还记得分析ServerBootStrap启动流程时候,提到了dobind的时候,首先执行initAndRegister方法,底层调用的就是AbstractChannel.AbstractUnsafe-register方法。
四、AbstractNioChannel解读
AbstractNioChannel本身也是一个抽象类,在AbstractChannel基础上增加了一些属性和方法,查看其构造函数如下:
private final SelectableChannel ch;
protected final int readInterestOp;
protected volatile SelectionKey selectionKey;
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
try {
ch.configureBlocking(false);
} catch (IOException e) {
ch.close();
}
}
主要属性如下:
- SelectableChannel ch,jdk原生nio对象
- int readInterestOp,nio事件类型
- SelectionKey selectionKey,注册selector后获取的key
可以看到,在AbstractNioChannel类中,已经将Netty的channel和jdk原生nio类关联起来了。
4.1. doRegister方法
分析bstractChannel.AbstractUnsafe-register时提到过,doRegister最终交给子类AbstractNioChannel去实现。查看源码如下:
@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
... ...
}
}
}
功能如下:
- 通过javaChannel()方法获取具体的nio channel,也就是构造函数中初始化的ch对象
- eventLoop().unwrappedSelector()获取到的是NioEventLoop的里unwrappedSelector对象,也就是jdk原生的selecotr对象
- javaChannel().register()方法表示把ch对象注册到NioEventLoop的selecotr上
- 注册成功后返回selectionKey,为其设置感兴趣的事件
4.2. AbstractNioUnsafe解读
在AbstractNioChannel类中,也对应存在一个抽象类AbstractNioUnsafe,继承了AbstractUnsafe类,实现了NioUnsafe接口。较为重要的是connect方法,源码如下:
@Override
public final void connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
try {
...省略校验 ...
boolean wasActive = isActive();
// 协议不同,连接方式也不同,因此doConnect方法由子类去实现
if (doConnect(remoteAddress, localAddress)) {
fulfillConnectPromise(promise, wasActive);
} else {
connectPromise = promise;
requestedRemoteAddress = remoteAddress;
// 获取连接超时事件
int connectTimeoutMillis = config().getConnectTimeoutMillis();
if (connectTimeoutMillis > 0) {
// 设置定时任务,最终还是由NioEventLoop线程去执行
connectTimeoutFuture = eventLoop().schedule(new Runnable() {
@Override
public void run() {
ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
if (connectPromise != null && !connectPromise.isDone()
&& connectPromise.tryFailure(new ConnectTimeoutException(
"connection timed out: " + remoteAddress))) {
close(voidPromise());
}
}
}, connectTimeoutMillis, TimeUnit.MILLISECONDS);
}
// 结果监听
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isCancelled()) {
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
connectPromise = null;
close(voidPromise());
}
}
});
}
} catch (Throwable t) {
promise.tryFailure(annotateConnectException(t, remoteAddress));
closeIfClosed();
}
}
connect方法,较为重要的逻辑链接逻辑交给了子类去实现,毕竟不同的协议,处理连接的方式可能不同。此外还有一个方法值得关注下,那就是finishConnect,源码如下
public final void finishConnect() {
assert eventLoop().inEventLoop();
try {
boolean wasActive = isActive();
// 判断连接结果,子类完成
doFinishConnect();
// 将SocketChannel改为监听读操作
fulfillConnectPromise(connectPromise, wasActive);
} catch (Throwable t) {
// 异常处理
fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress));
} finally {
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
connectPromise = null;
}
}
此方法只能在NioEventLoop中processSelectedKey方法中被调用,前文也层提到过。
五、AbstractNioByteChannel解读
AbstractNioChannel内部包装了nio channel,具备nio的注册和连接等功能,但是I/O的读写交给了子类去实现。通过类图也可以看出,AbstractNioChannel的实现有AbstractNioByteChannel和AbstractNioMessageChannel,前者发送和读取的对象是ByteBuf和 FileRegion,后者读写的对象是POJO对象。下面就让我们先来看看AbstractNioByteChannel相关实现。
5.1. flushTask属性
属性flushTask为task任务,定义如下:
private final Runnable flushTask = new Runnable() {
@Override
public void run() {
((AbstractNioUnsafe) unsafe()).flush0();
}
};
主要负责刷新发送缓存链表中的数据,write的数据没直接写到socket里,而是卸载了ChannelOutboundBuffer缓存中,当调用unsafe()).flush0()时候,最终调用的也是AbstractChannel里的抽象方法doWrite,具体的写数据逻辑,也交给了子类去实现
5.2. doWrite方法
主要功能是获取ChannelOutboundBuffer缓存中待发送的数据,进行循环发送,查看方法源码如下:
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
// 写请求自旋次数,默认为16
int writeSpinCount = config().getWriteSpinCount();
do {
// 获取当前channel得缓存ChannelOutboundBuffer中,还未发出去的消息
Object msg = in.current();
if (msg == null) {
// 所有消息都发成功了,清除cnannel中OP_WRITE事件
clearOpWrite();
return;
}
// 发送数据
writeSpinCount -= doWriteInternal(in, msg);
} while (writeSpinCount > 0);
// 缓冲区满了导致发送失败,doWriteInternal返回Integer.MAX_VALUE,此时writeSpinCount=Integer.MAX_VALUE,此时writeSpinCount < 0 = true
// 当发送16次还未发送完全,但每次都写成功时候,此时writeSpinCount=0,riteSpinCount < 0 = false
incompleteWrite(writeSpinCount < 0);
}
查看incompleteWrite源码如下:
protected final void incompleteWrite(boolean setOpWrite) {
if (setOpWrite) {
setOpWrite();
} else {
eventLoop().execute(flushTask);
}
}
setOpWrite=true,也就是上面分析的,此时缓冲区已经满了,导致发送失败,将OP_WRITE写操作时间添加到Channel的选择Key兴趣事件集中;
setOpWrite=false,表示写了16次还没写完,把选择Key的OP_WRITE事件从兴趣事件中移除,添加一个flushTask,并交给NioEventLoop线程去执行,NioEventLoop run方法自旋轮训到次任务时再执行。
查看doWriteInternal源码如下:
private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
// 处理ByteBuf类型数据
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
if (!buf.isReadable()) {
//
in.remove();
return 0;
}
// 实际发送的字节数据,交给子类去实现
final int localFlushedAmount = doWriteBytes(buf);
if (localFlushedAmount > 0) {
in.progress(localFlushedAmount);
if (!buf.isReadable()) {
in.remove();
}
// 正常发送,返回1,从16次里-1
return 1;
}
} else if (msg instanceof FileRegion) { // 处理 FileRegion类型数据
FileRegion region = (FileRegion) msg;
if (region.transferred() >= region.count()) {
in.remove();
return 0;
}
// 实际发送的字节数据,子类实现
long localFlushedAmount = doWriteFileRegion(region);
if (localFlushedAmount > 0) {
in.progress(localFlushedAmount);
if (region.transferred() >= region.count()) {
in.remove();
}
return 1;
}
} else {
// Should not reach here.
throw new Error();
}
// 发送失败,返回Integer.MAX_VALUE
return WRITE_STATUS_SNDBUF_FULL;
}
doWrite方法如上述所示,核心的doWriteBytes方法留给了子类去实现。
5.3. NioByteUnsafe解读
和其父类一样,AbstractNioByteChannel内部也有一个Unsafe类,用来执行I/O相关的操作,只不过这一次,不再是抽象类,而是继承了AbstractNioUnsafe类,实现了最后的抽象方法read,定义如下:
protected class NioByteUnsafe extends AbstractNioUnsafe {
@Override
public final void read() {
// 获取配置
final ChannelConfig config = config();
// 通道关闭
if (shouldBreakReadReady(config)) {
clearReadPending();
return;
}
final ChannelPipeline pipeline = pipeline();
// 内存分配器,默认实现为PooledByteBufAllocator
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
// 清空上一次读取的字节数
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
try {
do {
// 通过allocator分配内存
byteBuf = allocHandle.allocate(allocator);
// 读取通道接受缓冲区数据
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
// 没有数据可读,释放内存
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
if (close) {
readPending = false;
}
break;
}
// 更新读取消息计数器
allocHandle.incMessagesRead(1);
readPending = false;
// tcp传输会产生粘包问题,因此每次读都要触发channelRed事件,进而可以调用业务处理handler或者自定义拆包handler等
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
// 操作完毕
allocHandle.readComplete();
// 触发channel通道的readComplete事件
pipeline.fireChannelReadComplete();
if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close, allocHandle);
} finally {
// See https://github.com/netty/netty/issues/2254
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
}
5.4. 小节
通过上述分析可以看到,AbstractNioByteChannel内部类NioByteUnsafe实现了read方法,通过ByteBufAllocator进行缓冲区的读取。同时AbstractNioByteChannel实现了write方法,定义了用哪种方式刷新缓冲区的数据,只不过核心逻辑doWriteBytes方法依然留给了子类去实现。
六、AbstractNioMessageChannel解读
AbstractNioMessageChannel类写入和读取的数据都是Object,而不是字节流,因此,读数据的时候不存在粘包问题,所以可以先循环读完再触发channelRead事件。写逻辑就相对简单一些,把缓存在outBoundBuffer中的数据依次写入channel,直到数据全部写完,或者写次数不足再停止。
6.1. doWrite方法
doWrite方法如下:
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
final SelectionKey key = selectionKey();
final int interestOps = key.interestOps();
int maxMessagesPerWrite = maxMessagesPerWrite();
// 一直写,直到次数不足,次数可配置,默认是Integer.MAX_VALUE
while (maxMessagesPerWrite > 0) {
Object msg = in.current();
if (msg == null) {
break;
}
try {
boolean done = false;
// 获取配置中循环写的最大次数
for (int i = config().getWriteSpinCount() - 1; i >= 0; i--) {
// 调用子类去写数据
if (doWriteMessage(msg, in)) {
done = true;
break;
}
}
if (done) {
// 写成功,则从缓存链中移除,继续发送下一个节点
maxMessagesPerWrite--;
in.remove();
} else {
break;
}
} catch (Exception e) {
if (continueOnWriteError()) {
maxMessagesPerWrite--;
in.remove(e);
} else {
throw e;
}
}
}
if (in.isEmpty()) {
if ((interestOps & SelectionKey.OP_WRITE) != 0) {
key.interestOps(interestOps & ~SelectionKey.OP_WRITE);
}
} else {
if ((interestOps & SelectionKey.OP_WRITE) == 0) {
key.interestOps(interestOps | SelectionKey.OP_WRITE);
}
}
}
6.2. read方法
read方法源码如下:
@Override
public void read() {
// 这里其实没看太懂为啥加这个判断
assert eventLoop().inEventLoop();
...逻辑类似 ...
boolean closed = false;
Throwable exception = null;
try {
try {
do {
// 读取数据,子类去实现
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
// 记录读取的次数
allocHandle.incMessagesRead(localRead);
} while (continueReading(allocHandle)); // 默认不超过16次
} catch (Throwable t) {
exception = t;
}
// 循环处理读到的数据包
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
// 每个数据包都触发channelRead
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
// 触发readComplete事件
pipeline.fireChannelReadComplete();
... ...
} finally {
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
七、NioSocketChannel解读
通过类图可以看到,AbstractNioByteChannel的实现类为NioSocketChannel,通过方法名也可以看出来,此类不在是抽象类,实现了最后遗留的抽象方法,同时也实现了io.netty.channel.socket.ServerSocketChannel类,其实再Netty服务中,每一个socket连接都会对应生成一个NioSocketChannel对象,该类主要负责I/O具体的读写和连接操作。核心方法如下:
- doReadBytes方法
- 负责从socket中读取数据,在AbstractNioByteChannel.NioByteUnsafe-read方法中被调用
@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
// 获取计算内存分配器handle
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
// 设置尝试读取字节数为buf的可写字节数
allocHandle.attemptedBytesRead(byteBuf.writableBytes());
// 从channel中读取字节并写入buf中,返回本次读取的字节数
return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}
- doConnect方法
- 负责和客户端连接,在AbstractNioChannel.AbstractNioUnsafe-connect方法中被调用
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
// 通过channel进行网络连接
boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
if (!connected) {
// 连接未成功,则绑定OP_CONNECT事件
selectionKey().interestOps(SelectionKey.OP_CONNECT);
}
}
- doWriteBytes
- 负责将ByteBuf类型数据写入socket,在AbstractNioByteChannel-doWriteInternal方法中被调用
@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
// 获取buf的可读字节书
final int expectedWrittenBytes = buf.readableBytes();
// 将buf写入socket中,返回写入的字节书
return buf.readBytes(javaChannel(), expectedWrittenBytes);
}
- doWriteFileRegion
- 负责将FileRegion类型数据写入socket,在AbstractNioByteChannel-doWriteInternal方法中被调用
@Override
protected long doWriteFileRegion(FileRegion region) throws Exception {
final long position = region.transferred();
// 基于文件内存映射技术进行文件发送
return region.transferTo(javaChannel(), position);
}
- doWrite方法
- 负责将数据写入socket中,在AbstractChannel.AbstractUnsafe-flush0方法中被调用
以上方法,其实在前面的几个小节中也都提到过,不同的子类有不同的实现方式。大部分逻辑都很简单,就不展开讲解了重点讲解下doWrite方法。
7.1. doWrite方法
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
SocketChannel ch = javaChannel();
// buf的可读字节数
int writeSpinCount = config().getWriteSpinCount();
do {
// 无数据可写
if (in.isEmpty()) {
clearOpWrite();
return;
}
// 获取一次最大可写字节数
int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
// 缓存由多个entry组成,每次写时都可能写多个entry
ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
int nioBufferCnt = in.nioBufferCount();
// 缓存汇总buffer的数量
switch (nioBufferCnt) {
case 0:
// 非byteBuffer数据,父去处理
writeSpinCount -= doWrite0(in);
break;
case 1: {
// 可读buffer
ByteBuffer buffer = nioBuffers[0];
// buffer可读字节数
int attemptedBytes = buffer.remaining();
// 讲buffer发送到socket缓存中
final int localWrittenBytes = ch.write(buffer);
// 发送失败
if (localWrittenBytes <= 0) {
// 讲写事件添加到事件兴趣中
incompleteWrite(true);
return;
}
// 根据成功写入字节数和尝试写入字节数调整下次最大可写字节数
adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite);
// 写多少,移除多少
in.removeBytes(localWrittenBytes);
// 循环次数减1
--writeSpinCount;
break;
}
default: {
// 尝试写入字节数
long attemptedBytes = in.nioBufferSize();
// 发送到socket缓存中的字节数
final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
// 发送失败
if (localWrittenBytes <= 0) {
incompleteWrite(true);
return;
}
// 动态调整
adjustMaxBytesPerGatheringWrite((int) attemptedBytes, (int) localWrittenBytes,
maxBytesPerGatheringWrite);
in.removeBytes(localWrittenBytes);
--writeSpinCount;
break;
}
}
} while (writeSpinCount > 0);
// 通过次数为0判断是否发送完全
incompleteWrite(writeSpinCount < 0);
}
八、NioServerSocketChannel解读
NioServerSocketChannel为AbstractMessageByteChannel的子类,专供server端使用,只负责监听socket接入,不负责I/O读写。重点关注下如何建立链接即可
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = SocketUtils.accept(javaChannel());
try {
if (ch != null) {
// 每个新的链接都会构建一个 NioSocketChannel
buf.add(new NioSocketChannel(this, ch));
return 1;
}
} catch (Throwable t) {
try {
// 异常处理
ch.close();
} catch (Throwable t2) {
logger.warn("Failed to close a socket.", t2);
}
}
return 0;
}
九、小节
本文主要为大家分析了下Netty channel相关实现。Netty的源码封装的确实有些复杂,不过按照类图一路梳理下来也有种豁然开朗的感觉,和前文的ServerBoot启动,以及NioEventLoop等知识点也都串联起来了,内容有些多,大家可以先收藏,后面有时间慢慢看哈。