【基于 Spring Boot 的 WebSocket 心跳监测和自动重连机制实现详解】
● WebSocket 最初由 RFC 6455 标准化,运行于 TCP 之上。● 客户端发起一次 HTTP/1.1 升级(Upgrade)请求,服务器同意后,协议从 HTTP 切换到 WebSocket。● 双方在握手完成后可互发“帧”(Frame),常见的有 Text、Binary、Ping、Pong、Close 等类型。@Override@Override//收到 ping 返回 pongr
基于 Spring Boot 的 WebSocket 心跳监测和自动重连机制实现详解
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
如果你对文中内容有任何想法或疑问,欢迎留言交流~ 😊
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)