基于 Spring Boot 实现支持 300 + 人、包含单聊和群聊的对话聊天系统,需结合实时通信协议(WebSocket/STOMP)、消息路由、会话管理和消息持久化等核心能力。以下是完整的实现方案,包含架构设计、核心代码、扩展优化等内容。
一、技术选型
|
组件 / 技术 |
选型理由 |
|
|
通信协议 |
WebSocket(STOMP 子协议) |
基于 TCP 长连接,低延迟,原生支持双向通信;STOMP 简化消息路由(订阅 / 发布) |
|
消息容器 |
Spring WebSocket + SimpMessagingTemplate |
快速集成 WebSocket,内置消息广播 / 点对点发送能力 |
|
会话管理 |
Spring Security + WebSocket SessionRegistry |
鉴权、用户 – 会话绑定,支持踢人 / 离线检测 |
|
消息持久化 |
Redis(缓存)+ MySQL(持久化) |
缓存在线消息,持久化历史消息;Redis Pub/Sub 可扩展集群 |
|
序列化 |
Jackson(JSON) |
轻量、易解析,适配前后端数据交互 |
|
集群扩展(可选) |
Redis Pub/Sub + Spring Session |
多实例部署时,实现跨节点消息同步 |
二、核心架构设计
- 通信流程:客户端(Web/APP)通过 WebSocket 连接服务端,携带用户凭证鉴权;单聊:客户端订阅 /user/{userId}/msg 专属通道,服务端通过 SimpMessagingTemplate 点对点发送;群聊:客户端订阅 /topic/group/{groupId} 公共通道,服务端广播消息到该通道;消息持久化:发送消息时同步写入 Redis(近期消息)和 MySQL(全量消息),客户端拉取历史消息时优先查 Redis。
- 核心模块:鉴权模块:验证用户身份,绑定 WebSocket 会话与用户;消息路由模块:处理单聊 / 群聊消息的分发;会话管理模块:维护在线用户、检测离线、踢人等;消息持久化模块:缓存 + 持久化消息,支持历史查询;异常处理模块:连接断开重连、消息重发、限流等。

三、代码实现(核心部分)
1. 依赖引入(pom.xml)
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebSocket + STOMP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Spring Security(鉴权) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Redis(缓存/集群消息同步) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MySQL + MyBatis(持久化) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- Lombok(简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. WebSocket 配置(核心)
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker // 启用WebSocket消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 配置消息代理:客户端订阅的前缀(群聊/广播)
config.enableSimpleBroker("/topic", "/user");
// 客户端发送消息的前缀(服务端接收地址)
config.setApplicationDestinationPrefixes("/app");
// 点对点消息的前缀(替换默认的/user,避免冲突)
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册WebSocket端点:客户端连接地址
registry.addEndpoint("/chat-websocket")
.setAllowedOriginPatterns("*") // 允许跨域(生产环境需限定域名)
.withSockJS(); // 启用SockJS,兼容不支持WebSocket的浏览器
}
}
3. 实体类设计
(1)消息实体(ChatMessage)
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ChatMessage {
// 消息类型:单聊/群聊/上线/下线
public enum MessageType {
CHAT, GROUP_CHAT, JOIN, LEAVE
}
private MessageType type; // 消息类型
private String content; // 消息内容
private String sender; // 发送者ID
private String receiver; // 接收者ID(单聊)/群ID(群聊)
private LocalDateTime sendTime; // 发送时间
}
(2)用户在线状态(OnlineUser)
import lombok.Data;
@Data
public class OnlineUser {
private String userId; // 用户ID
private String sessionId; // WebSocket会话ID
private String username; // 用户名
private LocalDateTime loginTime; // 登录时间
}
4. 核心业务控制器(ChatController)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.user.SimpUserRegistry;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.List;
@RestController
public class ChatController {
// 点对点消息发送模板
@Autowired
private SimpMessagingTemplate messagingTemplate;
// 在线用户注册表(Spring内置)
@Autowired
private SimpUserRegistry simpUserRegistry;
// ========== 群聊消息处理 ==========
@MessageMapping("/chat/group") // 客户端发送地址:/app/chat/group
@SendTo("/topic/group/{receiver}") // 广播到群聊通道(receiver为群ID)
public ChatMessage handleGroupChat(@Payload ChatMessage message) {
message.setSendTime(LocalDateTime.now());
// 1. 持久化群聊消息(异步执行,避免阻塞)
saveGroupMessage(message);
// 2. 返回消息到群聊通道,所有订阅该群的客户端接收
return message;
}
// ========== 单聊消息处理 ==========
@MessageMapping("/chat/single") // 客户端发送地址:/app/chat/single
public void handleSingleChat(@Payload ChatMessage message) {
message.setSendTime(LocalDateTime.now());
// 1. 持久化单聊消息(异步)
saveSingleMessage(message);
// 2. 点对点发送到接收者的专属通道:/user/{receiver}/msg
messagingTemplate.convertAndSendToUser(
message.getReceiver(), // 接收者ID
"/msg", // 接收者订阅的子通道
message // 消息内容
);
}
// ========== 用户上线/下线处理 ==========
@MessageMapping("/chat/join")
@SendTo("/topic/online") // 广播在线状态
public ChatMessage handleUserJoin(@Payload ChatMessage message) {
message.setType(ChatMessage.MessageType.JOIN);
message.setSendTime(LocalDateTime.now());
// 记录在线用户(可结合Redis)
recordOnlineUser(message.getSender());
return message;
}
@MessageMapping("/chat/leave")
@SendTo("/topic/online")
public ChatMessage handleUserLeave(@Payload ChatMessage message) {
message.setType(ChatMessage.MessageType.LEAVE);
message.setSendTime(LocalDateTime.now());
// 移除在线用户
removeOnlineUser(message.getSender());
return message;
}
// ========== 辅助方法(持久化/状态管理) ==========
private void saveSingleMessage(ChatMessage message) {
// 异步调用MyBatis保存到MySQL,同时缓存到Redis(过期时间如1小时)
// redisTemplate.opsForList().leftPush("chat:single:" + message.getSender() + ":" + message.getReceiver(), message);
// chatMessageMapper.insertSingleMessage(message);
}
private void saveGroupMessage(ChatMessage message) {
// redisTemplate.opsForList().leftPush("chat:group:" + message.getReceiver(), message);
// chatMessageMapper.insertGroupMessage(message);
}
private void recordOnlineUser(String userId) {
// redisTemplate.opsForSet().add("online:users", userId);
}
private void removeOnlineUser(String userId) {
// redisTemplate.opsForSet().remove("online:users", userId);
}
// ========== 接口:查询历史消息 ==========
@GetMapping("/chat/history/single")
public List<ChatMessage> getSingleChatHistory(String sender, String receiver, Integer page, Integer size) {
// 优先查Redis,未命中查MySQL
// return chatMessageMapper.selectSingleHistory(sender, receiver, page, size);
return null;
}
@GetMapping("/chat/history/group")
public List<ChatMessage> getGroupChatHistory(String groupId, Integer page, Integer size) {
// return chatMessageMapper.selectGroupHistory(groupId, page, size);
return null;
}
// ========== 接口:查询在线用户 ==========
@GetMapping("/chat/online/users")
public List<OnlineUser> getOnlineUsers() {
// 从Redis/ SimpUserRegistry查询在线用户
// return onlineUserService.listOnlineUsers();
return null;
}
}
5. 安全配置(Spring Security)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // WebSocket场景下关闭CSRF(生产环境需评估)
.authorizeHttpRequests()
// 放行WebSocket连接和静态资源
.requestMatchers("/chat-websocket/**", "/chat/history/**", "/chat/online/**").permitAll()
// 其他接口需认证
.anyRequest().authenticated()
.and()
.formLogin().permitAll(); // 简化登录(生产环境替换为JWT/OAuth2)
return http.build();
}
// 模拟用户(生产环境替换为数据库查询)
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user1").password("{noop}123456").roles("USER").build());
manager.createUser(User.withUsername("user2").password("{noop}123456").roles("USER").build());
return manager;
}
}
四、客户端示例(Web 端,基于 STOMP.js + SockJS)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>聊天系统</title>
<!-- 引入SockJS和STOMP -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
</head>
<body>
<div>
<h3>群聊(group1)</h3>
<div id="groupChatContent"></div>
<input type="text" id="groupMsgInput" placeholder="输入消息">
<button onclick="sendGroupMsg()">发送群聊</button>
</div>
<div>
<h3>单聊(发给user2)</h3>
<div id="singleChatContent"></div>
<input type="text" id="singleMsgInput" placeholder="输入消息">
<button onclick="sendSingleMsg()">发送单聊</button>
</div>
<script>
// 1. 连接WebSocket
const socket = new SockJS('http://localhost:8080/chat-websocket');
const stompClient = Stomp.over(socket);
const sender = "user1"; // 当前登录用户
// 2. 建立连接
stompClient.connect({}, (frame) => {
console.log('连接成功:', frame);
// 3. 订阅群聊(group1)
stompClient.subscribe('/topic/group/group1', (message) => {
const msg = JSON.parse(message.body);
document.getElementById('groupChatContent').innerText += `${msg.sender}: ${msg.content}
`;
});
// 4. 订阅单聊(专属通道)
stompClient.subscribe(`/user/${sender}/msg`, (message) => {
const msg = JSON.parse(message.body);
document.getElementById('singleChatContent').innerText += `${msg.sender}: ${msg.content}
`;
});
// 5. 发送上线消息
stompClient.send('/app/chat/join', {}, JSON.stringify({
type: 'JOIN',
sender: sender
}));
});
// 6. 发送群聊消息
function sendGroupMsg() {
const content = document.getElementById('groupMsgInput').value;
stompClient.send('/app/chat/group', {}, JSON.stringify({
type: 'GROUP_CHAT',
content: content,
sender: sender,
receiver: 'group1' // 群ID
}));
document.getElementById('groupMsgInput').value = '';
}
// 7. 发送单聊消息
function sendSingleMsg() {
const content = document.getElementById('singleMsgInput').value;
stompClient.send('/app/chat/single', {}, JSON.stringify({
type: 'CHAT',
content: content,
sender: sender,
receiver: 'user2' // 接收者ID
}));
document.getElementById('singleMsgInput').value = '';
}
// 8. 断开连接时发送下线消息
window.onbeforeunload = () => {
stompClient.send('/app/chat/leave', {}, JSON.stringify({
type: 'LEAVE',
sender: sender
}));
stompClient.disconnect();
};
</script>
</body>
</html>
五、性能优化(支持 300 + 人)
- 连接优化:调整 Tomcat 的 WebSocket 连接数:server.tomcat.max-connections=1000(默认 1000,足够 300 + 人);启用心跳检测:stompClient.heartbeat.outgoing = 20000; stompClient.heartbeat.incoming = 20000;(20 秒心跳,检测断连)。
- 消息异步处理:消息持久化、状态更新等操作通过@Async异步执行,避免阻塞 WebSocket 线程;使用线程池控制异步任务:
- java
- 运行
- @Configuration @EnableAsync public class AsyncConfig { @Bean public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix(“chat-async-“); return executor; } }
- 缓存优化:Redis 缓存近期消息(如 1 小时内),减少 MySQL 查询压力;在线用户使用 Redis Set 存储,支持快速查询 / 删除。
- 集群扩展(可选):多实例部署时,通过 Redis Pub/Sub 同步消息:一个节点接收的消息,通过 Redis 广播到所有节点,确保所有客户端都能收到;使用 Spring Session + Redis 共享 WebSocket 会话,避免单点故障。
- 限流与容错:对单用户消息发送频率限流(如 10 条 / 秒),防止恶意刷屏;客户端断连后自动重连,服务端缓存未发送的消息(短时间),重连后补发。
六、生产环境注意事项
- 鉴权强化:替换示例中的简单登录,使用 JWT/OAuth2 认证,WebSocket 连接时校验 token;
- 跨域限制:生产环境限定允许跨域的域名,避免setAllowedOriginPatterns(“*”);
- 日志与监控:记录 WebSocket 连接 / 断开、消息发送失败等日志,监控连接数、消息延迟;
- 消息清理:定期清理 Redis 过期缓存和 MySQL 历史消息(如归档到冷数据存储);
- 安全防护:过滤消息内容中的敏感字符 / XSS 脚本,防止注入攻击。
总结
该方案基于 Spring Boot + WebSocket(STOMP)实现了 300 + 人的单聊 / 群聊能力,核心是通过 WebSocket 长连接保证实时性,Redis+MySQL 实现消息高效存储,同时通过异步处理、缓存优化支撑高并发。如需支持更大规模(如万人级),可进一步引入消息队列(如 RabbitMQ)、分布式 WebSocket 框架(如 Netty-socketio)等。
© 版权声明
文章版权归作者所有,未经允许请勿转载。





收藏了,感谢分享