netty websocket 长连接自动断开 问题定位修复

背景

某业务为了提高性能(原有方案前端定时查询后端接口后来因为接口太多+时效性取消了此方案)选型了长连接。使用netty 4.3.18 搭建长链接服务 生产消息1分钟内没有产生消息 前端就自动断开了,然后前端做了自动重连就会发现好多断开的连接。

排查

前端断开的函数回调日志 code=1006,reason="" 1006 用于期望收到状态码时连接非正常关闭,原因空

看到这个问题第一时间现在dev环境验证了 并没有发现断链的现象。

然后生产对比 环境 发现 生产转发使用了 nginx 代理 找运维看下了没有配置 proxy_read_timeout 参数默认是 60s 这就解释了 为什么每次都是1分钟断开重连

「proxy_read_timeout参数」 默认值60秒,该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间。即是服务器对你等待最大的时间,也就是说当你使用nginx转发webSocket的时候,如果60秒内没有通讯,依然是会断开的,所以,你可以按照你的需求来设定。比如说,我设置了5分钟,那么如果我5分钟内有通讯,或者5分钟内有做心跳的话,是可以保持连接不中断的。所以这个时间是看你的业务需求来调整时间长短的。

「proxy_send_timeout参数」 默认值 60s,设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接。

如何解决

netty增加心跳

保证 1分钟内有读写消息 这样ng 就会出触发断开连接 添加IdleStateHandler 设置心跳检查

scala 复制代码
public class IdleStateHandlerWebSocketChannelInitializer extends ```
ChannelInitializer<SocketChannel>
``` {
    private static final int READER_IDLE_TIME_SECONDS = 30; // 如果30秒没有读操作, 则触发一个 READER_IDLE 事件
    private static final int WRITER_IDLE_TIME_SECONDS = 40; // 如果40秒没有写操作, 则触发一个 WRITER_IDLE 事件
    private static final int ALL_IDLE_TIME_SECONDS = 70;    // 如果70秒没有读或写操作, 则触发一个 ALL_IDLE 事件


    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
    //  其他忽略
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new IdleStateHandler(READER_IDLE_TIME_SECONDS, WRITER_IDLE_TIME_SECONDS, ALL_IDLE_TIME_SECONDS));
       
    }
}

40s 内没有写消息会触发IdleState.WRITER_IDLE 事件往客户端发送一个心跳消息

java 复制代码
public class WebSocketServerChannelInboundHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    private static final String HEARTBEAT_SEQUENCE = "heartbeat"; // 心跳消息内容
    private static final String HEARTBEAT_SEQUENCE_OK = "heartbeat_ok"; // 心跳消息内容

    private final NettyMessage messageData;

    private final ExecutorService executor;

    public WebSocketServerChannelInboundHandler(NettyServerWebsocketConfig config, ExecutorService executor) {
        this.messageData = config.getNettyMessage();
        this.executor = executor;
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.WRITER_IDLE) {
                // 写空闲时发送心跳消息
                TextWebSocketFrame textWebSocketFrame = new TextWebSocketFrame(HEARTBEAT_SEQUENCE);
                ctx.writeAndFlush(textWebSocketFrame);
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
  • 长链接做心跳
    • 缺点:加大用户流量和服务端流量开销(一般国内业务没事,某国外流量比较精贵可能会不设置心跳减少流量开销)
    • 优点:无需客户端处理读写超时重连

nginx配置

ini 复制代码
proxy_connect_timeout 300s;  
proxy_send_timeout 300s;  
proxy_read_timeout 300s;

5 分钟没有读写 会触发关闭连接减少连接次数

复制代码
  • 设置 proxy_send_timeout 参数
    • 缺点:时间加长可以减少断开连接次数但是还是会发生
    • 优点:无需修改代码

参考

Nginx代理WebSocket方法-阿里云开发者社区 (aliyun.com)
CloseEvent - Web API 接口参考 | MDN (mozilla.org)

相关推荐
LucianaiB17 小时前
毕业老学长给我留的最后一句话是:“AI 写的,别挂我名。” 我直接 神(Seed Evolving) 来!助我!
后端
IvanCodes17 小时前
RAG 实战教程(一):RAG 工作原理与完整流程——分片、索引、召回、重排和生成
人工智能·后端·agent
CodeSheep18 小时前
稚晖君公司人事大变动,来了!
前端·后端·程序员
小满zs18 小时前
Go语言第八章(函数)
后端·go
eric-sjq19 小时前
10 分钟实战:用 0.6B 本地模型生成中文网页(WanlyFrontend + 编译器全流程)
javascript·机器学习·自然语言处理·html
To_OC19 小时前
LC 438 找到所有字母异位词:暴力超时后,我靠滑动窗口一招搞定
javascript·算法·leetcode
kyriewen20 小时前
面试官问"这段逻辑为什么这样写"——我才发现,这半年我写的代码,我自己都解释不了
前端·javascript·面试
stark张宇20 小时前
实战Go高级特性:Context超时控制、defer资源回收与Channel通信的关键避坑点
后端·go
一壶浊酒..21 小时前
Scrape Webpack练习
开发语言·javascript·webpack
凤山老林21 小时前
Spring Boot @Async 线上实战:从默认配置到生产级线程池治理
java·spring boot·后端