大家好,我是小悟。
一、WebSocket是什么?—— HTTP的“社恐”表弟变身“社交牛逼症”
想象一下HTTP协议是个有点“社恐”的程序员:
- 每次聊天都要先说“你好”,对方回“你好”,然后才能说正事
- 说完一句话就必须闭嘴,等对方回应才能说下一句
- 想实时聊天?得不停地问“你有新消息吗?”“现在呢?”“现在呢?”
而WebSocket就像HTTP喝了十杯咖啡的表弟:
- 一次握手,终身连接(直到你主动分手)
- 双向通话,随时插话
- 真正的“你一句我一句”,不再是你问一句我答一句的“审讯式聊天”
// HTTP vs WebSocket 的日常对话对比 // HTTP的尬聊场景: 你:喂,在吗?(请求) 服务器:在的(响应) 你:吃了吗?(请求) 服务器:吃了(响应) 你:吃的啥?(请求) 服务器:...你烦不烦(响应) // WebSocket的畅聊场景: 你:<连接建立> 你:吃了吗? 服务器:吃了,吃的炸鸡 服务器:你要不要也来点? 你:要要要!加杯可乐! // ... 自由流畅的对话继续
二、SpringBoot集成WebSocket详细步骤
第1步:引入依赖——给项目“灌咖啡”
<!-- pom.xml --> <dependencies> <!-- SpringBoot的WebSocket“咖啡包” --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- 前端用Stomp的“吸管”喝咖啡 --> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.4</version> </dependency> <!-- 前端用SockJS的“备用吸管”(万一主吸管坏了) --> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.5.1</version> </dependency> </dependencies>
第2步:配置类——搭建聊天室的“基础设施”
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; /** * WebSocket配置类 * 想象成给聊天室装上门、窗户和广播喇叭 */ @Configuration @EnableWebSocketMessageBroker // 这句咒语的意思是:“芝麻开门,我要用WebSocket!” public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { // 配置消息代理,相当于设置聊天室的“广播站” config.enableSimpleBroker("/topic", "/queue"); // 简单内存代理 // 设置应用程序的目的地前缀 // 客户端发送消息到 /app/xxx,就像寄信要写“XX省XX市” config.setApplicationDestinationPrefixes("/app"); // 用户私聊前缀(点对点) config.setUserDestinationPrefix("/user"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 注册WebSocket端点,相当于给聊天室安装“大门” registry.addEndpoint("/ws-chat") .setAllowedOriginPatterns("*") // 允许所有来源(生产环境别这么干!) .withSockJS(); // 后备选项,万一浏览器太老,就降级使用HTTP长轮询 // 再来一个不带SockJS的,给现代浏览器用 registry.addEndpoint("/ws-chat") .setAllowedOriginPatterns("*"); System.out.println("聊天室大门已安装!门牌号:/ws-chat"); System.out.println("备用方案:SockJS已就位,IE6也能凑合用(大概吧)"); } }
第3步:消息控制器——聊天室的“主持人”
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.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * 聊天控制器 * 这位就是聊天室的主持人,负责喊:“XX说了一句话,大家快听!” */ @Controller public class ChatController { private final SimpMessagingTemplate messagingTemplate; public ChatController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; System.out.println("聊天主持人已就位,话筒测试:喂喂喂~"); } /** * 广播消息 - 大厅聊天 * 客户端发送到:/app/chat.sendMessage * 服务端广播到:/topic/public */ @MessageMapping("/chat.sendMessage") // 接收消息的“信箱” @SendTo("/topic/public") // 广播的“大喇叭” public ChatMessage sendMessage(@Payload ChatMessage chatMessage) { // 给消息打个时间戳,就像聊天记录的时间标记 chatMessage.setTimestamp(LocalDateTime.now().format( DateTimeFormatter.ofPattern("HH:mm:ss") )); System.out.println("广播消息:" + chatMessage.getSender() + "说:" + chatMessage.getContent()); // 如果是系统消息(比如有人加入/退出) if (chatMessage.getType() == MessageType.JOIN) { chatMessage.setContent(chatMessage.getSender() + " 闪亮登场!"); } else if (chatMessage.getType() == MessageType.LEAVE) { chatMessage.setContent(chatMessage.getSender() + " 溜了溜了~ "); } return chatMessage; } /** * 用户加入 - 相当于进门喊一声“我来了!” */ @MessageMapping("/chat.addUser") @SendTo("/topic/public") public ChatMessage addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { // 在WebSocket会话中保存用户名,就像给用户发个“名牌” headerAccessor.getSessionAttributes().put("username", chatMessage.getSender()); chatMessage.setType(MessageType.JOIN); chatMessage.setTimestamp(LocalDateTime.now().format( DateTimeFormatter.ofPattern("HH:mm:ss") )); System.out.println("新用户加入:" + chatMessage.getSender()); System.out.println("当前在线人数:+1(我也不会算,大概吧)"); return chatMessage; } /** * 私聊功能 - 偷偷说悄悄话 * @param to 接收者用户名 */ @MessageMapping("/chat.private") public void privateMessage(@Payload ChatMessage chatMessage, @Header("to") String toUser) { chatMessage.setTimestamp(LocalDateTime.now().format( DateTimeFormatter.ofPattern("HH:mm:ss") )); System.out.println("私聊消息:" + chatMessage.getSender() + " 悄悄对 " + toUser + " 说:" + chatMessage.getContent()); // 发送给特定用户:/user/{用户名}/queue/private messagingTemplate.convertAndSendToUser( toUser, "/queue/private", chatMessage ); // 也发给自己,让自己看到发送的消息 messagingTemplate.convertAndSendToUser( chatMessage.getSender(), "/queue/private", chatMessage ); } /** * 消息模型类 - 聊天的“语言规范” */ public static class ChatMessage { private MessageType type; // 消息类型 private String content; // 消息内容 private String sender; // 发送者 private String timestamp; // 时间戳 // 构造方法、getter、setter省略(但实际必须要有!) // 想象成:每个消息都要有信封、信纸、写信人、写信时间 } /** * 消息类型枚举 - 聊天表情包分类 */ public enum MessageType { CHAT, // 普通聊天 JOIN, // 加入 LEAVE // 离开 } }
第4步:前端实现——用户的“聊天界面”
<!-- chat.html --> <!DOCTYPE html> <html> <head> <title>SpringBoot聊天室 - 禁止讨论为什么代码又报错</title> <style> body { font-family: 'Comic Sans MS', cursive; } #chat-container { border: 3px solid #4CAF50; border-radius: 15px; padding: 20px; max-width: 800px; margin: 0 auto; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); } #message-area { height: 400px; overflow-y: auto; border: 2px dashed #ccc; padding: 10px; margin-bottom: 20px; background: white; border-radius: 10px; } .message { margin: 10px 0; padding: 10px; border-radius: 10px; } .my-message { background: #e3f2fd; text-align: right; } .their-message { background: #f1f8e9; } .system-message { background: #fff3e0; text-align: center; font-style: italic; color: #ff9800; } .join-message { color: #4CAF50; } .leave-message { color: #f44336; } </style> </head> <body> <div id="chat-container"> <h1>SpringBoot聊天室</h1> <h3>当前状态:<span id="status">正在连接...</span></h3> <div id="connect-area"> <input type="text" id="username" placeholder="取个霸气的昵称" /> <button onclick="connect()" id="connect-btn">进入聊天室</button> </div> <div id="chat-area" style="display:none;"> <div id="message-area"></div> <div> <input type="text" id="message-input" placeholder="说点什么吧..." style="width: 70%; padding: 10px;" onkeypress="if(event.keyCode===13) sendMessage()" /> <button onclick="sendMessage()" style="padding: 10px;">发送</button> </div> <div style="margin-top: 20px;"> <input type="text" id="private-to" placeholder="私聊对象昵称" /> <input type="text" id="private-message" placeholder="悄悄话内容" /> <button onclick="sendPrivate()">发送悄悄话</button> </div> </div> </div> <!-- 引入WebSocket客户端库 --> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script> let stompClient = null; let username = null; // 连接WebSocket - 相当于“敲门” function connect() { username = document.getElementById('username').value.trim(); if (!username) { alert("请先取个昵称!不能叫'无名氏'吧?"); return; } const socket = new SockJS('/ws-chat'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { console.log("连接成功!服务器说:" + frame); document.getElementById('status').innerHTML = "已连接"; document.getElementById('connect-area').style.display = 'none'; document.getElementById('chat-area').style.display = 'block'; // 订阅公共频道 - 相当于“坐在大厅听广播” stompClient.subscribe('/topic/public', function(message) { showMessage(JSON.parse(message.body)); }); // 订阅私人频道 - 相当于“戴上耳机听悄悄话” stompClient.subscribe('/user/queue/private', function(message) { const msg = JSON.parse(message.body); msg.isPrivate = true; showMessage(msg); }); // 发送加入消息 stompClient.send("/app/chat.addUser", {}, JSON.stringify({sender: username, type: 'JOIN'}) ); }, function(error) { console.log("连接失败:" + error); document.getElementById('status').innerHTML = "连接失败"; }); } // 发送消息 - 相当于“对着话筒喊话” function sendMessage() { const messageInput = document.getElementById('message-input'); const content = messageInput.value.trim(); if (content && stompClient) { const chatMessage = { sender: username, content: content, type: 'CHAT' }; stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage) ); messageInput.value = ''; } } // 发送私聊 function sendPrivate() { const toUser = document.getElementById('private-to').value.trim(); const content = document.getElementById('private-message').value.trim(); if (toUser && content && stompClient) { stompClient.send("/app/chat.private", {to: toUser}, JSON.stringify({ sender: username, content: content, type: 'CHAT' }) ); document.getElementById('private-message').value = ''; } } // 显示消息 - 相当于“把话写在聊天记录上” function showMessage(message) { const messageArea = document.getElementById('message-area'); const messageElement = document.createElement('div'); messageElement.classList.add('message'); // 根据不同消息类型设置样式 if (message.isPrivate) { messageElement.innerHTML = ` <strong>${message.sender} 悄悄对你说:</strong> <br/>${message.content} <br/><small>${message.timestamp}</small> `; messageElement.style.background = '#fce4ec'; } else if (message.type === 'JOIN') { messageElement.classList.add('system-message', 'join-message'); messageElement.innerHTML = `${message.content}`; } else if (message.type === 'LEAVE') { messageElement.classList.add('system-message', 'leave-message'); messageElement.innerHTML = `${message.content}`; } else if (message.sender === username) { messageElement.classList.add('my-message'); messageElement.innerHTML = ` <strong>我:</strong>${message.content} <br/><small>${message.timestamp}</small> `; } else { messageElement.classList.add('their-message'); messageElement.innerHTML = ` <strong>${message.sender}:</strong>${message.content} <br/><small>${message.timestamp}</small> `; } messageArea.appendChild(messageElement); messageArea.scrollTop = messageArea.scrollHeight; } // 页面关闭时发送离开消息 window.addEventListener('beforeunload', function() { if (stompClient && username) { stompClient.send("/app/chat.sendMessage", {}, JSON.stringify({ sender: username, type: 'LEAVE', content: '' }) ); } }); </script> </body> </html>
第5步:进阶功能——让聊天室更“炫酷”
// 1. 在线用户管理 @Component public class ChatUserService { private final Set<String> onlineUsers = ConcurrentHashMap.newKeySet(); public void userConnected(String username) { onlineUsers.add(username); broadcastOnlineUsers(); } public void userDisconnected(String username) { onlineUsers.remove(username); broadcastOnlineUsers(); } private void broadcastOnlineUsers() { messagingTemplate.convertAndSend("/topic/onlineUsers", onlineUsers); } } // 2. 消息持久化(保存聊天记录) @Service public class ChatMessageService { @Autowired private ChatMessageRepository repository; public void saveMessage(ChatMessage message) { repository.save(message); System.out.println("消息已保存到数据库,以后可以翻旧账了"); } public List<ChatMessage> getRecentMessages() { return repository.findTop50ByOrderByTimestampDesc(); } } // 3. 消息拦截器(敏感词过滤) @Component public class ChatInterceptor implements ChannelInterceptor { private final String[] sensitiveWords = {"密码", "银行卡", "V我50"}; @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { String content = message.getPayload().toString(); for (String word : sensitiveWords) { if (content.contains(word)) { System.out.println("检测到敏感词,消息已被吞掉"); return null; // 吞掉消息 } } return message; } }
三、总结:从“HTTP的尬聊”到“WebSocket的畅聊”
WebSocket的优势:
- 真正的双向通信:不再是“你问我答”,而是“畅所欲言”
- 低延迟:消息实时到达,不用等HTTP的“快递员”来回跑
- 减少带宽:一次握手,多次通信,省去了HTTP的“客套话”
- 更少的服务器压力:不用维护成千上万的轮询请求
SpringBoot集成WebSocket的核心思想:
- 配置是骨架:
@EnableWebSocketMessageBroker是激活咒语 - 控制器是大脑:
@MessageMapping指定消息接收点 - 消息代理是广播站:
SimpleBroker负责消息分发 - STOMP是协议翻译官:把WebSocket的消息翻译成大家都能懂的语言
开发心得:
- 前端连接记住三步曲:SockJS创建连接 → Stomp封装协议 → 订阅/发送消息
- 后端开发记住三注解:
@MessageMapping(收信)、@SendTo(广播)、@Payload(取内容) - 生产环境要加料:认证、授权、SSL、集群支持、监控指标…
最后:
从前,HTTP每次聊天都要重新握手,像极了社恐人士每次开口前都要心理建设半天。现在,WebSocket一次握手终身连接,就像好哥们儿之间:“别废话,直接说!”而SpringBoot就是那个贴心的管家,帮你把WebSocket的各种复杂配置都打包好,你只需要:
- 加个依赖(点杯咖啡)
- 写个配置类(摆好桌椅)
- 写个控制器(找个主持人)
- 前端连一下(客人入场)
然后就可以享受:高性能、低延迟、全双工的聊天体验!
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海