当前位置: 首页 > news >正文

解决angular与jetty websocket 每30s自动断连的问题

背景:

        前端:angular 12,websocket接口由lib.dom.d.ts提供

        后端:java,websocket接口由jetty 12提供

问题现象:

        前端连上server后,每隔30s就会断开,由于长时间空闲,会导致websocket连接断开,所以前端也发送了ping报文(由于前端接口没有单独的ping/ping接口,因此ping报文和普通报文一样,都是调用的send()方法),每隔30s发一次。当然前端可实现以重连机制,但是这样就会出现反复重连的情况,会导致界面感知不友好,所以还是得找下根本原因。

前端打印的日志:

后端打印的日志:

onWebSocketClose, statusCode=1005, reason=null

前后端都没有断连的具体原因,只有状态码,这就很难办了。 为了省事,我用gpt搜出来的常见状态码如下:

从上图可以看出,不管是1005还是1006,都表示没有收到关闭帧的异常关闭。

 排查思路:

        因为客户端周期性的发送了ping报文,所以先排查下是不是服务端的空闲超时时间太短了导致的?

        // 显式创建 ServerConnector 并设置空闲超时ServerConnector connector = new ServerConnector(server);connector.setPort(port);connector.setIdleTimeout(86400000); // 1 天(毫秒)

上面的代码就是修改空闲超时时间,先设置为一天,然后重新测试,发现还是不行,前端还是会不断重连。

        难道设置的时间没效果?那就把jetty的日志打开,日志如下:

DEBUG 25-07-25 14:25:59.133 org.eclipse.jetty.websocket.core.server.internal.AbstractHandshaker.upgradeRequest(AbstractHandshaker.java:116) [qtp956673894-39]
session WebSocketCoreSession@1e78624f{SERVER,WebSocketSessionState@78abbc31{CONNECTING,i=NO-OP,o=NO-OP,c=null},[ws://10.10.10.10:8080/mq?token=test,null,false.[permessage-deflate]],af=true,i/o=4096/4096,fs=65536}->JettyWebSocketFrameHandler@6a65aae4[com.nsb.enms.notification.server.WebSocketServer]DEBUG 25-07-25 14:25:59.134 org.eclipse.jetty.io.IdleTimeout.setIdleTimeout(IdleTimeout.java:85) [qtp956673894-39]
Setting idle timeout 86400000 -> 86400000 on SocketChannelEndPoint@202ab0ab[{l=/10.10.10.10:8080,r=/20.20.20.20:45678,OPEN,fill=-,flush=-,to=6/86400000}{io=0/0,kio=0,kro=1}]->[HttpConnection@7fb68543[p=HttpParser{s=END,0 of -1},g=HttpGenerator@7e82a7c8{s=START}]=>HttpChannelState@bd8ef5b{handling=Thread[#39,qtp956673894-39,5,main], handled=false, send=SENDING, completed=false, request=GET@68dd4ee2 http://10.10.10.10:8080/mq?token=test HTTP/1.1}]

从日志上可以看到服务端的设置是生效了的。

        既然服务端没问题,那可能是客户端的空闲超时时间导致的?查了下资料,angular自带的websocket没有设置空闲超时时间的方法。那就换个思路,用java写个客户端来测试这个问题。

import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Frame;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.*;
import org.eclipse.jetty.websocket.core.OpCode;import java.nio.ByteBuffer;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;@WebSocket
public class ClientSocket {long start;public final CountDownLatch closeLatch = new CountDownLatch(1);private Session session;private ScheduledExecutorService scheduler;@OnWebSocketOpenpublic void onOpen(Session session) {this.session = session;start = System.currentTimeMillis();System.out.println("客户端:已连接到服务端,URI: " + session.getUpgradeRequest().getRequestURI() + "," + start);// 启动心跳scheduler = Executors.newSingleThreadScheduledExecutor();// 注意这里设置的间隔时间是31s
//        scheduler.scheduleAtFixedRate(this::sendPing, 0, 31, TimeUnit.SECONDS);// 注意这里设置的间隔时间是32s
//        scheduler.scheduleAtFixedRate(this::sendMSG, 0, 32, TimeUnit.SECONDS);new Thread(this::readConsoleInput).start();}@OnWebSocketMessagepublic void onMessage(Session session, String message) {System.out.println("客户端:收到服务端消息: " + message);}@OnWebSocketFramepublic void onFrame(Session session, Frame frame, Callback cb) {System.out.println("客户端:收到帧,操作码: " + frame.getOpCode());if (frame.getOpCode() == OpCode.PONG) {System.out.println("客户端:收到 PONG 帧,负载: " + frame.getPayload());}}@OnWebSocketClosepublic void onClose(Session session, int statusCode, String reason) {this.session = null;System.out.println("客户端:连接关闭,状态码: " + statusCode + ", 原因: " + reason + "," + (System.currentTimeMillis() - start));closeLatch.countDown();}@OnWebSocketErrorpublic void onError(Session session, Throwable throwable) {System.err.println("客户端:发生错误: " + throwable.getMessage() + "," + System.currentTimeMillis());throwable.printStackTrace();}private void readConsoleInput() {Scanner scanner = new Scanner(System.in);System.out.println("请输入消息(输入 'ping' 发送 PING 帧,'exit' 退出):");while (session != null && session.isOpen()) {String input = scanner.nextLine();if ("exit".equalsIgnoreCase(input)) {try {session.close(1000, "Client requested closure", Callback.from(() -> {System.out.println("客户端:消息发送成功: " + input);}, throwable -> {throwable.printStackTrace();}));} catch (Exception e) {e.printStackTrace();}break;} else if ("ping".equalsIgnoreCase(input)) {try {ByteBuffer payload = ByteBuffer.wrap("PingTest".getBytes());session.sendPing(payload, Callback.from(() -> {System.out.println("客户端:发送 PING 帧成功");}, throwable -> {throwable.printStackTrace();}));} catch (Exception e) {e.printStackTrace();}} else if (!input.trim().isEmpty()) {try {session.sendText(input, Callback.from(() -> {System.out.println("客户端:消息发送成功: " + input);}, throwable -> {throwable.printStackTrace();}));} catch (Exception e) {e.printStackTrace();}}}scanner.close();}private void sendPing() {if (session != null && session.isOpen()) {try {ByteBuffer payload = ByteBuffer.wrap("PingTest".getBytes());session.sendPing(payload, Callback.from(() -> {System.out.println("客户端:发送 PING 帧成功");}, throwable -> {throwable.printStackTrace();}));} catch (Exception e) {e.printStackTrace();}}}private void sendMSG() {if (session != null && session.isOpen()) {try {String input = String.valueOf(System.currentTimeMillis());session.sendText(input, Callback.from(() -> {System.out.println("客户端:消息发送成功: " + input);}, throwable -> {throwable.printStackTrace();}));} catch (Exception e) {e.printStackTrace();}}}
}

注意看,上面的代码,默认是把这两行代码屏蔽了的:

//        scheduler.scheduleAtFixedRate(this::sendPing, 0, 31, TimeUnit.SECONDS);
//        scheduler.scheduleAtFixedRate(this::sendMSG, 0, 32, TimeUnit.SECONDS);

这两行代码表示周期性发送ping/msg,

第一行表示每隔31s发送一次ping报文,

第二行表示每隔32s发送一次msg消息。

以下是调用代码:

import org.eclipse.jetty.websocket.client.WebSocketClient;import java.net.URI;
import java.time.Duration;
import java.util.concurrent.TimeUnit;public class TestClient {public static void main(String[] args) throws Exception {String serverUri = "ws://10.10.10.10:8080/mq";WebSocketClient client = new WebSocketClient();
//        client.setMaxTextMessageSize(65536);try {client.start();System.out.println("WebSocket 客户端已启动");ClientSocket wsClient = new ClientSocket();// 设置空闲超时时间,默认注释该行
//            client.setIdleTimeout(Duration.ofSeconds(60));client.connect(wsClient, new URI(serverUri));boolean closed = wsClient.closeLatch.await(5, TimeUnit.MINUTES);if (!closed) {System.out.println("等待连接关闭超时");}} catch (Exception e) {e.printStackTrace();} finally {client.stop();System.out.println("WebSocket 客户端已停止");}}
}

注意,因为java client支持设置空闲超时时间,所以上面的代码,默认先注释掉设置空闲超时时间的代码。执行上述代码,等待一会,可以看到如下输出日志:

WebSocket 客户端已启动
客户端:已连接到服务端,URI: ws://10.10.10.10:8080/mq,1753427445848
请输入消息(输入 'ping' 发送 PING 帧,'exit' 退出):
客户端:消息发送成功: 1753427445852
客户端:发生错误: Connection Idle Timeout,1753427475886
org.eclipse.jetty.websocket.api.exceptions.WebSocketTimeoutException: Connection Idle Timeoutat org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler.convertCause(JettyWebSocketFrameHandler.java:436)at org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler.onError(JettyWebSocketFrameHandler.java:240)...
Caused by: org.eclipse.jetty.websocket.core.exception.WebSocketTimeoutException: Connection Idle Timeout... 10 more
Caused by: java.util.concurrent.TimeoutException: Idle timeout expired: 30011/30000 msat org.eclipse.jetty.io.IdleTimeout.checkIdleTimeout(IdleTimeout.java:167)... 7 more
客户端:连接关闭,状态码: 1001, 原因: Connection Idle Timeout,30040
WebSocket 客户端已停止

从日志上可以得知,关闭原因是:Connection Idle Timeout,30040,说明连接空闲时间超过了30s导致客户端被关闭。

服务端也是打印的相同的日志:

onWebSocketClose, statusCode=1001, reason=Connection Idle Timeout, 

因为服务器端的空闲超时时间设置的1天,由此说明,连接断开是受客户端的空闲超时时间影响的,从日志上也可以得出,默认情况下,客户端的空闲超时时间就是30s

        我们前面提到的几行注释代码,就是为了验证这个30s的问题,接下来,挨个测试下那些被注释的代码,看下都有什么输出的结果。

验证代码:

测试1:设置客户端的超时时间

放开TestClient下的代码:

client.setIdleTimeout(Duration.ofSeconds(60));

这个表示空闲超时时间为60s后,重新执行测试用例,输出如下:

WebSocket 客户端已启动
客户端:已连接到服务端,URI: ws://10.10.10.10:8080/mq,1753428684378
请输入消息(输入 'ping' 发送 PING 帧,'exit' 退出):
客户端:发生错误: Connection Idle Timeout,1753428744397
...
Caused by: org.eclipse.jetty.websocket.core.exception.WebSocketTimeoutException: Connection Idle Timeout... 10 more
Caused by: java.util.concurrent.TimeoutException: Idle timeout expired: 60005/60000 msat org.eclipse.jetty.io.IdleTimeout.checkIdleTimeout(IdleTimeout.java:167)... 7 more
客户端:连接关闭,状态码: 1001, 原因: Connection Idle Timeout,60020
WebSocket 客户端已停止

日志显示60s之后才报超时,说明前面设置的超时代码生效了。

测试2:周期性发送ping报文

先注释掉TestClient的60s超时机制,保持默认30s,然后,将ClientSocket下的周期性发送ping报文的代码打开,间隔为31s。

TestClient.java 修改如下:

            // TestClient.java 中注释该行
//            client.setIdleTimeout(Duration.ofSeconds(60));

ClientSocket.java 修改如下: 

// ClientSocket.java 中放开该行,且周期为31s
scheduler.scheduleAtFixedRate(this::sendPing, 0, 31, TimeUnit.SECONDS);

执行结果如下:

WebSocket 客户端已启动
客户端:已连接到服务端,URI: ws://10.10.10.10:8080/mq,1753429262205
请输入消息(输入 'ping' 发送 PING 帧,'exit' 退出):
客户端:发送 PING 帧成功
客户端:收到帧,操作码: 10
客户端:收到 PONG 帧,负载: java.nio.HeapByteBufferR[pos=0 lim=8 cap=8]
客户端:发生错误: Connection Idle Timeout,1753429292256
Connection Idle Timeout... 10 more
Caused by: java.util.concurrent.TimeoutException: Idle timeout expired: 30015/30000 msat org.eclipse.jetty.io.IdleTimeout.checkIdleTimeout(IdleTimeout.java:167)... 7 more
客户端:连接关闭,状态码: 1001, 原因: Connection Idle Timeout,30053
WebSocket 客户端已停止

日志显示,问题复现,连接空闲超时超过30s 。

现在修改下ClientSocket.java代码,将超时时间设置为29s,代码如下:

// ClientSocket.java 中放开该行,且周期为29s
scheduler.scheduleAtFixedRate(this::sendPing, 0, 29, TimeUnit.SECONDS);

然后执行,输出的结果就不一样了: 

WebSocket 客户端已启动
客户端:已连接到服务端,URI: ws://10.10.10.10:8080/mq,1753429377099
请输入消息(输入 'ping' 发送 PING 帧,'exit' 退出):
客户端:发送 PING 帧成功
客户端:收到帧,操作码: 10
客户端:收到 PONG 帧,负载: java.nio.HeapByteBufferR[pos=0 lim=8 cap=8]
客户端:发送 PING 帧成功
客户端:收到帧,操作码: 10
客户端:收到 PONG 帧,负载: java.nio.HeapByteBufferR[pos=0 lim=8 cap=8]
客户端:发送 PING 帧成功
客户端:收到帧,操作码: 10
客户端:收到 PONG 帧,负载: java.nio.HeapByteBufferR[pos=0 lim=8 cap=8]
客户端:发送 PING 帧成功
客户端:收到帧,操作码: 10
客户端:收到 PONG 帧,负载: java.nio.HeapByteBufferR[pos=0 lim=8 cap=8]
客户端:发送 PING 帧成功
客户端:收到帧,操作码: 10
客户端:收到 PONG 帧,负载: java.nio.HeapByteBufferR[pos=0 lim=8 cap=8]

从上面可以得出结论,只要周期在30s以内发送ping报文,则不会出现超时的问题。

那么,ping报文是否必须发送呢,如果只发送msg呢,是否也能达到相同的效果?

测试3:发送msg报文

修改ClientSocket.java,代码如下:

        // 注释该行,不发送ping报文
//        scheduler.scheduleAtFixedRate(this::sendPing, 0, 29, TimeUnit.SECONDS);// 仅发送msg报文scheduler.scheduleAtFixedRate(this::sendMSG, 0, 32, TimeUnit.SECONDS);

设置发送周期为32s,输出结果如下: 

WebSocket 客户端已启动
客户端:已连接到服务端,URI: ws://10.10.10.10:8080/mq,1753430236546
请输入消息(输入 'ping' 发送 PING 帧,'exit' 退出):
客户端:消息发送成功: 1753430236548
客户端:发生错误: Connection Idle Timeout,1753430266587
org.eclipse.jetty.websocket.api.exceptions.WebSocketTimeoutException: Connection Idle Timeout...
Caused by: org.eclipse.jetty.websocket.core.exception.WebSocketTimeoutException: Connection Idle Timeout... 10 more
Caused by: java.util.concurrent.TimeoutException: Idle timeout expired: 30011/30000 msat org.eclipse.jetty.io.IdleTimeout.checkIdleTimeout(IdleTimeout.java:167)... 7 more
客户端:连接关闭,状态码: 1001, 原因: Connection Idle Timeout,30042
WebSocket 客户端已停止

问题同上,因为发送周期为32s,超过了30s,所以会报超时的错误。

接下来把发送周期改小为29s,看下是什么情况:

scheduler.scheduleAtFixedRate(this::sendMSG, 0, 29, TimeUnit.SECONDS);

输出日志:

WebSocket 客户端已启动
客户端:已连接到服务端,URI: ws://100.10.10.10:8080/mq,1753430446727
请输入消息(输入 'ping' 发送 PING 帧,'exit' 退出):
客户端:消息发送成功: 1753430446728
客户端:消息发送成功: 1753430475742
客户端:消息发送成功: 1753430504734
客户端:消息发送成功: 1753430533728
客户端:消息发送成功: 1753430562739
客户端:消息发送成功: 1753430591733
客户端:消息发送成功: 1753430620732
客户端:消息发送成功: 1753430649737
客户端:消息发送成功: 1753430678739

周期性地发送msg报文,连接也正常,没有出现超时的问题。

通过上面几个日志的对比,可以得出如下结论:

1、验证了默认30s超时的问题。

2、 只要在30s内周期性地发送报文,无论是ping还是msg,都可以避免超时的问题。

解决方案:

        经过以上的分析验证,现在已经明确了,问题就出在客户端的空闲超时上。解决方案有三种:

        1、设置客户端的空闲超时时间为一个很大的数;这个方法不现实,同时,因为angular上没相应的接口,所以也没法实现。

        2、客户端周期发送ping报文,间隔需要小于客户端的空闲超时时间;

        3、客户端周期发送msg报文,间隔需要小于客户端的空闲超时时间;实际上,msg报文是根据业务产生的,不具有周期性。java侧是将ping报文和msg报文分开的,但angular没有提供发送ping报文的单独接口,所以发送ping报文实际也是通过发送msg报文的接口发送出去的。

        4、服务端周期性的发送ping报文,间隔需要小于客户端的空闲超时时间;这个方法有个问题,服务端需要对每个新的请求都建一个定时的timer,并在每个请求断开时,取消对应的timer,这就增加了服务端管理session的复杂度。因此,我不建议采用这种方法。

        所以,比较好的解决办法是,angular需要周期性的发送ping报文,间隔时间需要小于空闲超时时间。

        但在文章开头,我们已经介绍了,前端实际上是有实现周期发送ping报文的,间隔周期设置的是30s。这个30s,就是问题的根源,因为是超时的临界值,稍不注意就超时了,可以用前面介绍的java客户端做测试,将周期改为30s,就会出现超时的情况。

        基于此,本文提出的问题也好解决,将超时时间改为30s以内即可,为了保险起见可以改为15s。

        经测试,问题解决,完结,撒花。

http://www.lryc.cn/news/599933.html

相关文章:

  • 从kHz到GHz:晶振频率范围如何决定其应用场景
  • streamyfin(世博会)android 编译
  • 告别虚函数性能焦虑:深入剖析C++多态的现代设计模式
  • 萤石云替代产品摄像头方案萤石云不支持TCP本地连接-东方仙盟
  • 蓝光中的愧疚
  • Nacos-服务注册,服务发现(一)
  • 中级统计师-经济学基础知识-第七章 失业与通货膨胀理论
  • 怎么放大单片机输出电流
  • linux C — udp,tcp通信
  • 【硬件】LT3763中文手册
  • 51 单片机单文件多文件结构工程模板的创建教程
  • Nginx 安全加固:如何阻止 IP 直接访问,只允许域名访问
  • Linux网络配置全攻略:IP、路由与双机通信
  • freqtrade关于获取k线数量,以及显示时间的问题
  • JAVA知识点(六):性能调优与线上问题排查
  • Day 3: 机器学习进阶算法与集成学习
  • 【13】C# 窗体应用WinForm——.NET Framework、WinForm、工程创建、工具箱简介、窗体属性及创建
  • [ComfyUI] -入门2- 小白零基础搭建ComfyUI图像生成环境教程
  • 语义分割-FCN-听课记录
  • vue使用xlsx库导出excel
  • 零基础-动手学深度学习-6.1 从全连接层到卷积
  • 【高等数学】第五章 定积分——第四节 反常积分
  • DuoPlus云手机再上新:统一配置品牌型号、代理分组与便捷搜索功能全面提升!
  • zabbix服务自动发现、自动注册及配置钉钉告警(小白的“升级打怪”成长之路)
  • 2025年第四届创新杯(原钉钉杯)赛题浅析-助攻快速选题
  • Keepalived 原理及配置(高可用)
  • 构建跨平台远程医疗系统中的视频通路技术方案探究
  • 关于PGCE专家技术认证解决方案
  • Colab中如何临时使用udocker(以MinIO为例)
  • 5G基站信号加速器!AD8021ARZ-REEL7亚德诺 超低噪声高速电压放大器 专利失真消除技术!