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. 测试结果

相关推荐
智驭未来掌门人14 小时前
利用FFmpeg快速实现一个RTSP推流服务器
后端
垚垚学技术_聚焦云原生14 小时前
K8s Pod 完整生命周期详解
后端
JavaGuide14 小时前
Github 史诗级故障,与此同时,Cursor 版「GitHub」正式上线!
前端·后端
不一样的少年_14 小时前
修了 Bug、做了重构,为什么老板还是觉得你没产出?
前端·后端·程序员
水深火乐14 小时前
安全地生成验证码、密码和 Token
后端
小强198815 小时前
SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程
后端
水深火乐15 小时前
interface和any
后端
步行cgn15 小时前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
神奇小汤圆15 小时前
Spring Boot + LangChain4j实现RAG——从零搭建企业级知识库问答系统
后端
H_Peak15 小时前
Python运算符与流程控制:if条件判断
后端