Gitee 地址 && 源码下载:SpringBootWSHeartbeat

引言

在现代前后端分离的架构中,WebSocket 作为一种全双工、双向通信协议,被广泛用于实时消息推送、在线协作、游戏、金融行情等场景。相比于传统的 HTTP 请求-响应模式,WebSocket 可以在一次握手之后保持连接长驻,客户端与服务端可随时互发数据,极大地提升了实时性和效率。但长连接也带来一个问题:如何确保连接的存活?如何在网络中断或服务器重启时自动重连?本文将基于一个简单的 Spring Boot + Tomcat 服务端和基于 nv-websocket-client 库的 Java 客户端示例,深入讲解心跳监测与自动重连机制的实现思路。

实现目标

● 启动时自动连接 WebSocket 服务端
● 每隔 10 秒发送一次 Ping 心跳包,验证连接活性
● 若服务端 30 秒内无响应(未收到 Pong),主动断开连接
● 自动按照指数退避策略进行重连(最大不超过 60 秒)
● 日志记录连接状态、重连、心跳等关键事件

一、WebSocket 技术基础介绍

1. 协议概览

● WebSocket 最初由 RFC 6455 标准化,运行于 TCP 之上。
● 客户端发起一次 HTTP/1.1 升级(Upgrade)请求,服务器同意后,协议从 HTTP 切换到 WebSocket。
● 双方在握手完成后可互发“帧”(Frame),常见的有 Text、Binary、Ping、Pong、Close 等类型。

2. 常见的控制帧

● Ping/Pong:探测连接存活性
● Close:优雅地关闭连接,携带关闭码与原因

3. 与 HTTP 的区别

特性 HTTP WebSocket
连接类型 短连接 长连接
通信模式 请求-响应 双向随时通信

二、服务端 WebSocketServer 实现

使用 Spring Boot 自带的 WebSocket 支持,结合 Tomcat 容器,实现一个简单的“回声”服务器。

1.引入依赖

pom.xml

<dependencies>
    <!--Spring web-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>2.7.3</version>
    </dependency>
    <!--Spring Boot WebSocket-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
      <version>2.7.3</version>
    </dependency>
</dependencies>

2.定义消息处理器

EchoWebSocketHandler.java

public class EchoWebSocketHandler extends AbstractWebSocketHandler {
    private static final Logger log = LoggerFactory.getLogger(EchoWebSocketHandler.class);

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        log.info("New connection from {}", session.getRemoteAddress());
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        //收到 ping 返回 pong
        if (message instanceof PingMessage) {
            PingMessage ping = (PingMessage) message;
            System.out.println("Received Ping");
            log.debug("Received Ping, payload {} bytes - replying Pong", ping.getPayload().capacity());
            session.sendMessage(new PongMessage(ping.getPayload()));
            return;
        }
        // 收到 text 返回 text
        if (message instanceof TextMessage) {
            TextMessage text = (TextMessage) message;
            String payload = text.getPayload();
            log.info("Received text: {}", payload);
            session.sendMessage(new TextMessage("Server received: " + payload));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        log.warn("Connection {} closed: {}", session.getId(), status);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) {
        log.error("Transport error on session {}", session.getId(), exception);
    }
}

3.定义配置类

WebSocketConfig.java

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 1. 新建一个你的消息处理器实例
        WebSocketHandler handler = new EchoWebSocketHandler();
        // 2. 把这个 handler 注册到 “/ws” 这个端点上
        registry.addHandler(handler, "/ws")
                // 3. 允许所有来源的跨域访问(只是测试环境用,生产建议指定白名单)
                .setAllowedOrigins("*");
    }
}

4. Spring Boot 配置文件

application.properties

server.port=8081
logging.level.com.jason.wsserver.server=DEBUG

配置端口为 8081 ,输出日志等级为 DEBUG

三、客户端 WebSocketClient 实现

客户端选用 nv-websocket-client 库,通过 WebSocketFactory 建立连接,并实现心跳与自动重连。

1.引入依赖

pom.xml

<dependencies>
    <!--Spring Web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.7.3</version>
    </dependency>
    <!--WebSocket-->
    <dependency>
        <groupId>com.neovisionaries</groupId>
        <artifactId>nv-websocket-client</artifactId>
        <version>2.14</version>
    </dependency>
</dependencies>

2. 定义客户端组件

WebSocketClient.java

/**
 * WebSocketClient 是一个Spring管理的组件,实现了ApplicationRunner接口,
 * 在应用启动时自动执行run方法以连接WebSocket服务器。
 * 支持心跳检测、自动重连和手动断开。
 */
@Component
public class WebSocketClient implements ApplicationRunner {
    // SLF4J 日志记录器,用于打印运行时日志。
    private static final Logger log = LoggerFactory.getLogger(WebSocketClient.class);

    //客户端标识,可用于日志区分不同实例。
    private String clientId = "JaosnClient";
    //WebSocket服务器地址,格式: ws://host:port/path
    private String wsUrl =  "ws://localhost:8081/ws";

    // nv-websocket-client 创建的 WebSocket 连接实例。
    private WebSocket ws;

    //单线程调度器,用于调度心跳和重连任务。
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    //当前心跳任务的Future引用,用于后续取消。
    private volatile ScheduledFuture<?> heartbeatTask;

    //上次收到PONG帧的时间戳,初始为当前系统时间
    private long lastPongTime = System.currentTimeMillis();
    //最大重连延迟(秒),用于指数退避上限。
    private static final long MAX_RECONNECT_DELAY = 60;
    //当前重连延迟(秒),初始为2秒,重连失败后以指数方式增长。
    private long reconnectDelay = 2;

    //定义时间格式
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /**
     * Spring Boot启动后回调,触发首次连接。
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        connectWS();
    }

    /**
     * 连接websocket服务端并设置自动重连,如果连接失败,会触发重连逻辑。
     */
    public void connectWS() {
        try {
            log.info("clientId: {}, connecting to wsUrl: {}", clientId, wsUrl);
            // 创建WebSocket连接
            ws = new WebSocketFactory().setConnectionTimeout(5000).createSocket(wsUrl);
            //注册回调监视器
            ws.addListener(new WebSocketAdapter() {

                /**
                 * 连接成功回调,启动心跳并发送初始消息,重置重连延迟。
                 */
                @Override
                public void onConnected(WebSocket websocket, Map<String, List<String>> headers) {
                    log.info("{} WebSocket connected", clientId);
                    startHeartbeatTask();
                    ws.sendText("Hello, Server!");
                    reconnectDelay = 2;
                }

                /**
                 * 收到PONG帧回调,更新lastPongTime。
                 */
                @Override
                public void onPongFrame(WebSocket websocket, WebSocketFrame frame) {
                    lastPongTime = System.currentTimeMillis();
                    log.debug("{} Received PONG {}", clientId , LocalDateTime.now().format(formatter));
                }

                /**
                 * 收到文本消息回调,打印消息内容。
                 */
                @Override
                public void onTextMessage(WebSocket websocket, String text) {
                    log.info("{} websocketReceive: {}", clientId, text);
                }

                /**
                 * 连接断开回调,停止心跳并安排重连。
                 */
                @Override
                public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame,
                                           WebSocketFrame clientCloseFrame, boolean closedByServer) {
                    log.info("{} WebSocket disconnected", clientId);
                    stopHeartbeatTask();    //停止心跳
                    scheduleReconnect();    //安排重连
                }

                /**
                 * 出现错误回调,打印异常并尝试重连。
                 */
                @Override
                public void onError(WebSocket websocket, WebSocketException cause) {
                    log.error("{} WebSocket error", clientId, cause);
                }
            }).connect();   //发起连接

        } catch (Exception e) {
            // 连接创建失败时打印错误并安排重连
            log.error("{} connectWS connect error: {}", clientId, e.getMessage(), e);
            scheduleReconnect();
        }
    }

    /**
     * 启动心跳定时任务:每10秒发送一次PING,并检测PONG响应超时。
     */
    private void startHeartbeatTask() {
        //如果旧心跳任务未完成,则取消
        if (heartbeatTask != null && !heartbeatTask.isDone()) {
            heartbeatTask.cancel(true);
            log.info("{} 旧心跳任务取消", clientId);
        }

        log.info("{} 启动心跳定时任务", clientId);
        lastPongTime = System.currentTimeMillis();

        // 安排每10秒一次的定时任务
        heartbeatTask = scheduler.scheduleAtFixedRate(() -> {
            try {
                if (ws != null && ws.isOpen()) {
                    ws.sendPing();
                    //获取当前时间并格式化
                    String currentTime = LocalDateTime.now().format(formatter);

                    log.debug("{} Sent PING {}", clientId,currentTime);
                }
                // 检查距离上次PONG的空闲时间
                long idle = System.currentTimeMillis() - lastPongTime;
                log.debug("{} (自上次收到 PONG 响应): {}s", clientId, idle / 1000.0);
                // 超过30秒未响应则断开连接以触发重连
                if (idle > 30_000) {
                    log.warn("{} WebSocket连接超时 ({}ms) 未收到PONG,断开连接", clientId, idle);
                    if (ws != null) ws.disconnect();
                }
            } catch (Exception e) {
                log.error("{} 心跳任务异常", clientId, e);
            }
        }, 0, 10, TimeUnit.SECONDS);
    }

    /**
     * 停止心跳任务
     */
    private void stopHeartbeatTask() {
        if (heartbeatTask != null) {
            heartbeatTask.cancel(true);
            heartbeatTask = null;
            log.info("{} 心跳任务已停止", clientId);
        }
    }

    /**
     * 触发重连,延时重连策略,指数退避
     */
    private void scheduleReconnect() {
        long delay = Math.min(reconnectDelay, MAX_RECONNECT_DELAY);
        log.info("{} 将在 {} 秒后重连...", clientId, delay);
        scheduler.schedule(() -> {
            try {
                connectWS();
            } finally {
                reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
            }
        }, delay, TimeUnit.SECONDS);
    }

    /**
     * 主动关闭连接(可选)
     */
    public void disconnect() {
        try {
            if (ws != null) {
                ws.disconnect();
                stopHeartbeatTask();
                log.info("{} WebSocket主动关闭", clientId);
            }
        } catch (Exception e) {
            log.error("{} WebSocket关闭异常", clientId, e);
        }
    }

}

3.Spring boot 配置文件

application.properties

server.port=8080
logging.level.com.jason.wsclient.client=DEBUG

四、运行效果

1.启动应用后开启心跳监测后日志输出示例:

Client 启动心跳定时任务
Client Sent PING 2025-06-16 22:09:13
Client (自上次收到 PONG 响应): 0.007s
Client Received PONG 2025-06-16 22:09:13
Client websocketReceive: Server received: Hello, Server!
Client Sent PING 2025-06-16 22:09:23
Client (自上次收到 PONG 响应): 9.985s
Client Received PONG 2025-06-16 22:09:23
Client Sent PING 2025-06-16 22:09:33
Client (自上次收到 PONG 响应): 10.001s
Client Received PONG 2025-06-16

2.断开重连以后,进行重连日志输出示例:

Tomcat started on port(s): 8080 (http)
Started WebsocketClientApplication

clientId: Client, start connecting to wsUrl: ws://localhost:8081/ws  
Client WebSocket connected  
Client 启动心跳定时任务  
Client Sent PING 2025-06-16 22:30:00  
Client (自上次收到 PONG 响应): 0.005s  
Client Received PONG 2025-06-16 22:30:00  
Client websocketReceive: Server received: Hello, Server!

Client Sent PING 2025-06-16 22:30:10  
Client (自上次收到 PONG 响应): 9.997s  
Client Received PONG 2025-06-16 22:30:10  

Client WebSocket disconnected  
Client 心跳任务已停止  
Client 将在 2 秒后重连...

clientId: Client, start connecting to wsUrl: ws://localhost:8081/ws  
Client connectWS connect error: Failed to connect to 'localhost:8081': Connection refused: connect  
Client 将在 2 秒后重连...

clientId: Client, start connecting to wsUrl: ws://localhost:8081/ws  
Client connectWS connect error: Failed to connect to 'localhost:8081': Connection refused: connect  
Client 将在 4 秒后重连...

clientId: Client, start connecting to wsUrl: ws://localhost:8081/ws  
Client connectWS connect error: Failed to connect to 'localhost:8081': Connection refused: connect  
Client 将在 8 秒后重连...

clientId: Client, start connecting to wsUrl: ws://localhost:8081/ws  
Client connectWS connect error: Failed to connect to 'localhost:8081': Connection refused: connect  
Client 将在 16 秒后重连...

clientId: Client, start connecting to wsUrl: ws://localhost:8081/ws  
Client connectWS connect error: Failed to connect to 'localhost:8081': Connection refused: connect  
Client 将在 32 秒后重连...

五、📌附完整源码

可参考项目结构:
这是一个 Maven 多模块项目,包含两个子模块:

📁 WebSocket-Server(服务端模块)

src/
└── main/
├──── java/
│      └── com.jason.wsserver/
│          ├── config/
│          │    └── WebSocketConfig.java      
│          ├── server/
│          │    └── EchoWebSocketHandler.java 
│          └── WebsocketServerApplication.java
└── resources/
└──── application.properties 

📁 WebSocket-Client(客户端模块)

src/
└── main/
├──── java/
│      └── com.jason.wsclient/
│           ├── client/
│           │    └── WebSocketClient.java      
│           └── WebsocketClientApplication.java 
└── resources/
└──── application.properties           

如果你对文中内容有任何想法或疑问,欢迎留言交流~ 😊

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐