在实际场景中,使用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关闭连接流程分析的更多相关文章

  1. Hogp连接流程分析

    当BLE设备已经完成配对,并且完成GATT服务的搜索,下一步就开始profile 的连接流程了,一般LE设备都是走的HOGP的流程,我们这篇文章就分析一下hogp的连接流程. 连接是从framewor ...

  2. Netty 拆包粘包和服务启动流程分析

    Netty 拆包粘包和服务启动流程分析 通过本章学习,笔者希望你能掌握EventLoopGroup的工作流程,ServerBootstrap的启动流程,ChannelPipeline是如何操作管理Ch ...

  3. 【转】Netty 拆包粘包和服务启动流程分析

    原文:https://www.cnblogs.com/itdragon/archive/2018/01/29/8365694.html Netty 拆包粘包和服务启动流程分析 通过本章学习,笔者希望你 ...

  4. Netty 源码学习——服务端流程分析

    在上一篇我们已经介绍了客户端的流程分析,我们已经对启动已经大体上有了一定的认识,现在我们继续看对服务端的流程来看一看到底有什么区别. 服务端代码 public class NioServer { pr ...

  5. Netty 源码学习——客户端流程分析

    Netty 源码学习--客户端流程分析 友情提醒: 需要观看者具备一些 NIO 的知识,否则看起来有的地方可能会不明白. 使用版本依赖 <dependency> <groupId&g ...

  6. u-boot启动流程分析(2)_板级(board)部分

    转自:http://www.wowotech.net/u-boot/boot_flow_2.html 目录: 1. 前言 2. Generic Board 3. _main 4. global dat ...

  7. spark 启动job的流程分析

    从WordCount開始分析 编写一个样例程序 编写一个从HDFS中读取并计算wordcount的样例程序: packageorg.apache.spark.examples importorg.ap ...

  8. Android之 MTP框架和流程分析

    概要 本文的目的是介绍Android系统中MTP的一些相关知识.主要的内容包括:第1部分 MTP简介            对Mtp协议进行简单的介绍.第2部分 MTP框架            介绍 ...

  9. boost.asio源码剖析(三) ---- 流程分析

    * 常见流程分析之一(Tcp异步连接) 我们用一个简单的demo分析Tcp异步连接的流程: #include <iostream> #include <boost/asio.hpp& ...

随机推荐

  1. 实现图片的上传(要求:上传到指定的FTP服务器)

    考核的知识点: (1)Linux系统的使用 (2)tengine 纯HTTP的web服务器 (3)SpringMVC的上传功能 (4)FTP的数据传到 1.1        传统上传方式的问题 但是在 ...

  2. Linux环境下C++调试的三板斧

    调试解决程序的漏洞,是程序员最基本的技能之一.用惯了图形化IDE,在目前使用gtest框架进行单元测试,需要通过xshell远程连接Linux虚拟机进行C++代码的调试时,觉得很不适应.经过几天查资料 ...

  3. AI小白必读:深度学习、迁移学习、强化学习别再傻傻分不清

    摘要:诸多关于人工智能的流行词汇萦绕在我们耳边,比如深度学习 (Deep Learning).强化学习 (Reinforcement Learning).迁移学习 (Transfer Learning ...

  4. 在Ubuntu下部署Flask项目

    FlaskDemo 命名为test.py # coding=utf-8 from flask import Flask app = Flask(__name__) @app.route("/ ...

  5. application x-www-form-urlencoded与JS的encodeURIComponent()

    application/x-www-form-urlencoded 表单的enctype属性表示在发送到服务器之前应该如何对表单数据进行编码,默认值是application/x-www-form-ur ...

  6. Python练习题 044:Project Euler 016:乘方结果各个数值之和

    本题来自 Project Euler 第16题:https://projecteuler.net/problem=16 ''' Project Euler 16: Power digit sum 2* ...

  7. 井字棋小游戏(C语言)

    最近沉迷于<NetHack>.<DCSS>等字符游戏,对其很感兴趣,于是用C语言写了个字符界面的井字棋小游戏.欢迎大家指教. 编写时遇到了一些问题,我原先准备用循环,直到读取到 ...

  8. Java知识日常收集整理001Java获取变量的数据类型的实现方法

    一.具体情况区分 对于简单类型变量,是无法直接获得变量类型的:要想获取,必须自定义函数进行返回. 对于包装类型变量,是可以直接获得的,变量名称.getClass().getName(); 二.代码实现 ...

  9. C 清空输入缓冲区,以及fflush(stdin)的使用误区和解决方法

    转载:https://blog.csdn.net/Veniversum/article/details/62048870 对C 语言初学者来说,fflush(stdin)函数被解释为会清空输入缓冲区的 ...

  10. chattr 和 lsattr 命令详解

    lsattr 命令 lsattr 命令用于查看文件的第二扩展文件系统属性. 语法: lsattr(选项)(参数) 选项: -E:可显示设备属性的当前值,但这个当前值是从用户设备数据库中获得的,而不是从 ...