Spring Boot 后端实现 WebSocket

目录

依赖导入

[使用 @ServerEndpoint 创建 WebSocket 端点](#使用 @ServerEndpoint 创建 WebSocket 端点)

[WebSocket 生命周期方法](#WebSocket 生命周期方法)

[Session 管理(保存在线用户)](#Session 管理(保存在线用户))

群发与单发消息实现

实战练习


本文目标:能在 Spring Boot 中编写一个简单的 WebSocket 服务端

依赖导入

在 Spring Boot 项目中导入 WebSocket 依赖

Spring Boot 自带了对 WebSocket 的官方支持,只需添加一行依赖即可。

1. Maven 依赖

在 pom.xml 中加入:

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

这个 starter 会自动引入 spring-websocket 和 spring-messaging,能使用两种实现方式:

Javax 标准注解版(@ServerEndpoint)

Spring 原生配置版(WebSocketHandler)

2. 可选依赖(若用 STOMP 或 SockJS)

暂时不用,但以后可加:

XML 复制代码
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
</dependency>

3. 启动类要求

Spring Boot 启动类无需特别修改,只要正常启动即可。

例如:

java 复制代码
@SpringBootApplication
public class WebSocketDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebSocketDemoApplication.class, args);
    }
}

使用 @ServerEndpoint 创建 WebSocket 端点

这是最常见也最直观的写法,底层基于 Java EE 的 WebSocket 标准 API(JSR 356)

Spring Boot 会自动扫描带 @ServerEndpoint 注解的类并注册成 WebSocket 端点。

注册端点 = 服务器对外开放了一个「实时通信接口」,客户端能主动连上来,之后双方想发消息就发,不用反复建立连接。

没注册的类,服务器不知道它是一个WebSocket 通信接口,不会把客户端的 WebSocket 连接路由到这个类。

1. 启用 @ServerEndpoint 支持

因为 @ServerEndpoint 来自 Javax 包,需要在配置类中启用 WebSocket 支持。

在项目中新建配置类,例如:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    /**
     * 用于扫描和注册所有带 @ServerEndpoint 的 Bean
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

2. 创建一个简单的 WebSocket 服务端

在项目中新增一个类,例如:

java 复制代码
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;

@ServerEndpoint("/ws/chat")
@Component
public class ChatEndpoint {

    @OnOpen
    public void onOpen(Session session) {
        System.out.println("🟢 新连接建立:" + session.getId());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("📩 收到消息:" + message + "(来自 " + session.getId() + ")");
    }

    @OnClose
    public void onClose(Session session) {
        System.out.println("🔴 连接关闭:" + session.getId());
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        System.out.println("⚠️ 出现错误:" + session.getId());
        throwable.printStackTrace();
    }
}

3. 启动项目后访问

启动 Spring Boot 项目后,WebSocket 端点地址为:

bash 复制代码
ws://localhost:8080/ws/chat

现在可以用浏览器控制台或 VSCode WebSocket 插件连接这个地址来测试通信。

使用 const ws = new WebSocket('ws://localhost:8080/ws/chat');在浏览器测试

WebSocket 生命周期方法

WebSocket 端点类中的 4 个核心注解方法(@OnOpen、@OnMessage、@OnClose、@OnError)

控制了从连接建立到断开的整个生命周期。

(1)@OnOpen 客户端连接时触发

java 复制代码
@OnOpen
public void onOpen(Session session) {
    System.out.println("🟢 新连接建立:" + session.getId());
}

触发时机:

浏览器或客户端调用 new WebSocket("ws://...") 连接成功后立即触发

常用场景:

向客户端发送欢迎消息

把 Session 对象加入到在线用户集合中(后面群发要用)

记录在线人数或用户状态

(2)@OnMessage 收到消息时触发

java 复制代码
@OnMessage
public void onMessage(String message, Session session) {
    System.out.println("📩 收到消息:" + message + "(来自 " + session.getId() + ")");
}

触发时机:

当客户端通过 ws.send("xxx") 发送消息时触发。

常用场景:

打印或处理收到的消息

实现业务逻辑(如聊天转发、命令执行)

调用 session.getBasicRemote().sendText() 回复客户端

(3)@OnClose 连接关闭时触发

java 复制代码
@OnClose
public void onClose(Session session) {
    System.out.println("🔴 连接关闭:" + session.getId());
}

触发时机:

客户端主动调用 ws.close();

网络中断或异常断开

服务端主动关闭连接

常用场景:

从在线用户集合中移除该 Session

广播"某某离开聊天室"的系统通知

清理资源

(4)@OnError 通信出错时触发

java 复制代码
@OnError
public void onError(Session session, Throwable throwable) {
    System.out.println("⚠️ 出错:" + session.getId());
    throwable.printStackTrace();
}

触发时机:

网络异常、编码异常、消息格式错误等情况

常用场景:

打印日志定位问题

通知管理员或客户端错误信息

5. 方法触发顺序(生命周期流程图)

java 复制代码
客户端连接 → @OnOpen → 
客户端发送消息 → @OnMessage → 
客户端关闭连接 → @OnClose → 
过程中出错 → @OnError

每个 Session 代表一个独立的客户端连接

Session 管理(保存在线用户)

在 WebSocket 中,每当一个客户端连接建立,框架都会为它创建一个 Session 对象。

我们可以利用它来:

  1. 识别不同的用户;

  2. 发送私聊(单发)或群发消息;

  3. 统计在线人数;

  4. 在断开时清理资源。

1. 为什么要管理 Session?

假设你写了一个聊天室:

小明、小红、小刚 都连上了服务器;

当小明发消息时,服务器需要广播给所有在线用户;

这时服务器必须知道有哪些连接(Session)存在。

所以要用一个线程安全的集合来管理所有在线 Session。

2. 使用 ConcurrentHashMap 保存 Session

在 ChatEndpoint 中添加一个静态集合:

java 复制代码
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import jakarta.websocket.Session;

@ServerEndpoint("/ws/chat")
@Component
public class ChatEndpoint {

    // 保存所有在线用户
    private static final Map<String, Session> ONLINE_USERS = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session) {
        ONLINE_USERS.put(session.getId(), session);
        System.out.println("🟢 新连接:" + session.getId());
        broadcast("🔔 用户 " + session.getId() + " 加入聊天室!");
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("📩 来自 " + session.getId() + ":" + message);
        broadcast("💬 用户 " + session.getId() + " 说:" + message);
    }

    @OnClose
    public void onClose(Session session) {
        ONLINE_USERS.remove(session.getId());
        System.out.println("🔴 连接关闭:" + session.getId());
        broadcast("❌ 用户 " + session.getId() + " 离开聊天室。");
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        System.out.println("⚠️ 出错:" + session.getId());
        throwable.printStackTrace();
    }

    /** 群发消息 */
    private void broadcast(String message) {
        ONLINE_USERS.forEach((id, s) -> {
            try {
                s.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}
代码部分 说明
ConcurrentHashMap<String, Session> 线程安全地存储在线连接
ONLINE_USERS.put() / remove() 用户上线 / 离线管理
broadcast() 向所有在线 Session 群发消息
session.getBasicRemote().sendText() 向某一客户端发送消息

3. 运行结果预期

启动 Spring Boot 项目;

打开两个浏览器页面;

分别在 Console 执行:

java 复制代码
const ws = new WebSocket("ws://localhost:8080/ws/chat");
ws.onmessage = e => console.log(e.data);
ws.send("你好");

ws.onmessage = e => console.log(e.data);是后端发啥消息过来,前端就自动把消息打印到控制台,能看到别人发的内容。

会看到:

这说明着群发逻辑生效,所有客户端都能接收到消息。

群发与单发消息实现

1. 群发(广播消息)

前一节已经实现过基础群发函数 broadcast():

java 复制代码
private void broadcast(String message) {
    ONLINE_USERS.forEach((id, session) -> {
        try {
            session.getBasicRemote().sendText(message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}
参数 实际存储的内容 相当于
id session.getId() 用户的唯一标识(类似微信号)
s Session 对象 用户的连接通道(类似微信聊天窗口)

可以把 (id, s) -> { ... } 理解为:

"对于每一个(id, s)这样的配对,执行以下操作"

用途

向所有在线用户推送系统消息(如"XX 进入聊天室")

转发聊天内容给所有连接的客户端

触发示例

java 复制代码
@OnMessage
public void onMessage(String msg, Session session) {
    broadcast("💬 用户 " + session.getId() + " 说:" + msg);
}

2. 单发(私聊消息)

若每个用户登录后都有自己的唯一标识(例如用户名),可在连接时把它放入 URL 参数:

前端连接:

java 复制代码
const ws = new WebSocket("ws://localhost:8080/ws/chat?username=Tom");

后端解析并存储:

java 复制代码
@OnOpen
public void onOpen(Session session) {
    String username = session.getQueryString().split("=")[1];
    USER_MAP.put(username, session);
    broadcast("🔔 用户 " + username + " 加入聊天室");
}

实现私聊:

java 复制代码
public void sendToUser(String username, String message) {
    Session session = USER_MAP.get(username);
    if (session != null && session.isOpen()) {
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

调用示例:

java 复制代码
sendToUser("Tom", "这是一条私聊消息");

3. 群发 + 单发混合场景

实际聊天系统中会根据消息内容决定:

若前端发送的 JSON 包含目标用户:执行 sendToUser();

否则默认广播。

例如:

java 复制代码
@OnMessage
public void onMessage(String json, Session session) {
    JSONObject msg = new JSONObject(json);
    String toUser = msg.optString("to");
    String content = msg.optString("content");
    if (toUser.isEmpty()) {
        broadcast("💬 公聊:" + content);
    } else {
        sendToUser(toUser, "📨 私聊:" + content);
    }
}

JSONObject 就是把 JSON 字符串转换成后端能方便操作的键值对字典,比如前端发{"to":"Tom","content":"你好"},转成 JSONObject 后,能直接按to和content取对应值
optString:从这个字典里按指定的键(比如to/content)取字符串值,就算键不存在也不会报错,只会返回空字符串,比直接取值更安全

实战练习

1. 创建 WebSocket 服务端

步骤:

在 ChatEndpoint 中实现 连接建立(onOpen)、消息接收(onMessage)和 连接关闭(onClose)等方法。

使用 ConcurrentHashMap 存储在线用户并广播消息。

java 复制代码
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

@ServerEndpoint("/ws/chat")
@Component
public class ChatEndpoint {

    // 存储所有在线用户
    private static final Map<String, Session> ONLINE_USERS = new ConcurrentHashMap<>();

    // 连接建立时
    @OnOpen
    public void onOpen(Session session) {
        String username = session.getQueryString() != null ? session.getQueryString().split("=")[1] : session.getId();
        ONLINE_USERS.put(username, session);  // 将用户 session 放入集合
        System.out.println("🟢 用户 " + username + " 已加入聊天室");
        broadcast("🔔 用户 " + username + " 加入聊天室");
    }

    // 收到消息时
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("📩 收到消息:" + message);
        broadcast("💬 " + session.getId() + " 说:" + message);  // 广播给所有人
    }

    // 连接关闭时
    @OnClose
    public void onClose(Session session) {
        String username = getUsernameBySession(session);
        ONLINE_USERS.remove(username);  // 从集合中移除
        System.out.println("🔴 用户 " + username + " 离开聊天室");
        broadcast("❌ 用户 " + username + " 离开聊天室");
    }

    // 错误发生时
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.out.println("⚠️ 错误:" + throwable.getMessage());
        throwable.printStackTrace();
    }

    // 群发消息
    private void broadcast(String message) {
        ONLINE_USERS.forEach((username, session) -> {
            try {
                session.getBasicRemote().sendText(message);  // 向每个用户发送消息
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    // 根据 Session 获取用户名
    private String getUsernameBySession(Session session) {
        return ONLINE_USERS.entrySet().stream()
                .filter(entry -> entry.getValue().equals(session))
                .map(Map.Entry::getKey)
                .findFirst()
                .orElse(session.getId());
    }
}

getUsernameBySession 方法

从存储 "用户名 - 连接" 的对照表中,根据用户的连接(Session)反查对应的用户名;如果没找到,就用连接的唯一 ID 当用户名。

entrySet:把 "用户名 - 连接" 对照表(ONLINE_USERS)拆成一个个 "键值对"(比如 <Tom, 连接 1>、<Jack, 连接 2>),方便逐个检查

stream:开启流式处理,能像流水线一样一步步筛选、转换数据

filter:筛选 只留下 "连接和目标 Session 匹配" 的那个键值对

entry:就是 "键值对" 的别名(比如 < Tom, 连接 1 > 这个整体)

map:转换 把筛选出的键值对,换成它的 "键"(也就是用户名)

findFirst:取第一个匹配的结果(因为一个连接只对应一个用户名)

orElse:兜底 如果没找到匹配的用户名,就用 Session 的 ID 代替

2. 测试功能:群发与单发

启动 Spring Boot 项目后,可以打开多个浏览器窗口模拟不同用户的连接。

例如:

用户 1:连接 ws://localhost:8080/ws/chat?username=Tom;

用户 2:连接 ws://localhost:8080/ws/chat?username=Alice。

功能测试:

用户 1 发送消息:其他用户会收到群聊消息;

用户 1 关闭连接:会广播"某某离开聊天室"的消息。

3. 前端连接与发送消息

可以在浏览器控制台中执行以下代码来模拟前端连接和消息发送:

java 复制代码
// 连接 WebSocket
const ws = new WebSocket("ws://localhost:8080/ws/chat?username=Tom");

// 监听消息
ws.onmessage = (event) => {
    console.log("📩 收到消息:", event.data);
};

// 发送消息
ws.send("大家好,我是Tom!");

// 关闭连接
ws.close();

ws.onmessage = e => console.log(e.data);

3. 测试结果

相关推荐
Q_Q19632884751 小时前
python+django/flask+vue的视频及游戏管理系统
spring boot·python·django·flask·node.js·php
Zzzzzxl_1 小时前
互联网大厂Java/Agent面试实战:AIGC内容社区场景下的技术问答(含RAG/Agent/微服务/向量搜索)
java·spring boot·redis·ai·agent·rag·microservices
Zzzzzxl_1 小时前
互联网大厂Java/Agent面试:Spring Boot、JVM、微服务、RAG与向量检索实战问答
java·jvm·spring boot·kafka·rag·microservices·vectordb
Q_Q5110082851 小时前
python+django/flask基于Web的研究生管理系统
spring boot·python·django·flask·node.js·php
一 乐1 小时前
旅游出行|基于Springboot+Vue的旅游出行管理系统设计与实现(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·旅游
Q_Q5110082851 小时前
python+django/flask农业信息管理系统_农产品销售商场系统
spring boot·python·django·flask·node.js·php
MarkHD1 小时前
车辆TBOX科普 第51次 WebSocket实时通信与数据序列化:JSON vs Protobuf的深度实践
websocket·网络协议·json
JaguarJack1 小时前
现代高效 PHP 开发的最佳实践
后端·php
悟空码字1 小时前
WebSocket实战:让服务器和客户端“煲电话粥”
java·websocket·编程技术·后端开发