SpringBoot項目中使用WebSocket實現(xiàn)實時通信功能
一、前言
在傳統(tǒng)的 HTTP 通信中,客戶端發(fā)起請求,服務(wù)器給出響應(yīng),一次通信就此結(jié)束。這種模式對于靜態(tài)頁面展示完全夠用,但對于需要實時推送的場景——如在線聊天、實時通知、股票行情、設(shè)備狀態(tài)監(jiān)控——就力不從心了。
早期的解決方案是輪詢:客戶端每隔幾秒就發(fā)一次請求問服務(wù)器"有新消息嗎",效率低下且浪費資源。WebSocket 的出現(xiàn)徹底改變了這一局面。
二、WebSocket 是什么
WebSocket 是一種在單個 TCP 連接上進行全雙工通信的網(wǎng)絡(luò)協(xié)議,由 HTML5 規(guī)范引入,RFC 6455 正式定義。
2.1 與 HTTP 的核心區(qū)別
| 對比項 | HTTP | WebSocket |
|---|---|---|
| 通信方向 | 單向(客戶端請求,服務(wù)器響應(yīng)) | 雙向(任意一方均可主動發(fā)送) |
| 連接狀態(tài) | 無狀態(tài),一問一答后斷開 | 有狀態(tài),連接建立后持續(xù)保持 |
| 協(xié)議頭開銷 | 每次請求都攜帶完整 Header | 握手一次后,后續(xù)幀頭極?。?~10字節(jié)) |
| 適用場景 | 普通頁面請求 | 實時推送、聊天、游戲 |
2.2 握手過程
WebSocket 復(fù)用了 HTTP 的握手機制,通過一次 HTTP 請求完成協(xié)議升級:
客戶端發(fā)送升級請求: GET /ws/chat HTTP/1.1 Host: localhost:8080 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 服務(wù)器返回: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
握手成功后,協(xié)議從 HTTP 升級為 WebSocket,后續(xù)通信不再經(jīng)過 HTTP 層,直接在 TCP 連接上傳輸 WebSocket 幀。
三、Spring Boot 集成 WebSocket
Spring Boot 提供了兩種方式使用 WebSocket:
- 方式一:基于 Java EE 標(biāo)準的
@ServerEndpoint(本文重點介紹) - 方式二:基于 Spring 的
WebSocketHandler
3.1 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>3.2 注冊配置類
使用內(nèi)置 Tomcat 時,需要手動注冊 ServerEndpointExporter,讓 Spring 能掃描到 @ServerEndpoint 注解:
@Configuration
public class WebSocketConfig {
/**
* 使用內(nèi)置 Tomcat 時必須注入此 Bean
* 若使用外部 Tomcat 部署,則不需要,容器會自行管理
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}3.3 編寫 WebSocket 端點
@Component
@ServerEndpoint("/ws/chat/{userId}")
public class ChatWebSocket {
private static final Logger log = LoggerFactory.getLogger(ChatWebSocket.class);
// 存儲所有在線連接,key=userId,value=WebSocket會話
private static final ConcurrentHashMap<String, Session> SESSIONS = new ConcurrentHashMap<>();
/**
* 連接建立時觸發(fā)
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
SESSIONS.put(userId, session);
log.info("用戶 {} 上線,當(dāng)前在線人數(shù):{}", userId, SESSIONS.size());
}
/**
* 收到客戶端消息時觸發(fā)
*/
@OnMessage
public void onMessage(String message, @PathParam("userId") String userId) {
log.info("收到用戶 {} 的消息:{}", userId, message);
// 解析消息,轉(zhuǎn)發(fā)給目標(biāo)用戶
handleMessage(userId, message);
}
/**
* 連接關(guān)閉時觸發(fā)
*/
@OnClose
public void onClose(@PathParam("userId") String userId) {
SESSIONS.remove(userId);
log.info("用戶 {} 下線,當(dāng)前在線人數(shù):{}", userId, SESSIONS.size());
}
/**
* 發(fā)生異常時觸發(fā)
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket 發(fā)生異常:{}", error.getMessage());
}
/**
* 向指定用戶發(fā)送消息
*/
public static void sendToUser(String userId, String message) {
Session session = SESSIONS.get(userId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("向用戶 {} 發(fā)送消息失敗:{}", userId, e.getMessage());
}
}
}
/**
* 廣播消息給所有在線用戶
*/
public static void broadcast(String message) {
SESSIONS.forEach((userId, session) -> {
if (session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("廣播消息失敗,userId={}:{}", userId, e.getMessage());
}
}
});
}
/**
* 處理消息邏輯(示例:解析JSON轉(zhuǎn)發(fā))
*/
private void handleMessage(String fromUserId, String message) {
try {
// 假設(shè)消息格式為 JSON:{"toUserId":"xxx","content":"hello"}
JSONObject json = JSONObject.parseObject(message);
String toUserId = json.getString("toUserId");
String content = json.getString("content");
JSONObject response = new JSONObject();
response.put("fromUserId", fromUserId);
response.put("content", content);
response.put("time", System.currentTimeMillis());
sendToUser(toUserId, response.toJSONString());
} catch (Exception e) {
log.error("消息處理失敗:{}", e.getMessage());
}
}
}3.4 在業(yè)務(wù)代碼中主動推送
WebSocket 不只能被動接收消息,也可以在任意業(yè)務(wù)邏輯中主動向客戶端推送:
@Service
public class OrderService {
public void completeOrder(Long orderId, Long userId) {
// 處理訂單完成邏輯...
// 訂單完成后,主動推送通知給用戶
JSONObject notify = new JSONObject();
notify.put("type", "ORDER_COMPLETE");
notify.put("orderId", orderId);
notify.put("message", "您的訂單已完成,請及時查看");
ChatWebSocket.sendToUser(String.valueOf(userId), notify.toJSONString());
}
}四、前端對接
<!DOCTYPE html>
<html>
<body>
<script>
const userId = "user_001";
const ws = new WebSocket(`ws://localhost:8080/ws/chat/${userId}`);
// 連接建立
ws.onopen = function () {
console.log("WebSocket 連接成功");
ws.send(JSON.stringify({
toUserId: "user_002",
content: "你好!"
}));
};
// 收到消息
ws.onmessage = function (event) {
const data = JSON.parse(event.data);
console.log(`收到來自 ${data.fromUserId} 的消息:${data.content}`);
};
// 連接關(guān)閉
ws.onclose = function () {
console.log("WebSocket 連接已關(guān)閉");
};
// 發(fā)生錯誤
ws.onerror = function (error) {
console.error("WebSocket 錯誤:", error);
};
</script>
</body>
</html>五、生產(chǎn)環(huán)境注意事項
5.1 @ServerEndpoint 無法注入 Spring Bean 的問題
@ServerEndpoint 的實例由 Tomcat 管理,不是 Spring Bean,所以直接用 @Autowired 注入會失?。?/p>
@ServerEndpoint("/ws/chat/{userId}")
public class ChatWebSocket {
@Autowired
private UserService userService; // ? 注入失敗,值為 null
}解決方案:通過靜態(tài)變量 + ApplicationContext 獲取
@ServerEndpoint("/ws/chat/{userId}")
public class ChatWebSocket implements ApplicationContextAware {
private static UserService userService;
@Override
public void setApplicationContext(ApplicationContext context) {
userService = context.getBean(UserService.class);
}
}或者更簡潔地,在配置類里提前拿到:
@Component
public class WebSocketBeanFactory implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
context = applicationContext;
}
public static <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
}
// 在 WebSocket 端點里使用
UserService userService = WebSocketBeanFactory.getBean(UserService.class);5.2 集群部署下的消息廣播問題
單機部署時,所有連接都在同一個 JVM 里,ConcurrentHashMap 存儲會話沒問題。但集群部署時,用戶 A 連接在節(jié)點1,用戶 B 連接在節(jié)點2,節(jié)點1無法直接找到用戶 B 的 Session。
解決方案:引入消息中間件(如 RabbitMQ / Redis Pub-Sub)
節(jié)點1收到消息
↓
發(fā)布到 MQ / Redis Channel
↓
所有節(jié)點訂閱并消費消息
↓
各節(jié)點檢查自己管理的 Session 里有沒有目標(biāo)用戶
↓
有則發(fā)送,無則忽略5.3 連接心跳?;?/h3>
長時間無數(shù)據(jù)交換時,防火墻或負載均衡器可能會斷開連接,需要定期發(fā)送心跳:
// 前端每 30 秒發(fā)送一次心跳
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "PING" }));
}
}, 30000);// 后端收到心跳回復(fù)
@OnMessage
public void onMessage(String message, Session session) {
JSONObject json = JSONObject.parseObject(message);
if ("PING".equals(json.getString("type"))) {
try {
session.getBasicRemote().sendText("{\"type\":\"PONG\"}");
} catch (IOException e) {
log.error("心跳回復(fù)失敗");
}
}
}5.4 連接斷開重連
網(wǎng)絡(luò)抖動時前端需要自動重連:
function createWebSocket(userId) {
const ws = new WebSocket(`ws://localhost:8080/ws/chat/${userId}`);
ws.onclose = function () {
console.log("連接斷開,3秒后重連...");
setTimeout(() => createWebSocket(userId), 3000);
};
return ws;
}六、完整流程總結(jié)
1. 引入 spring-boot-starter-websocket 依賴
↓
2. 注冊 ServerEndpointExporter Bean(內(nèi)置Tomcat必須)
↓
3. 編寫 @ServerEndpoint 端點類
實現(xiàn) @OnOpen / @OnMessage / @OnClose / @OnError
↓
4. 用 ConcurrentHashMap 管理所有在線 Session
↓
5. 業(yè)務(wù)代碼調(diào)用靜態(tài)方法主動推送消息
↓
6. 前端用 new WebSocket(url) 建立連接
通過 onmessage 接收,ws.send() 發(fā)送七、適用場景總結(jié)
| 場景 | 說明 |
|---|---|
| 在線聊天 | 用戶之間實時發(fā)送消息 |
| 實時通知 | 訂單完成、充值到賬、審批結(jié)果 |
| 設(shè)備監(jiān)控 | 硬件設(shè)備實時上報狀態(tài)數(shù)據(jù) |
| 協(xié)同編輯 | 多人同時編輯同一文檔 |
| 行情推送 | 股票、加密貨幣實時價格 |
| 在線游戲 | 多人游戲?qū)崟r同步狀態(tài) |
WebSocket 適合高頻、雙向、實時的通信場景。如果只是服務(wù)器單向推送(如消息通知),也可以考慮更輕量的 SSE(Server-Sent Events);如果實時性要求不高,簡單的短輪詢也能滿足需求。選擇合適的技術(shù),比盲目使用 WebSocket 更重要。
以上就是SpringBoot項目中使用WebSocket實現(xiàn)實時通信功能的詳細內(nèi)容,更多關(guān)于SpringBoot WebSocket實時通信的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot返回統(tǒng)一的JSON標(biāo)準格式實現(xiàn)步驟
這篇文章主要介紹了SpringBoot返回統(tǒng)一的JSON標(biāo)準格式,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08
Springboot的spring-boot-maven-plugin導(dǎo)入失敗的解決方案
這篇文章主要介紹了Springboot的spring-boot-maven-plugin導(dǎo)入失敗的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
SpringMVC核心原理與前后端數(shù)據(jù)交互機制詳解(推薦)
文章詳細介紹了SpringMVC的核心原理及前后端數(shù)據(jù)交互機制,包括SpringMVC的概述、架構(gòu)、工作流程、請求參數(shù)綁定、JSON數(shù)據(jù)處理、數(shù)據(jù)驗證、異常處理以及最佳實踐,感興趣的朋友跟隨小編一起看看吧2025-11-11
Java中控制多線程順序執(zhí)行的六種實現(xiàn)方案
在多線程編程中,線程的執(zhí)行順序本質(zhì)上是不確定的,由操作系統(tǒng)調(diào)度器決定,但在某些業(yè)務(wù)場景中,我們需要確保線程按照特定順序執(zhí)行,所以本文介紹了Java中控制多線程順序執(zhí)行的六種實現(xiàn)方案,需要的朋友可以參考下2025-09-09
一起來學(xué)習(xí)Java IO的轉(zhuǎn)化流
這篇文章主要為大家詳細介紹了Java IO的轉(zhuǎn)化流,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03
從@CrossOrigin到Gateway詳解Spring Boot跨域處理的10種姿勢
在前后端分離架構(gòu)成為主流的今天,跨域問題已成為每個Web開發(fā)者必須面對的挑戰(zhàn),本文將系統(tǒng)性地介紹10種Spring Boot跨域處理方案,下面小編就和大家簡單介紹一下吧2025-11-11

