目录

依赖导入

使用 @ServerEndpoint 创建 WebSocket 端点

WebSocket 生命周期方法

Session 管理(保存在线用户)

群发与单发消息实现

实战练习


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

依赖导入

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

1. Maven 依赖

在 pom.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)

暂时不用,但以后可加:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
</dependency>

3. 启动类要求

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

@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 支持。

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

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 服务端

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

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 端点地址为:

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  客户端连接时触发

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

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

常用场景:
向客户端发送欢迎消息
把 Session 对象加入到在线用户集合中(后面群发要用)
记录在线人数或用户状态

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

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

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

常用场景:
打印或处理收到的消息
实现业务逻辑(如聊天转发、命令执行)
调用 session.getBasicRemote().sendText() 回复客户端

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

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

触发时机:
客户端主动调用 ws.close();
网络中断或异常断开
服务端主动关闭连接

常用场景:
从在线用户集合中移除该 Session
广播“某某离开聊天室”的系统通知
清理资源

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

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

触发时机:
网络异常、编码异常、消息格式错误等情况

常用场景:
打印日志定位问题
通知管理员或客户端错误信息

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

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

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

Session 管理(保存在线用户)

在 WebSocket 中,每当一个客户端连接建立,框架都会为它创建一个 Session 对象。
我们可以利用它来:
1. 识别不同的用户;
2. 发送私聊(单发)或群发消息;
3. 统计在线人数;
4. 在断开时清理资源。

1. 为什么要管理 Session?

假设你写了一个聊天室:
小明、小红、小刚 都连上了服务器;
当小明发消息时,服务器需要广播给所有在线用户;
这时服务器必须知道有哪些连接(Session)存在。

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

2. 使用 ConcurrentHashMap 保存 Session

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

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 执行:

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():

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 进入聊天室”)
转发聊天内容给所有连接的客户端

触发示例

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

2. 单发(私聊消息)

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

前端连接:

const ws = new WebSocket("ws://localhost:8080/ws/chat?username=Tom");

后端解析并存储:

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

实现私聊:

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();
        }
    }
}

调用示例:

sendToUser("Tom", "这是一条私聊消息");

3. 群发 + 单发混合场景

实际聊天系统中会根据消息内容决定:
若前端发送的 JSON 包含目标用户:执行 sendToUser();
否则默认广播。

例如:

@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 存储在线用户并广播消息。

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. 前端连接与发送消息

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

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

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐