springboot实现300+人的对话聊天,支持群聊及单聊

内容分享8小时前发布
1 1 0

基于 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

多实例部署时,实现跨节点消息同步

二、核心架构设计

  1. 通信流程:客户端(Web/APP)通过 WebSocket 连接服务端,携带用户凭证鉴权;单聊:客户端订阅 /user/{userId}/msg 专属通道,服务端通过 SimpMessagingTemplate 点对点发送;群聊:客户端订阅 /topic/group/{groupId} 公共通道,服务端广播消息到该通道;消息持久化:发送消息时同步写入 Redis(近期消息)和 MySQL(全量消息),客户端拉取历史消息时优先查 Redis。
  2. 核心模块:鉴权模块:验证用户身份,绑定 WebSocket 会话与用户;消息路由模块:处理单聊 / 群聊消息的分发;会话管理模块:维护在线用户、检测离线、踢人等;消息持久化模块:缓存 + 持久化消息,支持历史查询;异常处理模块:连接断开重连、消息重发、限流等。

springboot实现300+人的对话聊天,支持群聊及单聊

三、代码实现(核心部分)

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 + 人)

  1. 连接优化:调整 Tomcat 的 WebSocket 连接数:server.tomcat.max-connections=1000(默认 1000,足够 300 + 人);启用心跳检测:stompClient.heartbeat.outgoing = 20000; stompClient.heartbeat.incoming = 20000;(20 秒心跳,检测断连)。
  2. 消息异步处理:消息持久化、状态更新等操作通过@Async异步执行,避免阻塞 WebSocket 线程;使用线程池控制异步任务:
  3. java
  4. 运行
  5. @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; } }
  6. 缓存优化:Redis 缓存近期消息(如 1 小时内),减少 MySQL 查询压力;在线用户使用 Redis Set 存储,支持快速查询 / 删除。
  7. 集群扩展(可选):多实例部署时,通过 Redis Pub/Sub 同步消息:一个节点接收的消息,通过 Redis 广播到所有节点,确保所有客户端都能收到;使用 Spring Session + Redis 共享 WebSocket 会话,避免单点故障。
  8. 限流与容错:对单用户消息发送频率限流(如 10 条 / 秒),防止恶意刷屏;客户端断连后自动重连,服务端缓存未发送的消息(短时间),重连后补发。

六、生产环境注意事项

  1. 鉴权强化:替换示例中的简单登录,使用 JWT/OAuth2 认证,WebSocket 连接时校验 token;
  2. 跨域限制:生产环境限定允许跨域的域名,避免setAllowedOriginPatterns(“*”);
  3. 日志与监控:记录 WebSocket 连接 / 断开、消息发送失败等日志,监控连接数、消息延迟;
  4. 消息清理:定期清理 Redis 过期缓存和 MySQL 历史消息(如归档到冷数据存储);
  5. 安全防护:过滤消息内容中的敏感字符 / XSS 脚本,防止注入攻击。

总结

该方案基于 Spring Boot + WebSocket(STOMP)实现了 300 + 人的单聊 / 群聊能力,核心是通过 WebSocket 长连接保证实时性,Redis+MySQL 实现消息高效存储,同时通过异步处理、缓存优化支撑高并发。如需支持更大规模(如万人级),可进一步引入消息队列(如 RabbitMQ)、分布式 WebSocket 框架(如 Netty-socketio)等。

© 版权声明

相关文章

1 条评论

  • 头像
    超会买的嘟嘟 读者

    收藏了,感谢分享

    无记录
    回复