WebSocket 从零到能跑:Java 开发者版

目录

[一、WebSocket 到底解决什么问题?](#一、WebSocket 到底解决什么问题?)

[HTTP 的痛点](#HTTP 的痛点)

[WebSocket 的解法](#WebSocket 的解法)

一句话理解

[二、核心概念(5 分钟搞懂)](#二、核心概念(5 分钟搞懂))

[1. 连接过程(握手)](#1. 连接过程(握手))

[2. WebSocket vs HTTP](#2. WebSocket vs HTTP)

[3. URL 格式](#3. URL 格式)

[三、Spring Boot + WebSocket 完整实战](#三、Spring Boot + WebSocket 完整实战)

做什么

项目结构

第一步:创建项目(pom.xml)

第二步:启动类

[第三步:WebSocket 配置(告诉 Spring 要用 WebSocket)](#第三步:WebSocket 配置(告诉 Spring 要用 WebSocket))

[第四步:WebSocket 处理器(核心业务逻辑)](#第四步:WebSocket 处理器(核心业务逻辑))

[第五步:REST 接口(服务器主动推送消息的入口)](#第五步:REST 接口(服务器主动推送消息的入口))

第六步:前端页面(index.html)

第七步:配置文件

四、启动和测试

启动

测试

五、核心概念速查

[WebSocket 连接状态(readyState)](#WebSocket 连接状态(readyState))

[WebSocket 的三种消息类型](#WebSocket 的三种消息类型)

线程安全

六、实际项目中的用法

[场景1:MES 看板实时推送传感器数据](#场景1:MES 看板实时推送传感器数据)

场景2:生产任务进度实时推送

七、生产环境注意事项

[1. 心保活(防止连接断开)](#1. 心保活(防止连接断开))

[2. 连接数限制](#2. 连接数限制)

[3. 鉴权(连接时验证 Token)](#3. 鉴权(连接时验证 Token))

[4. 关闭时清理资源](#4. 关闭时清理资源)

八、常见坑和解决方案

九、总结

适用人群:会 Java + Spring Boot,但没用过 WebSocket 的开发者 目标:看完后能独立在 Spring Boot 项目里加 WebSocket 实时推送功能


一、WebSocket 到底解决什么问题?

HTTP 的痛点

复制代码
场景:牧原 MES 看板,需要实时显示传感器温度
​
用 HTTP 轮询(笨办法):
  看板页面每隔 2 秒问一次服务器:"有新数据吗?"
  → 服务器:没有
  → 看板页面:有新数据吗?
  → 服务器:没有
  → 看板页面:有新数据吗?
  → 服务器:有!温度 28.5℃
  → 看板页面:有新数据吗?(99% 的时候都是"没有")
  → ...无限循环
​
问题:
  ❌ 浪费带宽(99% 的请求是无效的)
  ❌ 浪费服务器资源(每秒处理几百个"有没有新数据"的请求)
  ❌ 有延迟(数据来了,最多等 2 秒才能看到)

WebSocket 的解法

复制代码
用 WebSocket(聪明办法):
  看板页面和服务器建立一条"管道"
  服务器有新数据时,直接顺着管道推给看板
  → 看板不用问,数据来了直接显示
​
优点:
  ✅ 实时(数据产生后毫秒级推送)
  ✅ 省资源(不用反复建立连接)
  ✅ 双向(客户端也能主动发消息给服务器)

一句话理解

HTTP = 你去食堂打饭,每次排队点菜拿完走人 WebSocket = 你和食堂窗口开了根管子,厨师炒好菜直接顺着管子滑过来


二、核心概念(5 分钟搞懂)

1. 连接过程(握手)

复制代码
客户端                          服务器
  |                               |
  |--- HTTP 请求(带 Upgrade)--->|  ← "我想升级成 WebSocket"
  |                               |
  |<--- 101 Switching Protocols --|  ← "好的,升级成功"
  |                               |
  |=== WebSocket 连接建立 ========|  ← 现在是双向管道了
  |                               |
  |<== 随时可以互相发消息 ========>|  ← 全双工通信

2. WebSocket vs HTTP

HTTP WebSocket
连接 短连接,用完就断 长连接,一直保持
通信 单向(客户端请求→服务器响应) 双向(随时互推)
数据格式 每次都要带 Header(冗余) 只发数据,不带 Header(精简)
服务器 不能主动找客户端 可以主动推消息给客户端

3. URL 格式

复制代码
HTTP:  http://localhost:8080/api/data
WebSocket: ws://localhost:8080/ws/chat
HTTPS 的 WebSocket: wss://localhost:8080/ws/chat

三、Spring Boot + WebSocket 完整实战

做什么

做一个实时消息推送服务:服务器可以随时推消息给所有连接的客户端。

  • 场景1:MES 看板实时显示传感器数据

  • 场景2:系统告警实时推送

项目结构

复制代码
websocket-demo/
├── pom.xml
├── src/main/java/com/example/websocketdemo/
│   ├── WebSocketDemoApplication.java    # 启动类
│   ├── config/
│   │   └── WebSocketConfig.java         # WebSocket 配置
│   ├── controller/
│   │   └── MessageController.java       # REST 接口(测试用)
│   └── handler/
│       └── ChatWebSocketHandler.java    # WebSocket 处理器
└── src/main/resources/
    ├── application.yml
    └── static/
        └── index.html                   # 前端页面

第一步:创建项目(pom.xml)

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
​
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
    </parent>
​
    <groupId>com.example</groupId>
    <artifactId>websocket-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>websocket-demo</name>
    <description>WebSocket 入门演示</description>
​
    <properties>
        <java.version>17</java.version>
    </properties>
​
    <dependencies>
        <!-- Spring Boot Web(内嵌 Tomcat) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <!-- ★ 核心:Spring Boot WebSocket 支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
​
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

第二步:启动类

java 复制代码
package com.example.websocketdemo;
​
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
​
@SpringBootApplication
public class WebSocketDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebSocketDemoApplication.class, args);
        System.out.println("========================================");
        System.out.println("  WebSocket 演示项目启动成功!");
        System.out.println("  访问: http://localhost:8080/index.html");
        System.out.println("========================================");
    }
}

第三步:WebSocket 配置(告诉 Spring 要用 WebSocket)

java 复制代码
package com.example.websocketdemo.config;
​
import com.example.websocketdemo.handler.ChatWebSocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
​
/**
 * WebSocket 配置类
 *
 * 这个类告诉 Spring:
 * 1. 启用 WebSocket 功能(@EnableWebSocket)
 * 2. 注册 WebSocket 处理器,绑定 URL 路径
 */
@Configuration
@EnableWebSocket  // ← 开启 WebSocket 功能
public class WebSocketConfig implements WebSocketConfigurer {
​
    private final ChatWebSocketHandler chatWebSocketHandler;
​
    // Spring 自动注入处理器
    public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler) {
        this.chatWebSocketHandler = chatWebSocketHandler;
    }
​
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatWebSocketHandler, "/ws/chat")  // ← 绑定路径
                .setAllowedOrigins("*");  // ← 允许所有来源连接(开发用)
        // 生产环境要限制来源,比如:
        // .setAllowedOrigins("https://mes.example.com")
    }
}

这一步做了什么?

复制代码
当浏览器请求 ws://localhost:8080/ws/chat 时
→ Tomcat 把请求交给 ChatWebSocketHandler 处理
→ 不是交给 Controller(Controller 只处理 HTTP 请求)

第四步:WebSocket 处理器(核心业务逻辑)

java 复制代码
package com.example.websocketdemo.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * WebSocket 消息处理器
 *
 * 继承 TextWebSocketHandler → 只处理文本消息
 * 如果要处理二进制消息,继承 BinaryWebSocketHandler
 */
@Slf4j
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {

    /**
     * 在线用户列表
     * key = 用户ID(用 Session ID 代替)
     * value = WebSocketSession(连接对象)
     *
     * 用 ConcurrentHashMap 保证线程安全(WebSocket 是多线程的!)
     */
    private final Map<String, WebSocketSession> onlineUsers = new ConcurrentHashMap<>();

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");

    // ==================== 核心回调方法 ====================

    /**
     * ① 连接建立时调用
     * 当浏览器 new WebSocket("ws://...") 成功连接后触发
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String sessionId = session.getId();
        onlineUsers.put(sessionId, session);

        log.info("🟢 新连接: sessionId={}, 在线人数={}", sessionId, onlineUsers.size());

        // 给新连接的客户端发欢迎消息
        String welcomeMsg = String.format(
                "{\"type\":\"system\",\"content\":\"欢迎加入!你是第%d位在线用户\",\"time\":\"%s\"}",
                onlineUsers.size(),
                LocalDateTime.now().format(FORMATTER)
        );
        session.sendMessage(new TextMessage(welcomeMsg));

        // 通知所有人有新用户加入
        broadcast("{\"type\":\"system\",\"content\":\"用户" + sessionId.substring(0, 6) + "加入了聊天\",\"time\":\""
                + LocalDateTime.now().format(FORMATTER) + "\"}", session);
    }

    /**
     * ② 收到消息时调用
     * 当浏览器发送 ws.send("xxx") 后触发
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();  // 获取消息内容
        String sessionId = session.getId();

        log.info("📩 收到消息: from={}, msg={}", sessionId, payload);

        // 构建广播消息(带发送者信息和时间)
        String broadcastMsg = String.format(
                "{\"type\":\"chat\",\"sender\":\"%s\",\"content\":\"%s\",\"time\":\"%s\"}",
                sessionId.substring(0, 6),  // 用 Session ID 前6位当昵称
                payload,
                LocalDateTime.now().format(FORMATTER)
        );

        // 广播给所有人(包括发送者自己)
        broadcast(broadcastMsg, null);
    }

    /**
     * ③ 连接关闭时调用
     * 当浏览器关闭标签页或调用 ws.close() 后触发
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String sessionId = session.getId();
        onlineUsers.remove(sessionId);

        log.info("🔴 连接关闭: sessionId={}, 原因={}, 在线人数={}",
                sessionId, status, onlineUsers.size());

        // 通知其他人
        broadcast("{\"type\":\"system\",\"content\":\"用户" + sessionId.substring(0, 6) + "离开了\",\"time\":\""
                + LocalDateTime.now().format(FORMATTER) + "\"}", null);
    }

    /**
     * ④ 连接出错时调用
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        log.error("❌ 连接错误: sessionId={}", session.getId(), exception);
        onlineUsers.remove(session.getId());

        if (session.isOpen()) {
            session.close();
        }
    }

    // ==================== 工具方法 ====================

    /**
     * 广播消息给所有在线用户
     *
     * @param message       要发送的消息
     * @param excludeSession 要排除的会话(null=不排除任何人)
     */
    public void broadcast(String message, WebSocketSession excludeSession) {
        TextMessage textMessage = new TextMessage(message);

        for (WebSocketSession session : onlineUsers.values()) {
            // 跳过排除的会话
            if (excludeSession != null && excludeSession.getId().equals(session.getId())) {
                continue;
            }

            try {
                if (session.isOpen()) {
                    session.sendMessage(textMessage);
                }
            } catch (IOException e) {
                log.error("广播消息失败: sessionId={}", session.getId(), e);
            }
        }
    }

    /**
     * 发送消息给指定用户
     */
    public void sendToUser(String sessionId, String message) {
        WebSocketSession session = onlineUsers.get(sessionId);
        if (session != null && session.isOpen()) {
            try {
                session.sendMessage(new TextMessage(message));
            } catch (IOException e) {
                log.error("发送消息失败: sessionId={}", sessionId, e);
            }
        }
    }

    /**
     * 获取在线用户数
     */
    public int getOnlineCount() {
        return onlineUsers.size();
    }
}

第五步:REST 接口(服务器主动推送消息的入口)

java 复制代码
package com.example.websocketdemo.controller;

import com.example.websocketdemo.handler.ChatWebSocketHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 消息推送 Controller
 *
 * 提供 HTTP 接口,让外部系统(比如 MES 数据采集模块)
 * 通过 HTTP 请求触发 WebSocket 推送
 *
 * 流程:
 * 传感器数据 → 数据采集服务 → POST /api/push → WebSocket 广播 → 看板实时显示
 */
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class MessageController {

    private final ChatWebSocketHandler chatWebSocketHandler;

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");

    /**
     * 推送消息给所有在线用户
     *
     * POST /api/push
     * Body: { "message": "温度超过阈值!" }
     *
     * 测试命令:
     * curl -X POST http://localhost:8080/api/push \
     *   -H "Content-Type: application/json" \
     *   -d '{"message":"温度超过阈值!当前温度 38.5℃"}'
     */
    @PostMapping("/push")
    public String pushMessage(@RequestBody java.util.Map<String, String> body) {
        String message = body.get("message");
        if (message == null || message.isBlank()) {
            return "消息不能为空";
        }

        // 构建推送消息
        String pushMsg = String.format(
                "{\"type\":\"alert\",\"content\":\"%s\",\"time\":\"%s\"}",
                message,
                LocalDateTime.now().format(FORMATTER)
        );

        // 通过 Handler 广播
        chatWebSocketHandler.broadcast(pushMsg, null);

        return "已推送: " + message;
    }

    /**
     * 模拟传感器数据推送
     *
     * GET /api/sensor-data
     *
     * 每次调用生成一条随机温度数据,广播给所有客户端
     * 测试命令:
     * curl http://localhost:8080/api/sensor-data
     */
    @GetMapping("/sensor-data")
    public String pushSensorData() {
        // 模拟传感器数据
        double temperature = 20 + Math.random() * 15;  // 20~35℃
        String farmId = "1001";
        String sensorId = "TEMP-001";

        String sensorMsg = String.format(
                "{\"type\":\"sensor\",\"farmId\":\"%s\",\"sensorId\":\"%s\",\"temperature\":%.1f,\"unit\":\"℃\",\"time\":\"%s\"}",
                farmId, sensorId, temperature, LocalDateTime.now().format(FORMATTER)
        );

        chatWebSocketHandler.broadcast(sensorMsg, null);

        return String.format("已推送传感器数据: 温度 %.1f℃", temperature);
    }

    /**
     * 获取在线用户数
     */
    @GetMapping("/online")
    public String getOnlineCount() {
        return "当前在线: " + chatWebSocketHandler.getOnlineCount() + " 人";
    }
}

第六步:前端页面(index.html)

把文件放在 src/main/resources/static/index.html,Spring Boot 会自动提供静态资源服务。

java 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>WebSocket 实时聊天室</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: "Microsoft YaHei", sans-serif; background: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column; }
        .header { background: #16213e; padding: 15px 20px; text-align: center; font-size: 18px; border-bottom: 2px solid #0f3460; }
        .header .status { font-size: 13px; color: #aaa; margin-top: 5px; }
        .header .status.connected { color: #4ecca3; }
        .header .status.disconnected { color: #e74c3c; }
        .messages { flex: 1; overflow-y: auto; padding: 15px; }
        .msg { margin: 8px 0; padding: 10px 15px; border-radius: 12px; max-width: 70%; }
        .msg.system { background: #2d3436; color: #81ecec; margin: 8px auto; text-align: center; font-size: 13px; border-radius: 20px; max-width: 80%; }
        .msg.chat { background: #0f3460; }
        .msg.alert { background: #6c2819; border-left: 4px solid #e74c3c; }
        .msg.sensor { background: #1b4332; border-left: 4px solid #4ecca3; }
        .msg .sender { font-size: 12px; color: #4ecca3; margin-bottom: 3px; }
        .msg .time { font-size: 11px; color: #888; margin-top: 3px; }
        .input-area { display: flex; padding: 15px; background: #16213e; gap: 10px; }
        .input-area input { flex: 1; padding: 12px; border: 1px solid #0f3460; border-radius: 8px; background: #1a1a2e; color: #eee; font-size: 15px; outline: none; }
        .input-area button { padding: 12px 24px; border: none; border-radius: 8px; background: #e94560; color: white; font-size: 15px; cursor: pointer; }
        .input-area button:hover { background: #c81d4e; }
    </style>
</head>
<body>

<div class="header">
    💬 WebSocket 实时聊天室
    <div class="status disconnected" id="status">未连接</div>
</div>

<div class="messages" id="messages"></div>

<div class="input-area">
    <input id="msgInput" placeholder="输入消息,按回车发送..." />
    <button onclick="sendMessage()">发送</button>
</div>

<script>
    // ==================== 1. 连接 WebSocket ====================
    // 地址格式:ws://主机:端口/配置的路径
    const ws = new WebSocket('ws://localhost:8080/ws/chat');

    // ==================== 2. 四个核心事件 ====================

    // 事件1:连接成功
    ws.onopen = function() {
        console.log('WebSocket 连接成功');
        updateStatus('已连接 ✓', true);
        appendMessage('system', '已连接到服务器');
    };

    // 事件2:收到消息(服务器推送过来的)
    ws.onmessage = function(event) {
        console.log('收到消息:', event.data);

        try {
            // 解析 JSON 消息
            const msg = JSON.parse(event.data);

            // 根据消息类型显示不同样式
            if (msg.type === 'system') {
                appendMessage('system', msg.content);
            } else if (msg.type === 'chat') {
                appendMessage('chat', msg.content, msg.sender, msg.time);
            } else if (msg.type === 'alert') {
                appendMessage('alert', '⚠️ ' + msg.content, '告警', msg.time);
            } else if (msg.type === 'sensor') {
                appendMessage('sensor',
                    `🌡️ ${msg.sensorId}: ${msg.temperature}${msg.unit} (场区: ${msg.farmId})`,
                    '传感器', msg.time);
            }
        } catch (e) {
            // 非 JSON 消息,直接显示
            appendMessage('chat', event.data);
        }
    };

    // 事件3:连接关闭
    ws.onclose = function(event) {
        console.log('WebSocket 连接关闭', event);
        updateStatus('已断开 ✗', false);
        appendMessage('system', '与服务器断开了连接');

        // 自动重连(3秒后尝试重连)
        setTimeout(function() {
            appendMessage('system', '正在尝试重连...');
            // 实际项目中应该重新创建 WebSocket 连接
        }, 3000);
    };

    // 事件4:连接出错
    ws.onerror = function(error) {
        console.error('WebSocket 出错:', error);
        updateStatus('连接出错 ✗', false);
    };

    // ==================== 3. 发送消息 ====================

    function sendMessage() {
        const input = document.getElementById('msgInput');
        const message = input.value.trim();

        if (!message) return;

        if (ws.readyState !== WebSocket.OPEN) {
            appendMessage('system', '❌ 连接未就绪,无法发送');
            return;
        }

        // ★ 核心:ws.send() 发送消息到服务器
        ws.send(message);

        // 清空输入框
        input.value = '';
    }

    // 回车发送
    document.getElementById('msgInput').addEventListener('keydown', function(e) {
        if (e.key === 'Enter') {
            sendMessage();
        }
    });

    // ==================== 4. UI 辅助函数 ====================

    function appendMessage(type, content, sender, time) {
        const div = document.getElementById('messages');
        const msgDiv = document.createElement('div');
        msgDiv.className = 'msg ' + type;

        if (type === 'system') {
            msgDiv.textContent = content;
        } else {
            let html = '';
            if (sender) html += '<div class="sender">' + sender + '</div>';
            html += '<div>' + content + '</div>';
            if (time) html += '<div class="time">' + time + '</div>';
            msgDiv.innerHTML = html;
        }

        div.appendChild(msgDiv);

        // 自动滚动到底部
        div.scrollTop = div.scrollHeight;
    }

    function updateStatus(text, connected) {
        const el = document.getElementById('status');
        el.textContent = text;
        el.className = 'status ' + (connected ? 'connected' : 'disconnected');
    }
</script>
</body>
</html>

第七步:配置文件

XML 复制代码
# application.yml
server:
  port: 8080

spring:
  application:
    name: websocket-demo

四、启动和测试

启动

复制代码
cd D:\桌面\websocket-demo
mvn spring-boot:run

或者用 IDEA 直接运行 WebSocketDemoApplication.main()

测试

方式1:浏览器测试(聊天室)

打开两个浏览器窗口,访问:

复制代码
http://localhost:8080/index.html

在任意一个窗口发消息,两个窗口都会收到。

方式2:用 curl 模拟服务器推送

复制代码
# 推送告警消息给所有在线用户
curl -X POST http://localhost:8080/api/push ^
  -H "Content-Type: application/json" ^
  -d "{\"message\":\"温度超过阈值!当前温度 38.5℃\"}"

# 模拟传感器数据推送
curl http://localhost:8080/api/sensor-data

# 查看在线人数
curl http://localhost:8080/api/online

方式3:用 IDEA 的 HTTP Client 测试

复制代码
POST http://localhost:8080/api/push
Content-Type: application/json

{
  "message": "氨气浓度超标!请立即处理"
}

五、核心概念速查

WebSocket 连接状态(readyState)

复制代码
WebSocket.CONNECTING  = 0  // 正在连接
WebSocket.OPEN        = 1  // 已连接,可以收发消息 ← 你只能在这个状态 send()
WebSocket.CLOSING     = 2  // 正在关闭
WebSocket.CLOSED      = 3  // 已关闭

发送消息前必须检查状态:

java 复制代码
// Java 服务端
if (session.isOpen()) {
    session.sendMessage(new TextMessage("hello"));
}

// JavaScript 客户端
if (ws.readyState === WebSocket.OPEN) {
    ws.send("hello");
}

WebSocket 的三种消息类型

java 复制代码
// 1. 文本消息(最常用)
session.sendMessage(new TextMessage("hello"));

// 2. 二进制消息(传文件/图片)
session.sendMessage(new BinaryMessage(fileBytes));

// 3. Ping/Pong(心跳检测,Spring 自动处理)
// 你不用手动写,Spring WebSocket 默认每 30 秒发一次 Ping

线程安全

复制代码
⚠️ WebSocket 是多线程的!

多个客户端同时连接 → 多个线程同时调用你的 Handler
所以:
  ❌ 不能用 ArrayList 存在线用户 → 会并发修改异常
  ✅ 用 ConcurrentHashMap → 线程安全

  ❌ 不能直接操作 Session 而不判断状态
  ✅ 每次操作前 session.isOpen()

六、实际项目中的用法

场景1:MES 看板实时推送传感器数据

java 复制代码
传感器 → MQTT → 数据采集服务 → 存储到 TDengine
                                  ↓
                            检测到数据异常
                                  ↓
                         调用 WebSocketHandler.broadcast()
                                  ↓
                            看板实时显示告警
// 在你的 MES 项目中,SensorDataCollector 处理完数据后加一行:
@Service
@RequiredArgsConstructor
public class SensorDataCollector {

    private final ChatWebSocketHandler wsHandler;  // 注入 WebSocket 处理器

    public void onMessage(String topic, String payload) {
        // ... 处理传感器数据 ...

        if (filterResult.isAbnormal()) {
            // 异常数据 → 实时推送给看板
            String alertMsg = String.format(
                    "{\"type\":\"sensor_alert\",\"sensorId\":\"%s\",\"reason\":\"%s\",\"value\":%.1f}",
                    data.getSensorId(), filterResult.getReason(), data.getValue());
            wsHandler.broadcast(alertMsg, null);
        }
    }
}

场景2:生产任务进度实时推送

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductionTaskService {

    private final ChatWebSocketHandler wsHandler;

    public void updateTaskProgress(Long taskId, int progress) {
        // 更新数据库
        taskMapper.updateProgress(taskId, progress);

        // 推送给看板
        String msg = String.format(
                "{\"type\":\"task_progress\",\"taskId\":%d,\"progress\":%d}",
                taskId, progress);
        wsHandler.broadcast(msg, null);
    }
}

七、生产环境注意事项

1. 心保活(防止连接断开)

java 复制代码
// Spring Boot 已经默认启用了心跳检测(Ping/Pong)
// 在配置类中可以调整:
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(chatWebSocketHandler, "/ws/chat")
            .setAllowedOrigins("*");

    // 如果需要自定义,可以用注解配置
}

2. 连接数限制

java 复制代码
// 在配置中限制最大连接数
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatWebSocketHandler, "/ws/chat")
                .setAllowedOrigins("*");
    }

    // Tomcat 默认最大连接数可以通过 server.tomcat.max-connections 配置
}

3. 鉴权(连接时验证 Token)

java 复制代码
// 在 WebSocket 配置中添加拦截器
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatWebSocketHandler, "/ws/chat")
                .addInterceptors(new HttpSessionHandshakeInterceptor())  // 携带 HTTP Session
                .setAllowedOrigins("*");
    }
}

// 或者用自定义拦截器验证 JWT
@Component
public class JwtWebSocketInterceptor implements HandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler handler, Map<String, Object> attributes) {
        // 从 URL 参数或 Header 中获取 Token
        // 验证 Token 是否有效
        // 无效则 return false,拒绝连接
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                               WebSocketHandler handler, Exception ex) {
        // 握手完成后(可选)
    }
}

4. 关闭时清理资源

java 复制代码
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
    onlineUsers.remove(session.getId());
    // 关闭数据库连接、清理缓存等
}

八、常见坑和解决方案

问题 原因 解决方案
连接后立即断开 服务端没有正确处理握手 检查 @EnableWebSocket 和 Handler 注册
消息收不到 readyState 不是 OPEN 发送前检查 session.isOpen()
跨域连不上 CORS 限制 .setAllowedOrigins("*")
连接数爆了 没限制连接数 Tomcat max-connections 配置
服务器重启后客户端没重连 客户端没写重连逻辑 ws.onclose 里 setTimeout 重连
消息乱码 编码问题 确保用 TextMessage 发送 UTF-8 文本

九、总结

WebSocket 是全双工长连接协议,解决了 HTTP 轮询的延迟和资源浪费问题。在 Spring Boot 中,通过 @EnableWebSocket 开启,注册 WebSocketHandler 处理连接、消息、关闭三个核心事件。生产环境要注意线程安全(ConcurrentHashMap)、心跳保活、连接数限制和鉴权。我们在牧原 MES 项目中用 WebSocket 实现了传感器异常数据的实时推送,看板页面毫秒级收到告警。