Skip to content

WebSocket

1.1 介绍

WebSocket是基于TCP的一种新的网络协议,它实现了浏览器与服务器之间的全双工通信—浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

1.2 应用场景

需要长连接、全双工通信的场景:

  • 视频弹幕
  • 网页聊天
  • 体育实况更新
  • 股票基金报价实时更新

你好,Ethan!既然你已经掌握了 Spring Task(单向推送/定时调度)和 SQL 批量处理,学习 WebSocket 会非常顺感,因为它是解决“实时性”问题的终极方案。

在传统的 HTTP 协议中,服务器像是个“被动”的柜员,只有你问他(发送请求),他才回答(返回响应)。而 WebSocket 建立了一个全双工(Full-duplex)**的持久连接,服务器一旦发现订单超时,可以**主动把消息“拍”到前端屏幕上,而不需要前端不停地轮询。


1.3 WebSocket 的核心流程

WebSocket 的生命周期分为三个阶段:

  1. 握手(Handshake):客户端通过 HTTP 发起申请,请求升级协议。
  2. 连接(Established):一旦升级成功,HTTP 连接关闭,取而代之的是一个持久的 TCP 通道。
  3. 双向传输:双方可以随时发送数据,不需要再携带繁琐的 HTTP Header。

1.4 在 Spring Boot 中快速上手

在 Spring Boot 中使用 WebSocket,通常需要三步走:

第一步:引入依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

第二步:编写配置类

你需要定义一个 ServerEndpointExporter,它会自动注册你编写的 WebSocket 端点。

java
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

第三步:编写端点服务(核心类)

使用 @ServerEndpoint 注解定义连接地址,并处理连接、关闭和消息逻辑。

java
@Component
@ServerEndpoint("/ws/{clientId}") // 路径参数,比如订单系统的客户ID
@Slf4j
public class WebSocketServer {

    // 用来存放所有在线客户端的会话,注意要用线程安全的 Map
    private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session, @PathParam("clientId") String clientId) {
        log.info("客户端连接成功: {}", clientId);
        sessionMap.put(clientId, session);
    }

    @OnClose
    public void onClose(@PathParam("clientId") String clientId) {
        log.info("客户端断开连接: {}", clientId);
        sessionMap.remove(clientId);
    }

    /**
     * 服务器向指定客户端推送消息
     */
    public void sendToClient(String clientId, String message) {
        Session session = sessionMap.get(clientId);
        if (session != null) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                log.error("推送到客户端 {} 失败", clientId, e);
            }
        }
    }
}

1.5 联动实战:Spring Task + WebSocket

Ethan,结合你刚才写的“超时订单取消”任务,完美的实时交互应该是这样的:

当你的 Spring Task 扫描到订单超时并取消后,立刻通过 WebSocket 给前端发个弹窗提醒,而不是等用户刷新页面。

java
@Scheduled(cron = "0 * * * * ?")
public void rejectTimeoutOrder() {
    List<Long> orderIds = ordersMapper.getTimeoutOrder();
    if (!CollectionUtils.isEmpty(orderIds)) {
        // 1. 批量修改数据库状态
        ordersMapper.updateBatch(orderIds, 6, "超时自动取消");

        // 2. 实时通知用户(假设我们有 userId)
        for (Long id : orderIds) {
            String userId = getUserIdByOrderId(id); // 伪代码
            webSocketServer.sendToClient(userId, "您的订单 " + id + " 因超时已自动取消");
        }
    }
}

1.6 关键避坑指南(Ethan 必读)

  1. Bean 注入问题

    由于 WebSocket 的 Endpoint 是由容器自动创建的(多例模式),如果你在 WebSocketServer 里直接 @Autowired 一个 Mapper,可能会发现它是 null

    • 解决:通常使用 static 变量注入,或者通过 ApplicationContext 手动获取。
  2. 集群/分布式问题

    如果你的应用部署了两台服务器 A 和 B。用户连在 A 上,但 Spring Task 在 B 上跑,B 是找不到 A 上的 Session 的。

    • 解决:这种情况下需要引入 Redis 的发布/订阅(Pub/Sub) 模式。B 发消息到 Redis,A 监听到后再推给用户。
  3. 心跳机制(Heartbeat)

    网络不稳定或防火墙可能会悄悄切断长连接。建议前端每隔 30 秒发一个 ping,后端回一个 pong,确保连接还活着。


1.7 总结

维度HTTP 轮询WebSocket
实时性延迟取决于轮询间隔准实时
服务器压力高(频繁建立连接、发送 Header)低(连接后只传数据)
适用场景报表刷新、低频更新聊天、实时弹窗、股票走势