WebSocketJava后端实时通信的关键技术
WebSocket——Java后端实时通信的关键技术
1. 简介
传统Web应用中使用HTTP 请求-响应模型:客户端通过一次请求向服务器索要数据,服务器处理后返回结果,连接随后立即关闭。这种数据数据交互方式在需要实时更新的场景中有些力不从心。
- 在线聊天系统需要即时同步消息;
- 在线教育平台需要实时显示答题状态;
- 协作文档要求毫秒级更新;
- 直播弹幕需要低延迟推送;
这些需求都远远超出了 HTTP 请求-响应模式的能力边界。
2. 轮询
在ws之前,用轮询(Polling)“模拟”实时推送。客户端周期性地发送 Ajax 请求向服务器询问是否有新数据,如果没有就等待下一次请求。这种方式实现简单,但效率低。无论服务器是否有新数据,客户端都会持续发起请求,造成大量无效流量,还增加了服务器负载。
长轮询:服务器在收到请求后保持阻塞直到有新数据或超时再返回响应,虽然减少了请求次数,但连接仍然会频繁重建,依然浪费资源。
3. WebSocket
WebSocket 协议通过一次 HTTP 握手,在客户端和服务器之间建立一条持久化的 TCP 连接,握手完成后,双方可以在这条连接上自由地互发消息。服务器可以主动推送数据给客户端,而不再需要依赖客户端请求,避免了轮询造成的性能浪费。
3.1 ws的原理与握手机制
WebSocket 协议在设计上与 HTTP 兼容,使用 HTTP 完成最初的握手,以便顺利穿越各种中间层设备。
- 客户端在发起连接时,会向服务器发送一个 HTTP 请求,请求头中包含几个关键字段:
Upgrade: websocket
、Connection: Upgrade
、Sec-WebSocket-Key
和Sec-WebSocket-Version
。 - 服务器在确认支持 WebSocket 后,会返回
101 Switching Protocols
状态码,并在响应头中附带经过加密计算的Sec-WebSocket-Accept
字段,表示同意协议升级。 - 握手完成后,HTTP 协议升级为 WebSocket 协议,底层 TCP 连接保持不断开,后续所有通信都基于这一条长连接完成,数据以帧(Frame)的形式传输,协议开销小,延迟低。
3.2 基于 Spring Boot 的 WebSocket 实现
Spring Boot 提供了非常成熟的 WebSocket 支持,可以快速集成到现有项目中,而无需自己处理协议升级、帧解析等复杂细节。下面以“多人聊天室”为例,展示如何在 Spring Boot 项目中实现基于 WebSocket 的实时消息推送。
Step1: 在 pom.xml
中引入 Spring WebSocket 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
Step2:启用 WebSocket 功能,配置 WebSocket 的端点路径
// 此配置类声明了一个 WebSocket 端点 "/ws/chat",允许任意来源访问
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatWebSocketHandler(), "/ws/chat")
.setAllowedOrigins("*");
}
}
Step3:实现核心的 WebSocket 处理器 ChatWebSocketHandler
@Slf4j
public class ChatWebSocketHandler extends TextWebSocketHandler {
/**
* 使用线程安全的 `ConcurrentHashMap` 来存储所有活跃会话,确保在高并发场景下不会出现数据不一致
*/
private static final Set<WebSocketSession> SESSIONS =
Collections.newSetFromMap(new ConcurrentHashMap<>(128));
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 每当有新用户连接时,将其 WebSocketSession 加入集合
SESSIONS.add(session);
log.info("新客户端连接成功,当前在线人数:{}", SESSIONS.size());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
String payload = message.getPayload();
log.info("收到客户端消息:{}", payload);
// 收到消息时,将消息广播给所有在线用户
for (WebSocketSession webSocketSession : SESSIONS) {
if (webSocketSession.isOpen()) {
webSocketSession.sendMessage(new TextMessage(payload));
}
}
}
/*
* 连接关闭或异常发生时,及时移除会话,避免资源泄漏。
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
SESSIONS.remove(session);
log.info("客户端断开连接,当前在线人数:{}", SESSIONS.size());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
log.error("WebSocket 发生异常,关闭连接", exception);
try {
session.close();
} catch (IOException e) {
log.error("关闭异常连接失败", e);
}
}
}
3.3 消息推送优化与分布式支持
单实例下,直接在内存中维护会话集合就可以了。实际使用时,WebSocket 服务通常会部署在多个节点上,如果不做处理,用户 A 连接到实例 1,用户 B 连接到实例 2,那用户 A 发的消息无法直接推送给用户 B。
解决这个问题的常见做法是引入 Redis 发布订阅(Pub/Sub) 机制,将消息在集群内部广播。当用户 A 在实例 1 上发送消息时,实例 1 将消息发布到 Redis 频道,所有订阅该频道的 WebSocket 服务节点都能收到通知,从而在各自的节点上推送给本地连接的客户端。
@Component
@RequiredArgsConstructor
@Slf4j
public class RedisMessageSubscriber {
private final StringRedisTemplate stringRedisTemplate;
/**
* 发布消息到 Redis 频道
*/
public void publish(String channel, String message) {
stringRedisTemplate.convertAndSend(channel, message);
}
/**
* 订阅 Redis 消息
*/
@Bean
public RedisMessageListenerContainer listenerContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(stringRedisTemplate.getConnectionFactory());
container.addMessageListener((message, pattern) -> {
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
log.info("收到 Redis 广播消息:{}", msg);
// 转发到当前节点所有 WebSocket 客户端
ChatWebSocketHandler.broadcast(msg);
}, new PatternTopic("chat-room"));
return container;
}
}
这种方式下,WebSocket 服务可以水平扩展,任何节点的消息都能同步到所有客户端。
3.4 心跳检测与断线重连
WebSocket 长连接面临的一大挑战是网络不稳定。为了防止连接在无感知的情况下被中间设备断开,需要实现心跳检测。
在客户端,可以定期发送一个轻量的 ping
消息,如果长时间没有收到服务器的 pong
响应,就自动发起重连;在服务端,同样可以主动检查空闲连接的活跃状态,如果超过设定时间没有收到消息,则关闭会话释放资源。