目录

WebSocketJava后端实时通信的关键技术

WebSocket——Java后端实时通信的关键技术

https://i-blog.csdnimg.cn/direct/4779332a57cf4983ab06e802c578a2ef.gif#pic_center

1. 简介

传统Web应用中使用HTTP 请求-响应模型:客户端通过一次请求向服务器索要数据,服务器处理后返回结果,连接随后立即关闭。这种数据数据交互方式在需要实时更新的场景中有些力不从心。

  • 在线聊天系统需要即时同步消息;
  • 在线教育平台需要实时显示答题状态;
  • 协作文档要求毫秒级更新;
  • 直播弹幕需要低延迟推送;

这些需求都远远超出了 HTTP 请求-响应模式的能力边界。

2. 轮询

在ws之前,用轮询(Polling)“模拟”实时推送。客户端周期性地发送 Ajax 请求向服务器询问是否有新数据,如果没有就等待下一次请求。这种方式实现简单,但效率低。无论服务器是否有新数据,客户端都会持续发起请求,造成大量无效流量,还增加了服务器负载。

长轮询:服务器在收到请求后保持阻塞直到有新数据或超时再返回响应,虽然减少了请求次数,但连接仍然会频繁重建,依然浪费资源。

3. WebSocket

WebSocket 协议通过一次 HTTP 握手,在客户端和服务器之间建立一条持久化的 TCP 连接,握手完成后,双方可以在这条连接上自由地互发消息。服务器可以主动推送数据给客户端,而不再需要依赖客户端请求,避免了轮询造成的性能浪费。

3.1 ws的原理与握手机制

WebSocket 协议在设计上与 HTTP 兼容,使用 HTTP 完成最初的握手,以便顺利穿越各种中间层设备。

  • 客户端在发起连接时,会向服务器发送一个 HTTP 请求,请求头中包含几个关键字段: Upgrade: websocketConnection: UpgradeSec-WebSocket-KeySec-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 响应,就自动发起重连;在服务端,同样可以主动检查空闲连接的活跃状态,如果超过设定时间没有收到消息,则关闭会话释放资源。