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

发布于:2025-07-27 ⋅ 阅读:(12) ⋅ 点赞:(0)

背景:

        前端: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;

    @OnWebSocketOpen
    public 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();
    }

    @OnWebSocketMessage
    public void onMessage(Session session, String message) {
        System.out.println("客户端:收到服务端消息: " + message);
    }

    @OnWebSocketFrame
    public void onFrame(Session session, Frame frame, Callback cb) {
        System.out.println("客户端:收到帧,操作码: " + frame.getOpCode());
        if (frame.getOpCode() == OpCode.PONG) {
            System.out.println("客户端:收到 PONG 帧,负载: " + frame.getPayload());
        }
    }

    @OnWebSocketClose
    public void onClose(Session session, int statusCode, String reason) {
        this.session = null;
        System.out.println("客户端:连接关闭,状态码: " + statusCode + ", 原因: " + reason + "," + (System.currentTimeMillis() - start));
        closeLatch.countDown();
    }

    @OnWebSocketError
    public 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 Timeout
	at 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 ms
	at 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 ms
	at 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 ms
	at 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 ms
	at 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。

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


网站公告

今日签到

点亮在社区的每一天
去签到