大家好,我是小悟。
一、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的各种复杂配置都打包好,你只需要:
- 加个依赖(点杯咖啡)
- 写个配置类(摆好桌椅)
- 写个控制器(找个主持人)
- 前端连一下(客人入场)
然后就可以享受:高性能、低延迟、全双工的聊天体验!
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海