关闭连接:本质是取消 Channel 在 Selelctor 的注册
关闭连接:本质是取消 Channel 在 Selelctor 的注册
Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)
1. 主线分析
1.1 主线
关闭连接分两种:主动关闭(正常关闭)和被动关闭(异常关闭)。
- 多路复用器(Selector)接收到 OP_READ 事件
- 处理 OP_READ 事件:NioSocketChannel.NioSocketChannelUnsafe.read():
- 接受数据
- 判断接受的数据大小是否 < 0 , 如果是,说明是关闭,开始执行关闭
- 关闭 channel(包含 cancel 多路复用器的key)
- 清理消息:不接受新信息,fail 掉所有 queue 中消息。
- 触发 fireChannelInactive 和 fireChannelUnregistered。
- 读异常,同样开始执行关闭
1.2 知识点
(1)关闭连接本质
一句话概括:关闭连接的本质是取消 Channel 在 Selelctor 的注册。
- java.nio.channels.spi.AbstractInterruptibleChannel#close
- java.nio.channels.SelectionKey#cancel
(2)要点
- 关闭连接,会触发 OP_READ 方法。读取字节数是 -1 代表关闭。
- 数据读取进行时,强行关闭,触发 IO Exception,进而执行关闭。
- Channel 的关闭包含了 SelectionKey 的 cancel。
补充1:NIO 中,如果一个客户端进程退出,为什么会触发服务器的 OP_READ 事件?
epoll 触发一个对断关闭然后在 jvm 层被包装成了一个读事件。因为 epoll 收到退出事件的时候要触发一个读操作,读到 -1 认为退出,所以 java 从实际操作角度认为 epoll 的退出事件也是读。所以简化了 java 层处理的事件数。但这个时候用 channel.read() 方法读的时候,会报 java.io.IOException: 远程主机强迫关闭了一个现有的连接。如果是主动关闭可以在触发读事件第一件事是判断是否有效,比如先读一个字节 看看是不是 -1,如果是 -1 就停止。 如果异常是 reset by peer,则表示被动关闭,一个流氓方法是所有和链接相关的异常都 catch,然后关闭这个链接,没有更好的做法了,Netty 自己也是这样做的。
转载自《关于netty你需要了解的二三事》:https://cloud.tencent.com/developer/article/1452395
2. 源码分析
连接关闭是会触发 OP_READ 事件,无论是正常还是异常关闭,都会调用 closeOnRead 关闭连接,最终调用 unsafe.close 关闭连接。
2.1 read
AbstractNioByteChannel.NioByteUnsafe#read
-> closeOnRead
-> AbstractUnsafe#close
-> handleReadException
-> closeOnRead
在前面分析 AbstractNioByteChannel.NioByteUnsafe#read 时,我们忽略了异常的处理。现在回过头再看一下代码:
(1)NioByteUnsafe#read
try {
do {
byteBuf = allocHandle.allocate(allocator);
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
byteBuf.release();
byteBuf = null;
// 1. 正常关闭,返回 -1
close = allocHandle.lastBytesRead() < 0;
if (close) {
readPending = false;
}
break;
}
} while (allocHandle.continueReading());
if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
// 2. 如果异常是IOException,也需要关闭
handleReadException(pipeline, byteBuf, t, close, allocHandle);
}
说明: 无论是正常关闭(allocHandle.lastBytesRead() = -1)还是异常关闭(IOException),都会调用 closeOnRead 关闭连接。
(2)closeOnRead
closeOnRead 方法调用 close 关闭连接。
private void closeOnRead(ChannelPipeline pipeline) {
if (!isInputShutdown0()) {
if (isAllowHalfClosure(config())) {
// 特殊需求
shutdownInput();
pipeline.fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE);
} else {
// 基本上都是调用 close 方法关闭连接
close(voidPromise());
}
} else {
inputClosedSeenErrorOnRead = true;
pipeline.fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE);
}
}
2.2 close
unsafe.close 关闭连接,最终调用 NioSocketChannel#doClose(javaChannel.close) 或 NioEventLoop#cancel(key.cancel) 关闭连接,本质都会调用到 SelectionKey#cancel 取消注册。unsafe.close 做了如下工作:
- 预关闭:调用 prepareToClose 方法,实际上是判断 socket 是否配置了 soLinger。一旦配置了 soLinger 参数,socket 关闭就变成阻塞了,需要返回一个线程单独执行 doClose0 关闭任务。异步关闭连接,代码就不看了。
- 真正关闭连接:doClose0 方法会调用 javaChannel().close 来关闭连接。本质上也是调用 SelectionKey#cancel 取消注册。
- 清理 NioEventLoop 上的资源:重复调用 key.cancel(),但没有影响。同时清理 NioEventLoop 上资源。至于为什么 doDeregister 方法要重复取消注册?可能只调用 doDeregister 取消注册。
- 触发 ChannelInactive 和 ChannelUnregistered 事件。我们需要关注一下 head 有没有什么特殊的处理。
AbstractChannel.AbstractUnsafe#close
-> prepareToClose
-> doClose0
-> NioSocketChannel#doClose # √ javaChannel.close
-> fireChannelInactiveAndDeregister
-> deregister
-> AbstractNioChannel#doDeregister
-> NioEventLoop#cancel # √ key.cancel()
-> pipeline#fireChannelInactive
-> pipeline#fireChannelUnregistered
(1)close
private void close(final ChannelPromise promise, final Throwable cause,
final ClosedChannelException closeCause, final boolean notify) {
final boolean wasActive = isActive();
this.outboundBuffer = null; // 清理资源,不允许再写数据到缓冲区
Executor closeExecutor = prepareToClose(); // 1. 预关闭,设置soLinger后会阻塞关闭连接
doClose0(promise); // 2. 真正关闭连接
fireChannelInactiveAndDeregister(wasActive); // 3. 调用deregister,清理资源并触发事件
}
private void deregister(final ChannelPromise promise, final boolean fireChannelInactive) {
try {
doDeregister(); // 4. 调用eventloop.cancel
} catch (Throwable t) {
} finally {
pipeline.fireChannelInactive(); // 5. 触发channelInactive
pipeline.fireChannelUnregistered(); // 6. 触发dhannelUnregistered
}
}
(2)prepareToClose
prepareToClose 返回了一个线程用来单独执行关闭任务,因为开启 soLinger 后,关闭连接是阻塞的,需要异步关闭连接。NioSocketChannelUnsafe 中的实现如下:
@Override
protected Executor prepareToClose() {
// 配置soLinger后会阻塞关闭连接,返回一个默认的连接池执行关闭任务
if (javaChannel().isOpen() && config().getSoLinger() > 0) {
doDeregister();
return GlobalEventExecutor.INSTANCE;
}
return null;
}
(3)doClose
@Override
protected void doClose() throws Exception {
super.doClose();
javaChannel().close(); // 核心
}
(4)doDeregister
// AbstractNioChannel
@Override
protected void doDeregister() throws Exception {
eventLoop().cancel(selectionKey());
}
void cancel(SelectionKey key) {
key.cancel(); // 核心
cancelledKeys ++;
if (cancelledKeys >= CLEANUP_INTERVAL) {
cancelledKeys = 0;
needsToSelectAgain = true;
}
}
每天用心记录一点点。内容也许不重要,但习惯很重要!
posted on 2020-04-06 19:35 binarylei 阅读(1771) 评论(0) 编辑 收藏 举报