请教一个关于 netty 的问题, TCP 客户端在短时间内重连,旧的通道不关闭会一直触发未接收到心跳事件 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
ReinerShir
V2EX    程序员

请教一个关于 netty 的问题, TCP 客户端在短时间内重连,旧的通道不关闭会一直触发未接收到心跳事件

  •  
  •   ReinerShir 2021-05-11 10:44:28 +08:00 3067 次点击
    这是一个创建于 1672 天前的主题,其中的信息可能已经有所发展或是发生改变。

    首先我添加了一个 netty 自带的心跳检测事件:

     channel.pipeline(). //定义超时时间,参数分别为接收超时、发送超时、所有超时的时间 addLast(new IdleStateHandler(60,0,0)). 

    即 1 分钟未收到数据包就断开连接

    这是断开事件的处理:

    /** * TCP 事件触发管理 */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) {//超时事件 IdleStateEvent idleEvent = (IdleStateEvent) evt; //超时一段时间未接收到消息 if (idleEvent.state() == IdleState.READER_IDLE) {//读 //断开连接 ctx.channel().close(); } } super.userEventTriggered(ctx, evt); } 

    现在问题来了,某个设备断电了,两秒后通电又重连了,然而在过了 60 秒后这个事件被触发了!因为老的连接通道一直未接收到数据,这时我会断开老的连接:ctx.channel().close(); ,我理想中的情况是老的连接就废弃了就让它断开嘛,新的连接通道继续使用就是,然而在我断开老的连接时新的连接也会断开。

    这就导致了刚刚才通电恢复连接的设备又得重连,因此,想请教一下ctx.channel().close() 难道不是关闭当前的连接通道吗?为什么会将重新连接的通道也关闭了?

    我也试过如果重连了就不关闭通道,但是不关闭的话该事件会一直触发,每 60 秒触发一次。

    20 条回复    2021-05-21 13:37:37 +08:00
    dallaslu
        1
    dallaslu  
       2021-05-11 11:41:31 +08:00
    断开之前打印一下,即将断开的到底是哪一个 channel
    ReinerShir
        2
    ReinerShir  
    OP
       2021-05-11 13:24:36 +08:00
    @dallaslu 打印过,断开的 channel 和新连接的 channel ID 是不一样的,但是奇怪的是我断开老 channel,新的连接也跟着一起断了
    3dwelcome
        3
    3dwelcome  
       2021-05-11 14:02:18 +08:00
    是不是掉电后,系统两个 TCP 都用了一个 source port,打印一下呢。

    比如你服务器发送 close()断开老通道,是向 192.168.1.5:4567 发送 FIN 消息。

    结果新通道刚巧也是 192.168.1.5:4567,就被一起断开了。
    ReinerShir
        4
    ReinerShir  
    OP
       2021-05-11 14:36:16 +08:00
    @3dwelcome 这个我也怀疑过,但是并不是这种情况,以下是我断电后打印的日志:

    断电后重新连接:
    重新连接 ,通道信息:[id: 0x15d2a4e1, L:/xxxx:8100 - R:/xxxx:27877]

    重连后还是触发是未接收到心跳包:
    将断开连接 ,连接通道:[id: 0x71bcb2a6, L:/xxxx:8100 - R:/xxxx:33776]

    断开后再次重连:
    重新连接,通道信息:[id: 0x6dc6bf85, L:/xxxx:8100 - R:/xxxx:50792]

    可以看到每次重连源端口都是变化的
    4kingRAS
        5
    4kingRAS  
       2021-05-11 15:40:26 +08:00
    你断开不要在 userEventTriggered 里写,直接 pipeline.addLast(new ReadTimeoutHandler(35)); 用自带的超时 handler

    注意 pipeline 添加 handler 的顺序
    xinhochen
        6
    xinhochen  
       2021-05-11 17:47:28 +08:00
    可以抓下 TCP 包来看看新连接的断开是谁主动发起的,看下是 netty 还是设备
    ReinerShir
        7
    ReinerShir  
    OP
       2021-05-12 09:13:13 +08:00
    @xinhochen 新连接的断开是应该 netty 发起的,通过日志看到执行断开老的 channel,但是新的连接也跟着一块断开了
    dallaslu
        8
    dallaslu  
       2021-05-12 11:00:50 +08:00
    来个可复现问题的代码吧
    xinhochen
        9
    xinhochen  
       2021-05-12 14:35:52 +08:00
    @ReinerShir 代码中有没有其它主动关闭 channel 的地方?如果没有,最好是抓 tcp 包,看设备日志,看程序日志。这三个放在一起分析,基本就能发现问题了。程序日志只能说明 channel 断开了,并不能说明是谁断开的。
    ReinerShir
        10
    ReinerShir  
    OP
       2021-05-13 09:33:45 +08:00
    @xinhochen 设备因为条件限制没法看日志,通过程序的日志可以判断不是设备主动断开,因为我断开老 channel 后没有收到心跳包了,而且其它每个可能会断开 channel 的地方我都加了日志的,并没有触发


    @dallaslu 核心代码是就上面那些了,复现的流程是,设备断电再打开 -> 两秒后设备重连 -> 60 秒后触发 netty 超时(IdleState.READER_IDLE)事件 -> 断开老的通道连接(ctx.channel().close()) -> 然后就没有心跳包了,新连接也断开了
    xinhochen
        11
    xinhochen  
       2021-05-13 21:57:18 +08:00
    @ReinerShir 设备如果用的 SIM 卡,要考虑运营商核心网的影响:设备断电后,netty 与运营商核心网的连接不会断开。建议把相关日志全部发上来看下,而不是截取部分,避免因为思维盲点遗漏了关键信息。
    xinhochen
        12
    xinhochen  
       2021-05-13 21:58:22 +08:00
    补充运营商核心网的相关信息:设备断电后,netty 与运营商核心网的连接不会"马上"断开
    ReinerShir
        13
    ReinerShir  
    OP
       2021-05-17 16:01:38 +08:00
    @xinhochen 确实是用的 SIM 卡,用了个投机取巧的办法解决了

    ```java
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent) {//超时事件
    IdleStateEvent idleEvent = (IdleStateEvent) evt;
    //超时一段时间未接收到消息
    if (idleEvent.state() == IdleState.READER_IDLE) {//读
    String deviceNo = NettyChannelManager.getKey(ctx.channel());

    if(!StringUtils.isEmpty(deviceNo)) {
    Channel savedChannel = NettyChannelManager.getChannel(deviceNo);
    Integer count = sessionRemoveMap.get(deviceNo);
    //断开连接事件有可能在重连后触发,因此要判断是否已经重连了,如果未重连才真正的断开连接
    if(count!=null||!savedChannel.isActive()) {
    if(count>0) {
    //移除标记
    sessionRemoveMap.remove(deviceNo);
    logger.warn("未接收到客户端消息,断开连接,设备号:{} ,{}",deviceNo,ctx.channel());
    //断开连接并移除保存的状态
    NettyChannelManager.removeAndClose(deviceNo);
    ctx.close();
    }else {
    //如果已经重连,则本通道不要再触发无心跳包事件
    //移除标记
    sessionRemoveMap.remove(deviceNo);
    return;
    }
    }else {
    logger.info("断线标记:{} channel:{}",deviceNo,ctx.channel());
    sessionRemoveMap.put(deviceNo, 1);
    }
    }else {
    logger.warn("将直接断开连接,channel:{}",ctx.channel());
    //直接断开
    ctx.close();
    }
    }
    }
    super.userEventTriggered(ctx, evt);
    }
    ```

    第一次断线只是标记一下,第二次断线才真正断开,如果重连了,清除标记

    ```java
    //如果已重连,将超时标记清除
    if((action & 0x000000ff)==EventContract.EVENT_REPORT_AUTHENTICATION){
    deviceId = NettyChannelManager.getKey(channel);
    //如果之前确实断线过一次
    if(sessionRemoveMap.remove(deviceId)!=null) {
    //移除老通道
    NettyChannelManager.removeChannel(ctx.channel());
    }
    sessionRemoveMap.put(deviceId,0);

    }

    ```
    ReinerShir
        14
    ReinerShir  
    OP
       2021-05-19 09:50:54 +08:00
    @dallaslu
    @xinhochen
    再请教一个问题,服务器发送指令到设备时有时候会和心跳包回复粘一块,比如心跳包用 channel.writeAndFlush(resp) 回复了 0x01 ,发送指令时同样用 channel.wrteAndFlush 返回 0x02,通过抓包发现偶尔会出现心跳包和指令连在一块的情况,即变成了 0x010x02
    xinhochen
        15
    xinhochen  
       2021-05-19 10:03:21 +08:00
    @ReinerShir 这种情况只能从消息定义上着手了,方案很多:分隔符(需要额外定义转义符,对应 netty 里的 DelimiterBasedFrameDecoder )、长度字段(对应 netty 里的 LengthFieldBasedFrameDecoder)
    ReinerShir
        16
    ReinerShir  
    OP
       2021-05-19 13:54:54 +08:00
    @xinhochen 我的错,没描述完整,我接收设备的消息是没问题的,服务端是做了分割的,代码如下:

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
    channel.pipeline().
    //定义超时时间,参数分别为接收超时、发送超时、所有超时的时间
    addLast(new IdleStateHandler(60,0,0)).
    //包尾以 EEFF 结束,使用 netty 自带的粘包处理器,false 参数表示不去掉包尾字符
    addLast(new DelimiterBasedFrameDecoder(1024,false,Unpooled.copiedBuffer(TCPServerUtils.hexStr2bytes("EEFF")))).
    //addLast(new LengthFieldBasedFrameDecoder(1024,2,1)).
    //addLast(new LengthFieldPrepender(1)).
    addLast(new CustomDecode()). //自定义解码器
    addLast(new ServerEventHandlerAdapter(event)) //自定义处理器
    ;
    }

    和设备通信是用分割符的,服务端做了粘包和半包处理。

    现在问题是出在设备端,有时候心跳回复和指令连在一起发过去了,我问了下设备那边的开发,他说是我发的不对,猜测可能是在心跳回复的那 0.几秒的时候正好指令过来了,所以就连在一块了
    xinhochen
        17
    xinhochen  
       2021-05-20 19:45:05 +08:00
    @ReinerShir 只有 Decoder,没有 Encoder 么?一般来说,发消息过去,也需要有分隔符之类的。要不然就会遇到你说的这种心跳和指令在一起的情况。
    ReinerShir
        18
    ReinerShir  
    OP
       2021-05-21 10:20:45 +08:00
    @xinhochen 我查了下发现 netty 发现没有 DelimiterBasedFrameEncoder .唯一找到一个 MessageToByteEncoder 不明白怎么用,官方文档根本没提起该类。

    我和设备之间通信是有自定义分割符的,例如返回信息给设备:
    AABB0201EEFF ,其中 EEFF 就是包尾分割符。

    现在的问题是我在代码中 writeAndFlush(AABB0201EEFF). 另一个线程 AABB0201EEFF(AABB0302EEFF),结果设备收到的是:AABB0201EEFFAABB0302EEFF ,这样子
    xinhochen
        19
    xinhochen  
       2021-05-21 12:06:46 +08:00
    @ReinerShir Encoder 就是自己继承 MessageToByteEncoder,然后实现 encode 方法。当然你那种把 AABB 和 EEFF 放在 writeAndFlush 里也是可以的,但是万一协议有变化,修改的工作量就大了,这就是为什么有 Encoder 存在的原因。
    设备收到这种是非常正常的,需要设备那边对收到数据做处理,就和你在 netty 里对 EEFF 做处理是一样的。TCP 里的数据是流式的,一次收到的数据不全,或者收到多余的数据都是再正常不过的事了。
    ReinerShir
        20
    ReinerShir  
    OP
       2021-05-21 13:37:37 +08:00
    @xinhochen 明白了,之所以来这里提问是因为设备开发那边说没办法做分割,所以才想能不能服务端这边确保每次发送数据都量独立一个包,谢谢啦。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3005 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 12:59 PVG 20:59 LAX 04:59 JFK 07:59
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86