ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot+WebSocket构建实时聊天系统:从原理到实战

2026/9/4 10:38:50 拓冰建站 浏览量
SpringBoot+WebSocket构建实时聊天系统:从原理到实战 简介这是一套面向计算机相关专业本科生的毕业设计级在线聊天系统实现方案适用于课程设计、期末大作业及毕设开发尤其适合具备Spring Boot基础并希望深入理解实时通信与分布式架构的学生。系统采用Spring Boot Netty构建高并发后台集成FastDFS分布式文件存储、Nginx反向代理及MUIH5Plus移动端框架完整覆盖前台手机端含登录注册、通讯录、朋友圈、扫一扫等模块与后台实时消息管理功能。压缩包共290个文件包含73个Java源码、74个编译类文件、27个XML配置、20个HTML页面及配套CSS/JS资源总大小1.35MB结构清晰、模块解耦明确如huyan-huxin-netty专司消息分发huyan-huxin-mybatis负责数据持久。已有146人学习下载代码经严格调试可直接运行同时提供完整项目说明文档便于快速掌握架构设计逻辑、Netty通信机制与前后端协同流程。1. 项目概述与核心价值最近在整理过往的项目资料翻到了几年前带学生做的一个基于SpringBoot的在线聊天系统。这个项目虽然作为毕业设计提交但其设计思路和实现细节对于想深入理解WebSocket实时通信、SpringBoot全栈开发以及中小型即时通讯IM系统架构的朋友来说依然有很高的参考价值。它不是那种大而全的“轮子”而是一个功能完整、结构清晰、可以直接跑起来的教学级/入门级实战项目。如果你正面临毕业设计选题或者想自己动手搭建一个具备实时聊天功能的Web应用那么这个从零到一的设计与实现过程或许能给你提供一条清晰的路径。这个系统的核心目标很明确构建一个支持多用户实时文字聊天、好友管理、消息历史记录的Web应用。它避开了音视频等复杂媒体流处理专注于最基础的、也是最重要的实时消息推送与状态同步机制。技术栈上后端以SpringBoot为核心整合了WebSocket、Spring Security、JPAHibernate和MySQL前端则采用了相对轻量的Thymeleaf模板引擎配合原生JavaScript与jQuery避免了复杂前端框架的学习曲线让开发者能更聚焦于业务逻辑与通信协议本身。整个项目源码结构清晰注释详尽配套的项目说明文档也详细阐述了设计思路、模块划分和部署步骤非常适合学习和二次开发。2. 系统整体架构与设计思路拆解2.1 为什么选择这样的技术栈在项目启动前技术选型是首要问题。对于毕业设计或快速原型开发选型的核心原则是“成熟、简单、快”。后端SpringBoot 是毋庸置疑的起点。它通过自动配置和起步依赖极大地简化了Spring应用的初始搭建和开发过程。对于聊天系统我们最需要的是WebSocket支持和用户认证。SpringBoot通过spring-boot-starter-websocket和spring-boot-starter-security这两个starter几乎零配置就能引入所需能力。数据库访问方面Spring Data JPA让我们能用面向对象的方式操作数据库避免了繁琐的SQL拼接对于快速迭代非常友好。选择MySQL作为数据库是因为它普及率高学习资源丰富且完全能满足中小规模聊天系统的数据存储需求。前端为什么不用Vue/React这是一个常见的疑问。对于这个教学性质的项目使用Thymeleaf原生JS的组合有几个好处第一降低学习门槛。学生或初学者可能尚未掌握现代前端框架使用原生技术能让他们更专注于理解HTTP请求、WebSocket连接等基础网络概念。第二项目结构更简单。无需构建工具Webpack/Vite直接通过SpringBoot服务静态资源开发调试更直接。第三Thymeleaf能很好地与SpringBoot集成实现服务端渲染方便在页面中直接注入用户状态、好友列表等数据。当然如果你熟悉Vue或React完全可以将前端剥离通过REST API和WebSocket与后端交互这是更工程化的做法但复杂度也会相应增加。2.2 核心架构模式从短轮询到WebSocket理解实时聊天首先要明白传统的HTTP协议是“请求-响应”模式服务器无法主动向客户端推送数据。早期实现“实时”效果通常采用短轮询Short Polling或长轮询Long Polling。短轮询客户端每隔几秒就向服务器发一次请求问“有新消息吗”。这种方式实现简单但无效请求多对服务器压力大实时性差。长轮询客户端发起请求后服务器会保持连接直到有数据或超时才返回。客户端收到响应后立即发起下一个请求。这减少了无效请求但连接管理复杂且每个连接在等待期间仍然占用资源。而WebSocket协议的出现完美解决了上述问题。它在一次HTTP握手升级后建立全双工、持久化的TCP连接。此后服务器和客户端可以随时主动向对方发送数据帧真正实现了低延迟、低开销的实时双向通信。我们的聊天系统正是基于WebSocket构建核心通信层。系统架构图概念层面[浏览器客户端1] --WebSocket-- [SpringBoot服务器] --WebSocket-- [浏览器客户端2] | | | |---HTTP (登录、获取好友列表)---| | | |---JPA--- [MySQL数据库] |用户通过HTTP请求登录服务器验证后建立会话。登录成功页面加载时前端JavaScript会尝试与服务器的WebSocket端点建立连接。连接建立后服务器将该WebSocket会话与当前登录用户绑定通常存入一个ConcurrentHashMap。当用户A发送消息给用户B时消息通过已建立的WebSocket连接发送到服务器。服务器根据消息中的接收者IDB从映射表中找到B的WebSocket会话将消息转发过去。用户B的浏览器通过WebSocket接收到消息实时渲染到聊天窗口。所有消息在转发的同时会通过JPA异步持久化到MySQL中用于历史消息查询。3. 核心模块详解与实现要点3.1 用户认证与会话管理安全是聊天系统的基石。我们采用Spring Security进行基础的认证与授权。实现要点配置Spring Security在配置类中我们需要定义哪些路径需要认证如/chat/**哪些路径开放如/login,/register, 静态资源。同时要配置密码编码器推荐使用BCryptPasswordEncoder确保用户密码不以明文存储。Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/, /login, /register, /css/**, /js/**).permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage(/login) // 自定义登录页 .defaultSuccessUrl(/chat) // 登录成功跳转到聊天页 .permitAll() .and() .logout() .permitAll(); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }用户实体与Repository创建User实体类包含id、username、password加密后、nickname等字段。通过JpaRepository实现基本的增删改查。注册与登录逻辑注册时对用户输入的密码进行BCrypt加密后存入数据库。登录时Spring Security会自动完成用户名、密码的校验和会话HttpSession的创建。注意事项会话绑定WebSocket连接本身是无状态的。我们需要在WebSocket握手阶段将WebSocket会话与当前已认证的HTTP会话从而与用户ID关联起来。这通常可以通过实现HandshakeInterceptor接口在beforeHandshake方法中从HTTP请求中获取用户信息如从SecurityContext或HttpSession并将其属性设置到WebSocket的attributes中。并发用户映射在服务器内存中需要维护一个ConcurrentHashMapString, WebSocketSession键是用户唯一标识如username或userId值是对应的WebSocketSession对象。这样当需要向特定用户发送消息时能快速找到其会话。务必注意线程安全ConcurrentHashMap是必须的。3.2 WebSocket通信核心实现这是项目的“心脏”。Spring提供了对WebSocket的高级抽象我们主要使用EnableWebSocketMessageBroker和基于消息代理的简单模式。实现步骤配置WebSocket创建一个配置类实现WebSocketMessageBrokerConfigurer接口。Configuration EnableWebSocketMessageBroker // 启用WebSocket消息代理 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 定义WebSocket端点客户端将连接到此端点 registry.addEndpoint(/ws-chat).withSockJS(); // 使用SockJS作为降级方案 } Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 设置消息代理的前缀客户端订阅地址以/topic或/user开头 registry.enableSimpleBroker(/topic, /queue); // 设置应用目的地前缀客户端发送消息到/app开头的地址 registry.setApplicationDestinationPrefixes(/app); // 点对点消息前缀可选用于一对一 registry.setUserDestinationPrefix(/user); } }/ws-chat是WebSocket的握手端点。withSockJS()提供了降级支持在不支持WebSocket的浏览器上会自动使用轮询等方式模拟。enableSimpleBroker启用了一个简单的内存消息代理处理以/topic广播和/queue队列为前缀的消息。setApplicationDestinationPrefixes定义了客户端发送消息的目标前缀所有发送到/app的消息都会被带有MessageMapping注解的方法处理。消息控制器Controller处理客户端发送来的消息。Controller public class ChatController { Autowired private SimpMessagingTemplate messagingTemplate; // 用于向客户端发送消息 MessageMapping(/chat.sendMessage) // 客户端发送到 /app/chat.sendMessage SendToUser(/queue/messages) // 发送给消息的特定接收者 public ChatMessage sendMessage(Payload ChatMessage chatMessage, Principal principal) { // 1. 可以在这里保存消息到数据库异步 // 2. 通过messagingTemplate.convertAndSendToUser() 发送给指定用户 String recipient chatMessage.getReceiver(); messagingTemplate.convertAndSendToUser( recipient, /queue/messages, chatMessage ); return chatMessage; // 也可以不返回取决于业务 } MessageMapping(/chat.addUser) SendTo(/topic/public) // 广播给所有订阅了/topic/public的客户端 public ChatMessage addUser(Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { // 将用户名添加到WebSocket会话属性中 headerAccessor.getSessionAttributes().put(username, chatMessage.getSender()); return chatMessage; } }前端连接与通信使用SockJS和STOMP客户端库。// 建立连接 var socket new SockJS(/ws-chat); var stompClient Stomp.over(socket); stompClient.connect({}, function (frame) { console.log(Connected: frame); // 订阅公共频道广播 stompClient.subscribe(/topic/public, function (message) { showMessage(JSON.parse(message.body)); }); // 订阅个人消息队列点对点 stompClient.subscribe(/user/queue/messages, function (message) { showPrivateMessage(JSON.parse(message.body)); }); // 通知服务器用户加入 stompClient.send(/app/chat.addUser, {}, JSON.stringify({sender: username, type: JOIN}) ); }); // 发送消息 function sendMessage() { var chatMessage { sender: username, receiver: targetUserId, content: $(#message-input).val(), type: CHAT, timestamp: new Date() }; stompClient.send(/app/chat.sendMessage, {}, JSON.stringify(chatMessage)); }实操心得SimpMessagingTemplate是关键工具它比注解SendTo更灵活可以在任何Service层中注入并使用实现复杂的消息路由逻辑。用户目标/usersetUserDestinationPrefix和convertAndSendToUser的配合是实现一对一私聊的优雅方式。Spring会自动将目的地/user/{username}/queue/messages转换为该用户当前会话独有的队列地址。消息对象设计ChatMessage类需要精心设计字段至少包含sender发送者、receiver接收者可为空表示广播、content内容、type类型如JOIN、CHAT、LEAVE、timestamp时间戳。这为前端区分消息类型、渲染不同样式提供了依据。3.3 好友关系与消息持久化单纯的实时通信还不够一个完整的聊天系统需要关系链和历史记录。好友关系设计数据表可以设计一个friendship表包含id、user_id、friend_id、status如0-待确认1-已好友2-已拉黑、create_time等字段。这是一个典型的多对多关系通过中间表实现。业务逻辑提供“添加好友”、“处理好友请求”、“获取好友列表”、“删除好友”等接口。添加好友时通常先插入一条状态为“待确认”的记录对方同意后更新为“已好友”。消息持久化设计数据表message表包含id、sender_id、receiver_id、content、message_type文本/图片/文件等、is_read是否已读、send_time等字段。存储策略消息的持久化不能阻塞实时转发流程。否则如果数据库写入慢会拖累消息的实时性。推荐的做法是在ChatController的sendMessage方法中接收到消息后立即通过SimpMessagingTemplate转发给目标用户。同时将消息对象放入一个内存队列如BlockingQueue或直接提交给一个异步任务执行器Async。由另一个线程从队列中取出消息或由异步方法执行数据库插入操作。Service public class MessagePersistenceService { Autowired private MessageRepository messageRepository; Async // 启用异步执行 public void saveMessageAsync(ChatMessage chatMessage) { // 将ChatMessage转换为MessageEntity MessageEntity entity convertToEntity(chatMessage); messageRepository.save(entity); } } // 在Controller中调用 MessageMapping(/chat.sendMessage) public void sendMessage(Payload ChatMessage chatMessage, Principal principal) { // 1. 实时转发 messagingTemplate.convertAndSendToUser(chatMessage.getReceiver(), /queue/messages, chatMessage); // 2. 异步持久化 messagePersistenceService.saveMessageAsync(chatMessage); }需要在启动类上添加EnableAsync注解以启用异步支持。注意事项历史消息拉取当用户打开与某个好友的聊天窗口时前端应通过HTTP GET请求调用后端接口分页查询两人之间的历史消息。这属于传统的请求-响应模式与WebSocket的实时推送是互补的。消息状态同步“已读”状态的处理是个难点。一种常见做法是当用户点开某个聊天窗口渲染完历史消息后前端主动发送一个“已读回执”类型的WebSocket消息到服务器服务器更新该用户与当前聊天对象之间所有未读消息的is_read状态并通知对方用户“消息已读”。4. 关键功能实现与代码解析4.1 一对一私聊的实现细节一对一私聊是核心功能其关键在于如何将消息准确路由到目标用户的WebSocket会话。后端实现核心如前所述我们利用Spring的convertAndSendToUser方法。但前提是我们需要在用户建立WebSocket连接时将其身份如username与当前的SimpMessageHeaderAccessor或Principal关联起来。这通常在握手拦截器或连接监听器中完成。更稳健的做法是实现ChannelInterceptor在CONNECT类型的消息被处理时将用户信息存储起来Component public class UserChannelInterceptor implements ChannelInterceptor { Override public Message? preSend(Message? message, MessageChannel channel) { StompHeaderAccessor accessor StompHeaderAccessor.wrap(message); if (StompCommand.CONNECT.equals(accessor.getCommand())) { Principal principal accessor.getUser(); if (principal ! null) { // 将用户名与sessionId关联存储到全局Map或Redis中 String username principal.getName(); String sessionId accessor.getSessionId(); userSessionRegistry.registerSessionId(username, sessionId); } } return message; } }然后在发送私聊消息时可以从这个注册表中根据用户名找到其对应的sessionId进而构造出正确的用户目的地。前端实现核心前端需要维护一个当前聊天对象currentChatWith的状态。当用户点击好友列表中的某个好友时设置此状态并将消息输入框和发送按钮的目标指向该用户。同时可以触发一个HTTP请求去拉取与该好友的历史消息。// 点击好友列表项 function selectFriend(friendId, friendName) { currentChatWith {id: friendId, name: friendName}; $(#chat-with-name).text(friendName); // 清空当前消息显示区域 $(#message-area).empty(); // 拉取历史消息 $.get(/api/messages/history?friendId${friendId}, function(messages) { messages.forEach(msg renderMessage(msg)); }); } // 发送消息函数需要修改 function sendMessage() { if (!currentChatWith) { alert(请先选择聊天对象); return; } var chatMessage { sender: currentUser, receiver: currentChatWith.id, // 指定接收者ID content: $(#message-input).val(), type: CHAT }; stompClient.send(/app/chat.private, {}, JSON.stringify(chatMessage)); }4.2 群聊聊天室功能的扩展在基础一对一功能上扩展群聊功能是很好的练习。其核心在于“订阅”机制。实现思路创建聊天室后端提供接口创建聊天室ChatRoom实体并记录成员ChatRoomMember。加入聊天室用户前端点击加入某个聊天室时向服务器发送一个订阅请求例如发送到/app/chat.joinRoom并携带房间ID。后端处理该请求将用户加入该房间的成员列表内存或数据库并将该用户的WebSocket会话订阅到以房间ID为标识的特定主题例如/topic/room.{roomId}。MessageMapping(/chat.joinRoom) public void joinRoom(Payload ChatMessage message, SimpMessageHeaderAccessor headerAccessor) { String roomId message.getRoomId(); String username message.getSender(); // 1. 业务逻辑记录用户加入房间数据库或缓存 roomService.userJoinRoom(username, roomId); // 2. 订阅逻辑实际上订阅是由前端发起的。这里可以返回一个确认信息通知前端去订阅。 // 或者利用SimpMessagingTemplate直接向该房间主题发送一条“用户加入”的广播。 ChatMessage joinMsg new ChatMessage(); joinMsg.setType(ChatMessage.MessageType.JOIN); joinMsg.setSender(username); joinMsg.setContent(username 加入了聊天室); messagingTemplate.convertAndSend(/topic/room. roomId, joinMsg); }发送群消息用户发送消息到/app/chat.sendToRoom后端根据消息中的roomId将消息广播到/topic/room.{roomId}。所有订阅了该主题的用户都会收到。MessageMapping(/chat.sendToRoom) public void sendToRoom(Payload ChatMessage message) { messagingTemplate.convertAndSend(/topic/room. message.getRoomId(), message); // 同样异步持久化群消息 messagePersistenceService.saveRoomMessageAsync(message); }前端订阅用户成功加入房间后前端需要动态订阅对应的主题。function joinRoom(roomId) { stompClient.send(/app/chat.joinRoom, {}, JSON.stringify({roomId: roomId})); // 订阅该房间的主题 var subscription stompClient.subscribe(/topic/room.${roomId}, function(message) { showRoomMessage(JSON.parse(message.body)); }); // 将订阅对象保存起来以便离开房间时取消订阅 roomSubscriptions[roomId] subscription; }4.3 用户在线状态管理显示好友是否在线能极大提升用户体验。实现在线状态的核心是监听WebSocket的连接和断开事件。后端实现实现事件监听器创建组件实现WebSocketDisconnectClient和WebSocketConnectClient监听器或使用ApplicationListener监听SessionConnectEvent和SessionDisconnectEvent。Component public class WebSocketEventListener { Autowired private SimpMessagingTemplate messagingTemplate; EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { // 从事件中获取用户信息需在握手时存入 StompHeaderAccessor headers StompHeaderAccessor.wrap(event.getMessage()); String username headers.getUser().getName(); // 更新用户状态为“在线”可以存入Redis或内存Map userStatusService.userOnline(username); // 广播给该用户的好友用户X已上线 notifyFriendsStatusChange(username, true); } EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { StompHeaderAccessor headers StompHeaderAccessor.wrap(event.getMessage()); String username headers.getUser().getName(); // 更新用户状态为“离线” userStatusService.userOffline(username); // 广播给该用户的好友用户X已离线 notifyFriendsStatusChange(username, false); } private void notifyFriendsStatusChange(String username, boolean isOnline) { // 1. 查询该用户的所有好友 ListString friendUsernames friendshipService.getFriendUsernames(username); // 2. 向每个好友的“状态更新”队列发送通知 for (String friend : friendUsernames) { MapString, Object msg new HashMap(); msg.put(user, username); msg.put(status, isOnline ? ONLINE : OFFLINE); messagingTemplate.convertAndSendToUser(friend, /queue/status, msg); } } }状态存储对于小型系统可以用内存ConcurrentHashMap存储用户名 - 状态。对于需要分布式扩展或持久化状态的需求应该使用Redis。Redis的SET数据结构可以存储在线用户集合并且Key可以设置过期时间配合心跳机制可以处理非正常断开的情况。前端实现前端需要订阅个人的状态队列/user/queue/status。当收到状态更新消息时更新好友列表UI中相应用户的在线状态图标。// 连接建立后订阅状态队列 stompClient.subscribe(/user/queue/status, function(message) { var statusUpdate JSON.parse(message.body); var friendUsername statusUpdate.user; var isOnline statusUpdate.status ONLINE; updateFriendStatusUI(friendUsername, isOnline); });5. 项目部署、优化与常见问题排查5.1 从开发到生产部署毕业设计不仅要求功能实现也常常需要演示部署。SpringBoot项目部署非常简便。打包与运行打包在项目根目录使用Maven命令mvn clean package -DskipTests会在target目录下生成一个可执行的JAR文件如chat-system-0.0.1-SNAPSHOT.jar。这个JAR包内嵌了Tomcat服务器。运行将JAR包上传到服务器如Linux使用命令java -jar chat-system-0.0.1-SNAPSHOT.jar即可启动。默认使用8080端口。生产配置在src/main/resources下创建application-prod.yml文件覆盖开发环境的配置如数据库地址、日志级别、服务器端口等。启动时指定profilejava -jar -Dspring.profiles.activeprod your-app.jar。数据库初始化确保生产环境的MySQL数据库已创建并运行项目的SQL初始化脚本如果使用了schema.sql和data.sql或通过Flyway/Liquibase这样的数据库迁移工具来管理表结构。前端资源优化如果前端代码较多可以考虑在打包前进行压缩minify。对于SpringBootThymeleaf项目也可以考虑将静态资源JS、CSS放到CDN上以加快加载速度。5.2 性能优化与扩展思考当用户量增长时基础架构可能会遇到瓶颈。WebSocket连接数瓶颈单个Tomcat实例的WebSocket连接数有上限受线程池和内存限制。解决方案是引入WebSocket集群。问题用户A连接到服务器1用户B连接到服务器2。当A发消息给B时服务器1无法直接找到B的会话因为B的连接在服务器2上。解决方案使用消息中间件如RabbitMQ, Redis Pub/Sub, Kafka作为“公共消息总线”。所有服务器实例都订阅这个总线。当服务器1需要发送消息给用户B时它不直接发送而是将消息发布到总线上并标注目标用户是B。所有服务器包括服务器2都会收到这条消息但只有服务器2发现用户B连接在自己这里于是它通过本地WebSocket会话将消息发给B。Spring提供了StompBrokerRelay来支持这种模式可以将消息代理从内存模式切换到真正的消息代理如RabbitMQ。会话与状态共享在集群中之前存储在内存ConcurrentHashMap中的用户-会话映射会失效。需要将这部分信息外部化通常使用Redis来存储。Spring Session项目可以轻松地将HttpSession和WebSocket Session存到Redis中实现多服务器共享。数据库压力消息量巨大时频繁的INSERT操作会成为瓶颈。可以考虑分库分表按用户ID或时间对消息表进行水平拆分。冷热数据分离近期活跃的聊天记录存MySQL历史记录归档到更廉价的存储如对象存储或大数据分析平台。读写分离写操作主库读操作从库。5.3 常见问题与调试技巧实录在开发和调试过程中你肯定会遇到各种问题。这里记录几个典型的“坑”和解决方法。问题1WebSocket连接失败报404错误。可能原因WebSocket端点路径配置错误或Spring Security拦截了WebSocket握手请求。排查检查WebSocketConfig中registerStompEndpoints注册的端点路径如/ws-chat确保前端连接的URL正确如ws://localhost:8080/ws-chat。检查Spring Security配置确保放行了WebSocket握手端点。通常需要在SecurityConfig的configure方法中添加http.authorizeRequests().antMatchers(/ws-chat/**).permitAll();问题2能连接但收不到订阅的消息。可能原因订阅的地址与后端发送消息的目的地不匹配或者消息没有成功发送到消息代理。排查在前端Stomp客户端的connect回调中和send方法前后添加console.log确认连接和发送成功。在后端Controller方法中打日志确认方法被调用。使用浏览器的开发者工具F12- Network - WS查看WebSocket帧。可以看到详细的SUBSCRIBE、SEND、MESSAGE帧这是调试WebSocket通信最直接的方式。确认SEND帧的目的地destination和MESSAGE帧的来源是否匹配你的订阅。问题3用户上下线状态不准确。可能原因SessionDisconnectEvent可能因为网络波动、浏览器意外关闭等原因无法及时触发导致用户状态一直显示在线。解决方案实现心跳机制。前端定期如每30秒通过一个特定的WebSocket目的地如/app/heartbeat向后端发送心跳包。后端收到后更新该用户最后一次心跳时间。同时后端启动一个定时任务定期检查所有在线用户如果某个用户最后一次心跳时间超过阈值如90秒则判定其离线并更新状态、通知其好友。这比单纯依赖断开事件更可靠。问题4多标签页登录导致消息混乱。现象同一个用户在浏览器打开两个标签页登录同一账号一个标签页发送的消息可能在两个标签页都收到或者状态异常。原因两个标签页建立了两个独立的WebSocket连接但都对应同一个用户ID。后端的消息路由可能无法区分或者只记录了最后一个连接。解决思路这是一个设计权衡。简单的系统可以强制“后登录踢前一个”在建立新连接时关闭旧连接。更复杂的系统需要支持多端在线这时就需要维护一个用户到多个会话的映射发送消息时需要遍历该用户的所有活跃会话进行广播。同时前端需要处理同一消息被同一浏览器接收多次的问题可以通过消息ID去重。这个基于SpringBoot的在线聊天系统项目麻雀虽小五脏俱全。它串联起了SpringBoot Web开发、安全认证、实时通信、数据库操作等多个核心知识点。通过动手实现它你不仅能完成一个漂亮的毕业设计更能深入理解现代Web应用中实时交互背后的原理与挑战。在源码的基础上你可以继续探索消息加密、文件传输、表情包、甚至简单的音视频通话WebRTC等功能让它变得更加强大。本文还有配套的精品资源点击获取