SpringBoot实现WebSocket实现用户一对一和一对多信息的发送
(个人学习记录)
这里我是通过controller、service、和serviceImpl的结构来实现的,但是还多了一个核心端点类 (WebSocketEndpoint):这个类是专门负责管理 WebSocket 协议的连接生命周期(建立、关闭、接收消息、处理错误),并且对接业务层(WebSocketService)。
一、首先我们导入相关依赖
1、导入websocket的maven依赖:
xml
//websocket依赖导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
因为后面我们要用到hutool工具来处理json格式的数据,所以我们这里也导入hutool的一个json处理maven:
xml
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.28</version>
</dependency>
下面是我的一个xml配置,可以一下:只是一些非常简单的maven,主要是为了简单的学习websocket在springboot方面该怎么实现消息的发送和接收。
xml
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>websocketChat</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>websocketChat</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.28</version>
</dependency>
</dependencies>
</project>
二、构建消息接收类
就是一个普通的bean
java
package org.example.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Chat {
//主键id
private Integer id;
//发送者id
private Integer userId;
//接收者id
private Integer targetUserId;
//创建时间
private LocalDateTime createTime;
//发送的内容
private String content;
// broadcast为广播,single为一对一(不传则默认一对一);
private String msgType;
}
三、websocketConfig配置
java
package org.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置类:启用@ServerEndpoint注解的支持
*/
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,自动注册@ServerEndpoint标记的类
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
四、构建核心端点类(这个是最重要的部分)
这个类是最重要的,这个类负责处理客户端发送的websocket连接请求。
这个类的核心职责是:作为 WebSocket 服务端的入口,接收前端的 WebSocket 连接请求,触发连接建立 / 关闭 / 消息 / 异常等生命周期事件,并将具体的业务逻辑委托给 WebSocketService 处理,实现前端与后端的实时双向通信(比如聊天、消息推送等场景)。
java
package org.example.chat;
import lombok.extern.slf4j.Slf4j;
import org.example.service.WebSocketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.SpringConfigurator;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint(value = "/ws/{userId}")
@Component
@Slf4j
public class WebSocketEndpoint {
// 解决WebSocket中注入Spring Bean的问题(核心)
private static WebSocketService webSocketService;
@Autowired
public void setWebSocketService(WebSocketService webSocketService) {
WebSocketEndpoint.webSocketService = webSocketService;
}
private Integer userId;
/**
* 连接建立时触发
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") Integer userId) {
this.userId = userId;
webSocketService.connect(userId, session);
}
/**
* 连接关闭时触发
*/
@OnClose
public void onClose() {
webSocketService.disconnect(this.userId);
}
/**
* 收到客户端消息时触发
*/
@OnMessage
public void onMessage(String message) {
webSocketService.handleMessage(message);
}
/**
* 发生错误时触发
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("【WebSocket】用户{}连接出错:{}", this.userId, error.getMessage());
error.printStackTrace();
}
}
下面是方法(注解)的一些解释:
| 方法 | 注解 | 触发时机 | 核心逻辑 |
|---|---|---|---|
onOpen |
@OnOpen |
客户端成功建立 WebSocket 连接时 | 1. 保存当前连接的用户 ID;2. 调用 webSocketService.connect() 初始化连接(如记录在线用户)。 |
onClose |
@OnClose |
客户端关闭 WebSocket 连接时(主动 / 被动) | 调用 webSocketService.disconnect() 清理连接(如移除在线用户)。 |
onMessage |
@OnMessage |
服务端收到客户端通过 WebSocket 发送的消息时 | 调用 webSocketService.handleMessage() 处理消息(如解析、转发消息)。 |
onError |
@OnError |
WebSocket 连接发生错误时(如断连、解析失败) | 打印错误日志,定位连接异常原因。 |
**重点注意:**在这个端点类(WebSocketEndpoint),直接用 @Autowired 注入 WebSocketService 行不通,必须用「静态字段 + Setter 注入」的特殊方式,核心原因是 WebSocket 端点的实例管理方与 Spring 容器不一致,这是 WebSocket 结合 Spring 开发的核心痛点。
也就是必须用下面的方式注入webSocketService(在这个端点类里面):
java
// 解决WebSocket中注入Spring Bean的问题(核心)
private static WebSocketService webSocketService;
@Autowired
public void setWebSocketService(WebSocketService webSocketService) {
WebSocketEndpoint.webSocketService = webSocketService;
}
不能用下面这种方式注入:(只是在这个端点类不可以这样,在controller里面是可以这样注入的)
java
@Autowired
private WebSocketService webSocketService;
五、构建WebSocketController
JAVA
package org.example.controller;
import org.example.entity.Chat;
import org.example.service.WebSocketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/websocket")
public class WebSocketController {
@Autowired
private WebSocketService webSocketService;
/**
* 单点发送消息
*/
@PostMapping("/send")
public String sendMessage(@RequestBody Chat chat) {
try {
if (chat.getReceiverId() == null || chat.getContent() == null) {
return "参数错误:targetUserId和content不能为空";
}
//可以在这里存储数据信息
webSocketService.sendOneMessage(chat.getReceiverId(), chat.getContent());
return "一对一消息发送成功";
} catch (Exception e) {
return "一对一消息发送失败:" + e.getMessage();
}
}
/**
* 广播消息
*/
@PostMapping("/sendall")
public String sendAllMessage(@RequestParam String message) {
try {
webSocketService.sendAllMessage(message);
return "广播消息发送成功";
} catch (Exception e) {
return "广播消息发送失败:" + e.getMessage();
}
}
/**
* 批量发送消息
*/
@PostMapping("/sendmore")
public String sendMoreMessage(@RequestParam Integer[] userIds, @RequestParam String message) {
try {
webSocketService.sendMoreMessage(userIds, message);
return "批量消息发送成功";
} catch (Exception e) {
return "批量消息发送失败:" + e.getMessage();
}
}
}
六、构建service和serviceImpl
service:(因为端点类里也注入了这个service,所以里面有一些端点类的实现,当然也有controller类的实现)
java
package org.example.service;
public interface WebSocketService {
/**
* 建立连接时初始化用户会话
* @param userId 用户ID
* @param session WebSocket会话
*/
void connect(Integer userId, javax.websocket.Session session);
/**
* 断开连接时清理用户会话
* @param userId 用户ID
*/
void disconnect(Integer userId);
/**
* 处理客户端发送的消息
* @param message 客户端原始消息
*/
void handleMessage(String message);
/**
* 广播消息给所有在线用户
* @param message 消息内容
*/
void sendAllMessage(String message);
/**
* 单点发送消息
* @param userId 目标用户ID
* @param message 消息内容
*/
void sendOneMessage(Integer userId, String message);
/**
* 给多个用户发送消息
* @param userIds 目标用户ID数组
* @param message 消息内容
*/
void sendMoreMessage(Integer[] userIds, String message);
}
serviceImpl:
java
package org.example.service.impl;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.example.service.WebSocketService;
import org.springframework.stereotype.Service;
import javax.websocket.Session;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@Slf4j
@Service
public class WebSocketServiceImpl implements WebSocketService {
// 存放所有WebSocket连接实例
private static CopyOnWriteArraySet<Object> webSocketSet = new CopyOnWriteArraySet<>();
// 存放用户ID与Session的映射(核心:保证线程安全)
private static ConcurrentHashMap<Integer, Session> sessionPool = new ConcurrentHashMap<>();
/**
* 用户连接时初始化
*/
@Override
public void connect(Integer userId, Session session) {
try {
webSocketSet.add(this);
sessionPool.put(userId, session);
log.info("【WebSocket】用户{}连接成功,当前在线总数:{}", userId, webSocketSet.size());
} catch (Exception e) {
log.error("【WebSocket】用户{}连接初始化失败", userId, e);
throw new RuntimeException(e);
}
}
/**
* 用户断开连接时清理
*/
@Override
public void disconnect(Integer userId) {
try {
webSocketSet.remove(this);
sessionPool.remove(userId);
log.info("【WebSocket】用户{}断开连接,当前在线总数:{}", userId, webSocketSet.size());
} catch (Exception e) {
log.error("【WebSocket】用户{}断开连接失败", userId, e);
}
}
/**
* 处理客户端通过WebSocket发送的消息
*/
@Override
public void handleMessage(String message) {
log.info("【WebSocket】收到客户端消息:{}", message);
try {
JSONObject msgJson = new JSONObject(message);
Integer senderId = msgJson.getInt("userId");
Integer receiverId = msgJson.getInt("targetUserId");
String content = msgJson.getStr("content");
// 构造转发消息
JSONObject sendMsg = new JSONObject();
sendMsg.put("senderId", senderId);
sendMsg.put("content", content);
// 转发给目标用户
this.sendOneMessage(receiverId, sendMsg.toString());
} catch (JSONException e) {
log.error("【WebSocket】JSON解析失败,消息格式错误", e);
} catch (Exception e) {
log.error("【WebSocket】消息转发失败", e);
}
}
/**
* 广播消息给所有在线用户
*/
@Override
public void sendAllMessage(String message) {
log.info("【WebSocket】广播消息:{}", message);
for (Object webSocket : webSocketSet) {
try {
// 遍历所有Session并发送
for (Session session : sessionPool.values()) {
if (session.isOpen()) {
session.getAsyncRemote().sendText(message);
}
}
} catch (Exception e) {
log.error("【WebSocket】广播消息发送失败", e);
}
}
}
/**
* 单点发送消息
*/
//接收者userId
@Override
public void sendOneMessage(Integer userId, String message) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【WebSocket】给用户{}发送单点消息:{}", userId, message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
log.error("【WebSocket】给用户{}发送单点消息失败", userId, e);
}
} else {
log.warn("【WebSocket】用户{}未在线,消息发送失败:{}", userId, message);
}
}
/**
* 给多个用户发送消息(修正原代码String转Integer的问题)
*/
@Override
public void sendMoreMessage(Integer[] userIds, String message) {
for (Integer userId : userIds) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【WebSocket】给用户{}发送批量消息:{}", userId, message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
log.error("【WebSocket】给用户{}发送批量消息失败", userId, e);
}
}
}
}
}
七、进行测试:
我们先启动后端,
通过apifox来实现测试:
1、首先创建websocket连接:

这里我已经构建了三个用户了
2、发送websocket连接
注意websocket发送的请求是ws开头,不是http或https.
在这里我把要发送的消息的json格式也贴上期了

我们点击连接用户1和用户2,用户1的id为1001,用户2的id为1002.
可以看到上面我们是用户1给用户2发送连接,
下面的图片连接和发送的结果:(这里面的逻辑可以在后端修改,通过这个发送的信息默认调用@onMessage注解标注的方法,在端点类里面)
用户1:

可以看到当连接成功后我们的连接按钮变为断开,消息框点击发送按钮后,也发送了消息。
用户2:也接收到了信息,

最后我们通过http来实现消息的发送,也就是调用controller方法。
当然,在发送之前必须要websocket进行连接,不然端点类没有相关用户,也不知道给谁发消息了,
如果用户不在,我们可以在后端把数据存储起来,等用户上线后直接从数据库拉取。(当然需要实现后端的逻辑,这里没有写)

在用户2进行查看是否发送过去:

可以看到http也发送成功了