Netty关闭连接流程分析
在实际场景中,使用Netty4来实现RPC框架,服务端一般会验证协议,最简单的方法的协议探测,判断魔数是否正确。如果服务端无法识别协议会立即抛出异常,并主动关闭连接,此时客户端会收到read信号,在发现是一个关闭连接请求后会关闭本地连接,这其中用户可控的是InboundHandle收到 channelInactive方法。
今天想搞清楚的是:
假设consumer端 的InboundHandle收到 channelInactive 事件,是否可以立即结束这次请求并返回请求失败,而不必等到timeout才结束。
单纯的这个协议探测失败场景中,答案是可以立即结束,因为服务端不会响应任何数据。其他场景呢,在第1个InboundHandle中可以直接响应操作失败吗?
服务端主动关闭连接时,Netty的处理流程
在netty4中channel都被绑定到特定I/O EventLoop线程中,I/O线程收到信号为SelectionKey.OP_READ的就绪信号,如果allocHandle.lastBytesRead() = -1,则表示这是连接关闭的请求。
单独卡read的处理逻辑在AbstractNioByteChannel.read()中
public final void read() {
final ChannelConfig config = config();
if (shouldBreakReadReady(config)) {
clearReadPending();
return;
}
final ChannelPipeline pipeline = pipeline();
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
try {
do {
byteBuf = allocHandle.allocate(allocator);
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
// nothing was read. release the buffer.
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0; // -1表示关闭连接
if (close) {
// There is nothing left to read as we received an EOF.
readPending = false;
}
break;
}
allocHandle.incMessagesRead(1);
readPending = false;
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
if (close) {
closeOnRead(pipeline); //执行关连接逻辑
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close, allocHandle);
} finally {
// Check if there is a readPending which was not processed yet.
// This could be for two reasons:
// * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
// * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
//
// See https://github.com/netty/netty/issues/2254
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
}
close = allocHandle.lastBytesRead() < 0;
根据最后可读的bytes为-1,即为关闭
接下来是关闭连接的流程
检查此连接之前是否为active状态,并且当前是否为active,进入fireChannelInactiveAndDeregister流程,注销channel
这里不会立即注销channel,而是以一个任务的形式放到eventloop中稍后执行,文档中解释了不立即执行的原因:
private void invokeLater(Runnable task) {
try {
// This method is used by outbound operation implementations to trigger an inbound event later.
// They do not trigger an inbound event immediately because an outbound operation might have been
// triggered by another inbound event handler method. If fired immediately, the call stack
// will look like this for example:
//
// handlerA.inboundBufferUpdated() - (1) an inbound handler method closes a connection.
// -> handlerA.ctx.close()
// -> channel.unsafe.close()
// -> handlerA.channelInactive() - (2) another inbound handler method called while in (1) yet
//
// which means the execution of two inbound handler methods of the same handler overlap undesirably.
eventLoop().execute(task);
} catch (RejectedExecutionException e) {
logger.warn("Can't invoke task later as EventLoop rejected it", e);
}
}
大致的意思是 这个方法被用于出港操作来延迟触发一个到达的事件, 主要针对的场景是InboundHandler中产生了出港操作,比如主动断开连接,可能出现多个InboundHandler都会触发出港操作,这些操作可能是相同的如果立即执行,可能会重叠执行。
如果客户端从未收到任何数据,直接被服务端主动关闭是没有这个顾虑的,因为还没有InboundHandler处理数据。
invokeLater(new Runnable() {
@Override
public void run() {
try {
doDeregister();
} catch (Throwable t) {
logger.warn("Unexpected exception occurred while deregistering a channel.", t);
} finally {
if (fireChannelInactive) {
pipeline.fireChannelInactive();
}
// Some transports like local and AIO does not allow the deregistration of
// an open channel. Their doDeregister() calls close(). Consequently,
// close() calls deregister() again - no need to fire channelUnregistered, so check
// if it was registered.
if (registered) {
registered = false;
pipeline.fireChannelUnregistered();
}
safeSetSuccess(promise);
}
}
});
在从eventloop移除当前channel后(doDeregister()方法),进入finally代码块,如果之前连接是可用的,则fireChannelInactive为true
接下来进入pipeline.fireChannelInactive();流程
DefaultChannelPipreline 中有一个 AbstractChannelHandlerContext 链表,他按用户配置的顺序被写入Pipiline中,表头是Netty自定义的HeadContext,先执行头部的AbstractChannelHandlerContext
@Override
public final ChannelPipeline fireChannelInactive() {
AbstractChannelHandlerContext.invokeChannelInactive(head);
return this;
}
AbstractChannelHandlerContext持有 ChannelHandler对象,此处实际为 ChannelInboundHandler
private void invokeChannelInactive() {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelInactive(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelInactive();
}
}
这里会先进入netty预置的DefaultChannelPipeline$HeadContext实现中,然后走到了我们配置的第一个InboundHandle中来
默认的channelInactive实现在ByteToMessageDecoder中
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
channelInputClosed(ctx, true);
}
Netty对关闭连接容忍度非常高,关闭连接前如果连接仍然有可读的数据,会尝试把他读出来
private void channelInputClosed(ChannelHandlerContext ctx, boolean callChannelInactive) throws Exception {
CodecOutputList out = CodecOutputList.newInstance();
try {
channelInputClosed(ctx, out);
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
try {
if (cumulation != null) {
cumulation.release();
cumulation = null;
}
int size = out.size();
fireChannelRead(ctx, out, size);
if (size > 0) {
// Something was read, call fireChannelReadComplete()
ctx.fireChannelReadComplete();
}
if (callChannelInactive) {
ctx.fireChannelInactive();
}
} finally {
// Recycle in all cases
out.recycle();
}
}
}
/**
* Called when the input of the channel was closed which may be because it changed to inactive or because of
* {@link ChannelInputShutdownEvent}.
*/
void channelInputClosed(ChannelHandlerContext ctx, List<Object> out) throws Exception {
if (cumulation != null) {
callDecode(ctx, cumulation, out);
decodeLast(ctx, cumulation, out);
} else {
decodeLast(ctx, Unpooled.EMPTY_BUFFER, out);
}
}
/**
* Get {@code numElements} out of the {@link CodecOutputList} and forward these through the pipeline.
*/
static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
for (int i = 0; i < numElements; i ++) {
ctx.fireChannelRead(msgs.getUnsafe(i));
}
}
callDecode会尝试去decode已经在channel还为取完的数据,如果取到了,则外部方法中的size就会大于0,会走完fireChannelRead的流程
/**
* A {@link Channel} received a message.
*
* This will result in having the {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}
* method called of the next {@link ChannelInboundHandler} contained in the {@link ChannelPipeline} of the
* {@link Channel}.
*/
ChannelInboundInvoker fireChannelRead(Object msg);
fireChannelRead方法会调用ChannelPipeline中的下一个ChannelInboundHandler执行channelRead,我们常用的MessageToMessageDecoder执行channelRead时,会调用decode方法,也就是我们实现来处理请求的方法。
然后设置channelReadComplete()
最后调用ctx.fireChannelInactive();将事件传给下一个handler。
Netty4中I/O EventLoop是单线程执行的,对一个channel来说是线程安全的,而channel上的数据必然是先于close信号到达的,那么,当我们收到close新号时,之前已经发送过来的数据,一定已经至少被尝试处理过了吗?如果是这样,还有必要执行一下callDecode逻辑来fireChannelRead吗?
回到最初的代码块中
AbstractNioByteChannel.read()中
public final void read() {
final ChannelConfig config = config();
if (shouldBreakReadReady(config)) {
clearReadPending();
return;
}
final ChannelPipeline pipeline = pipeline();
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
try {
do {
byteBuf = allocHandle.allocate(allocator);
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
// nothing was read. release the buffer.
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
if (close) {
// There is nothing left to read as we received an EOF.
readPending = false;
}
break;
}
allocHandle.incMessagesRead(1);
readPending = false;
pipeline.fireChannelRead(byteBuf); //如果是正常的read,立即 fireChannelRead
byteBuf = null;
} while (allocHandle.continueReading());
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close, allocHandle);
} finally {
// Check if there is a readPending which was not processed yet.
// This could be for two reasons:
// * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
// * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
//
// See https://github.com/netty/netty/issues/2254
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
}
可以发现,如果是普通的读操作,会立即触发fireChannelRead,经过前面的分析,可以知道该方法将会invokeChannelRead,并且当前的executor是在eventLoop中的,那么channelRead会被立即执行,最终触发我们配置的InboundHandle
而close新号必然晚于正常的数据流,因此数据一定是先被InboundHandle处理后才接受到close信号的。
结论:
单从服务端主动断开连接的场景,如果收到close新号,则在第一个inboundHandle的 channelInactive 方法中,直接通知业务任务已失败是安全的,因为数据流如果返回完全,则必然被处理过。
如果客户端inbountHandle过程中主动close掉channel,也是安全的,因为他会稍后执行,并且尝试将最新的Inbound数据再次decode,如果有可处理的数据就走完所有InboundHandle(channel已经关闭,写响应会失败),没有则结束
Netty关闭连接流程分析的更多相关文章
- Hogp连接流程分析
当BLE设备已经完成配对,并且完成GATT服务的搜索,下一步就开始profile 的连接流程了,一般LE设备都是走的HOGP的流程,我们这篇文章就分析一下hogp的连接流程. 连接是从framewor ...
- Netty 拆包粘包和服务启动流程分析
Netty 拆包粘包和服务启动流程分析 通过本章学习,笔者希望你能掌握EventLoopGroup的工作流程,ServerBootstrap的启动流程,ChannelPipeline是如何操作管理Ch ...
- 【转】Netty 拆包粘包和服务启动流程分析
原文:https://www.cnblogs.com/itdragon/archive/2018/01/29/8365694.html Netty 拆包粘包和服务启动流程分析 通过本章学习,笔者希望你 ...
- Netty 源码学习——服务端流程分析
在上一篇我们已经介绍了客户端的流程分析,我们已经对启动已经大体上有了一定的认识,现在我们继续看对服务端的流程来看一看到底有什么区别. 服务端代码 public class NioServer { pr ...
- Netty 源码学习——客户端流程分析
Netty 源码学习--客户端流程分析 友情提醒: 需要观看者具备一些 NIO 的知识,否则看起来有的地方可能会不明白. 使用版本依赖 <dependency> <groupId&g ...
- u-boot启动流程分析(2)_板级(board)部分
转自:http://www.wowotech.net/u-boot/boot_flow_2.html 目录: 1. 前言 2. Generic Board 3. _main 4. global dat ...
- spark 启动job的流程分析
从WordCount開始分析 编写一个样例程序 编写一个从HDFS中读取并计算wordcount的样例程序: packageorg.apache.spark.examples importorg.ap ...
- Android之 MTP框架和流程分析
概要 本文的目的是介绍Android系统中MTP的一些相关知识.主要的内容包括:第1部分 MTP简介 对Mtp协议进行简单的介绍.第2部分 MTP框架 介绍 ...
- boost.asio源码剖析(三) ---- 流程分析
* 常见流程分析之一(Tcp异步连接) 我们用一个简单的demo分析Tcp异步连接的流程: #include <iostream> #include <boost/asio.hpp& ...
随机推荐
- 喜大普奔!GitHub中文版帮助文档上线了!
日前,GitHub 文档的简体中文正式发布,开发者可以到官方文档上随意查阅浏览中文文档啦! 对于想要玩 GitHub,但一直苦于英语水平较差的程序员来说,这真是一个天大的好消息.下面一起来感受一下 ...
- linux学习(三)Linux 系统目录结构
一.查看目录 登录系统后,在当前命令窗口下输入命令: ls / 树状目录结构: 二.目录解析 /bin: 存放二进制可执行文件(ls,cat,mkdir等). /boot: 存放启动Linux时使用的 ...
- Ajax一目了然
1.ajax的概念 局部刷新技术.不是一门新技术,是多种技术的组合.是浏览器端的技术. 2.ajax的作用. 实现在当前结果页面中显示其他请求的响应内容 3.ajax的使用 ajax的基本流程 //创 ...
- HYWZ 吴恩达-机器学习+神经网络反向传播
- iOS14剪切板探究,淘宝实现方法分析
随着iOS 14的发布,剪切板的滥用也被大家所知晓.只要是APP读取剪切板内容,系统都会在顶部弹出提醒,而且这个提醒不能够关闭.这样,大家在使用APP的过程中就能够看到哪些APP使用了剪切板. 正好我 ...
- Neo4j---性能优化
不会项目管理的研发不是好loder(^_^ ^_^),开个玩笑,目的是想说项目管理很重要,研发同胞们需要重视.重视.重视(重要的事情说三遍).随着项目业务扩展,不再是停留在基本某一业务范围,海量数据接 ...
- LeetCode刷题总结-动态规划篇
本文总结LeetCode上有动态规划的算法题,推荐刷题总数为54道.具体考点分析如下图: 1.中心扩展法 题号:132. 分割回文串 II,难度困难 2.背包问题 题号:140. 单词拆分 II,难度 ...
- 山寨一个Spring的@Component注解
1. 前言 我们在上一篇对Mybatis如何将Mapper接口注入Spring IoC进行了分析,有同学问胖哥这个有什么用,这个作用其实挺大的,比如让你实现一个类似@Controller的注解(或者继 ...
- Allegro PCB 转 PADS Layout 之后的修修补补
操作系统:Windows 10 x64 工具:PADS Layout VX.2.3 参考:Allegro转PADS以及后续修改 我们可以看到转换后的PCB文件,乱糟糟的,所以还需要我们手动修改一下. ...
- 机器学习算法——kNN(k-近邻算法)
算法概述 通过测量不同特征值之间的距离进行 [分类] 优点:精度高.对异常值不敏感.无数据输入假定. 缺点:计算复杂度高.空间复杂度高. 适用数据范围: 数值型 和 标称型 . 算法流程 数据 样本数 ...