最近工作中,需要將原先的Http請求換成WebSocket,故此需要使用到WebSocket與前端交互。故此這邊需要研究一下WebSocket到底有何優點和不可替代性:
WebSocket優點:
WebSocket 協議提供了一種在客戶端和服務器之間進行全雙工通信的機制,這意味著客戶端和服務器可以在任何時候互相發送消息,而不需要預先建立請求。與傳統的 HTTP 輪詢相比,WebSocket 有以下不可替代的優點:
1.?低延遲: WebSocket 提供了真正的實時通信能力,因為它允許服務器在數據可用時立即將其推送到客戶端。這比 HTTP 輪詢的“詢問-回答”模式更高效,輪詢模式可能會引入不必要的延遲。
2.?減少網絡流量: 在 HTTP 輪詢中,客戶端需要定期發送請求以檢查更新,即使沒有更新也是如此。這會產生大量冗余的 HTTP 頭部信息和請求響應。相比之下,WebSocket 在建立連接后,只需要非常少的控制開銷就可以發送和接收消息。
3.?持久連接: WebSocket 使用單個持久連接進行通信,而不需要為每個消息或請求重新建立連接。這減少了頻繁建立和關閉連接的開銷,提高了效率。
4.?雙向通信: WebSocket 支持全雙工通信,客戶端和服務器可以同時發送消息,而不需要等待對方的響應。這對于需要快速雙向數據交換的應用程序來說是非常重要的。
5.?更好的服務器資源利用: 由于 WebSocket 連接是持久的,服務器可以更有效地管理資源,而不是在每個輪詢請求中重新初始化資源。
6.?協議開銷小: WebSocket 消息包含非常少的協議開銷,相比之下,HTTP 協議的每個請求/響應都包含了完整的頭部信息。
7.?支持二進制數據: WebSocket 不僅支持文本數據,還支持二進制數據,這使得它可以用于更廣泛的應用場景,如游戲、視頻流和其他需要高效二進制數據傳輸的應用。
8.?兼容性: 盡管是較新的技術,WebSocket 已經得到了現代瀏覽器的廣泛支持,并且可以通過 Polyfills 在不支持的瀏覽器上使用。
時序圖:
?
這個流程圖展示了以下步驟:
- 握手階段:客戶端向服務器發送 WebSocket 連接請求,服務器響應并切換協議。
- 連接建立:WebSocket 連接建立后,客戶端和服務器可以相互發送消息。
- 通信循環:客戶端和服務器在建立的 WebSocket 連接上進行消息交換。
- 關閉握手:客戶端或服務器發起關閉連接的請求,另一方響應,然后連接關閉。
因為以上優點這邊將需要重新構建一套WebSocket工具類實現這邊的要求:
工具類實現:
在 Spring 中封裝 WebSocket 工具類通常涉及使用 Spring 提供的 WebSocket API。
WebSocketUtils
WebSocket 工具類封裝示例,它使用 Spring 的 WebSocketSession 來發送消息給客戶端。
- 異常處理: 在發送消息時,如果發生異常,我們可以添加更詳細的異常處理邏輯。
- 會話管理: 我們可以添加同步塊或使用 ConcurrentHashMap 的原子操作來確保線程安全。
- 用戶標識符管理: 提供一個更靈活的方式來管理用戶標識符和會話之間的關系。
- 事件發布: 使用 Spring 事件發布機制來解耦和管理 WebSocket 事件。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/*** @Author derek_smart* @Date 202/5/11 10:05* @Description WebSocket 工具類*/
@Component
public class WebSocketUtils extends TextWebSocketHandler {private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();@Autowiredprivate ApplicationEventPublisher eventPublisher;public void registerSession(String userIdentifier, WebSocketSession session) {sessions.put(userIdentifier, session);// Publish an event when a session is registeredeventPublisher.publishEvent(new WebSocketSessionRegisteredEvent(this, session, userIdentifier));}public void removeSession(String userIdentifier) {WebSocketSession session = sessions.remove(userIdentifier);if (session != null) {// Publish an event when a session is removedeventPublisher.publishEvent(new WebSocketSessionRemovedEvent(this, session, userIdentifier));}}public void sendMessageToUser(String userIdentifier, String message) {WebSocketSession session = sessions.get(userIdentifier);if (session != null && session.isOpen()) {try {session.sendMessage(new TextMessage(message));} catch (IOException e) {// Handle the exception, e.g., logging or removing the sessionhandleWebSocketException(session, e);}}}public void sendMessageToAllUsers(String message) {TextMessage textMessage = new TextMessage(message);sessions.forEach((userIdentifier, session) -> {if (session.isOpen()) {try {session.sendMessage(textMessage);} catch (IOException e) {// Handle the exception, e.g., logging or removing the sessionhandleWebSocketException(session, e);}}});}private void handleWebSocketException(WebSocketSession session, IOException e) {// Log the exception// Attempt to close the session if it's still openif (session.isOpen()) {try {session.close();} catch (IOException ex) {// Log the exception during close}}// Remove the session from the mapsessions.values().remove(session);// Further exception handling...}@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {// This method can be overridden to handle connection established event}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {// This method can be overridden to handle connection closed event}// Additional methods like handleTextMessage can be overridden if needed// Custom eventspublic static class WebSocketSessionRegisteredEvent extends ApplicationEvent {private final WebSocketSession session;private final String userIdentifier;/*** Create a new WebSocketSessionRegisteredEvent.* @param source the object on which the event initially occurred (never {@code null})* @param session the WebSocket session which has been registered* @param userIdentifier the identifier of the user for whom the session is registered*/public WebSocketSessionRegisteredEvent(Object source, WebSocketSession session, String userIdentifier) {super(source);this.session = session;this.userIdentifier = userIdentifier;}public WebSocketSession getSession() {return session;}public String getUserIdentifier() {return userIdentifier;}}public static class WebSocketSessionRemovedEvent extends ApplicationEvent {private final WebSocketSession session;private final String userIdentifier;/*** Create a new WebSocketSessionRemovedEvent.* @param source the object on which the event initially occurred (never {@code null})* * @param session the WebSocket session which has been removed* * @param userIdentifier the identifier of the user for whom the session was removed* */public WebSocketSessionRemovedEvent(Object source, WebSocketSession session, String userIdentifier) {super(source);this.session = session;this.userIdentifier = userIdentifier;}public WebSocketSession getSession() {return session;}public String getUserIdentifier() {return userIdentifier;}}
}
?
?
在這個工具類中,我們使用了 ConcurrentHashMap 來存儲和管理 WebSocket 會話。每個會話都與一個用戶標識符相關聯,這允許我們向特定用戶發送消息。 使用了 ApplicationEventPublisher 來發布會話注冊和移除事件,這樣可以讓其他組件在需要時響應這些事件。
另外,我們讓 WebSocketUtils 繼承了 TextWebSocketHandler,這樣它可以直接作為一個 WebSocket 處理器。這意味著你可以重寫
afterConnectionEstablished 和 afterConnectionClosed 方法來處理連接建立和關閉的事件,而不是在一個單獨的 WebSocketHandler 中處理它們。
通過這些優化,WebSocketUtils 工具類變得更加健壯和靈活,能夠更好地集成到 Spring 應用程序中
工具類提供了以下方法:
- registerSession: 當新的 WebSocket 連接打開時,將該會話添加到映射中。
- removeSession: 當 WebSocket 連接關閉時,從映射中移除該會話。
- sendMessageToUser: 向特定用戶發送文本消息。
- sendMessageToAllUsers: 向所有連接的用戶發送文本消息。
- getSessions: 返回當前所有的 WebSocket 會話。
WebSocketHandler
為了完整地實現一個 WebSocket 工具類,你還需要創建一個?WebSocketHandler?來處理 WebSocket 事件,如下所示:
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
/*** @Author derek_smart* @Date 202/5/11 10:05* @Description WebSocketHandler*/
public class MyWebSocketHandler implements WebSocketHandler {private final WebSocketUtils webSocketUtils;public MyWebSocketHandler(WebSocketUtils webSocketUtils) {this.webSocketUtils = webSocketUtils;}@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {String userIdentifier = retrieveUserIdentifier(session);webSocketUtils.registerSession(userIdentifier, session);}@Overridepublic void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {// Handle incoming messages}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {// Handle transport error}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {String userIdentifier = retrieveUserIdentifier(session);webSocketUtils.removeSession(userIdentifier);}@Overridepublic boolean supportsPartialMessages() {return false;}private String retrieveUserIdentifier(WebSocketSession session) {// Implement logic to retrieve the user identifier from the sessionreturn session.getId(); // For example, use the WebSocket session ID}
}
在 MyWebSocketHandler 中,我們處理了 WebSocket 連接的建立和關閉事件,并且在這些事件發生時調用 WebSocketUtils 的方法注冊或移除會話。
WebSocketConfig
要在 Spring 中配置 WebSocket,你需要在配置類中添加 WebSocketHandler 和 WebSocket 的映射。以下是一個簡單的配置示例:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {private final MyWebSocketHandler myWebSocketHandler;public WebSocketConfig(WebSocketUtils webSocketUtils) {this.myWebSocketHandler = new MyWebSocketHandler(webSocketUtils);}@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(myWebSocketHandler, "/ws").setAllowedOrigins("*");}
}
WebSocketEventListener
我們擴展了 ApplicationEvent 類,這是所有 Spring 事件的基類。我們添加了兩個字段:session 和 userIdentifier,以及相應的構造函數和訪問器方法。這樣,當事件被發布時,監聽器可以訪問到事件的詳細信息。
要發布這個事件,你需要注入 ApplicationEventPublisher 到你的 WebSocketUtils 類中,并在會話注冊時調用 publishEvent 方法: 要發布這個事件,你需要在會話移除時調用 ApplicationEventPublisher 的 publishEvent 方法
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;@Component
public class WebSocketEventListener {@EventListenerpublic void handleWebSocketSessionRegistered(WebSocketUtils.WebSocketSessionRegisteredEvent event) {// 處理會話注冊事件WebSocketSession session = event.getSession();String userIdentifier = event.getUserIdentifier();// 你可以在這里添加你的邏輯,例如發送歡迎消息或記錄會話信息}@EventListenerpublic void handleWebSocketSessionRemoved(WebSocketUtils.WebSocketSessionRemovedEvent event) {// 處理會話移除事件WebSocketSession session = event.getSession();String userIdentifier = event.getUserIdentifier();// 你可以在這里添加你的邏輯,例如更新用戶狀態或釋放資源}}
類圖:
?
在這個類圖中,我們展示了以下類及其關系:
- WebSocketUtils: 包含了會話管理和消息發送的方法。
- WebSocketSessionRegisteredEvent: 一個自定義事件類,用于表示 WebSocket 會話注冊事件。
- WebSocketSessionRemovedEvent: 一個自定義事件類,用于表示 WebSocket 會話移除事件。
- WebSocketEventListener: 一個監聽器類,它監聽并處理 WebSocket 會話相關的事件。
- WebSocketController: 一個控制器類,用于處理 HTTP 請求并使用 WebSocketUtils 類的方法。
- ChatWebSocketHandler: 一個 WebSocket 處理器類,用于處理 WebSocket 事件。
- WebSocketConfig: 配置類,用于注冊 WebSocket 處理器。
測試類實現:
WebSocketController
構建一個簡單的聊天應用程序,我們將使用 WebSocketUtils 來管理 WebSocket 會話并向用戶發送消息。 在這個示例中,我們將創建一個控制器來處理發送消息的請求,并使用 WebSocketUtils 類中的方法來實際發送消息。 首先,確保 WebSocketUtils 類已經被定義并包含了之前討論的方法。 接下來,我們將創建一個 WebSocketController 來處理發送消息的請求:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/*** @Author derek_smart* @Date 202/5/11 10:09* @Description WebSocket 測試類*/
@RestController
@RequestMapping("/chat")
public class WebSocketController {private final WebSocketUtils webSocketUtils;@Autowiredpublic WebSocketController(WebSocketUtils webSocketUtils) { this.webSocketUtils = webSocketUtils;}// 發送消息給特定用戶@PostMapping("/send-to-user")public ResponseEntity<?> sendMessageToUser(@RequestParam String userIdentifier, @RequestParam String message) {try {webSocketUtils.sendMessageToUser(userIdentifier, message);return ResponseEntity.ok().build();} catch (IOException e) {// 日志記錄異常,返回錯誤響應return ResponseEntity.status(500).body("Failed to send message to user.");}}// 發送廣播消息給所有用戶@PostMapping("/broadcast")public ResponseEntity<?> broadcastMessage(@RequestParam String message) {webSocketUtils.sendMessageToAllUsers(message);return ResponseEntity.ok().build();}
}
在?WebSocketController?中,我們提供了兩個端點:一個用于向特定用戶發送消息,另一個用于廣播消息給所有連接的用戶。
?
ChatWebSocketHandler
現在,讓我們創建一個?WebSocketHandler?來處理 WebSocket 事件:
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;public class ChatWebSocketHandler extends TextWebSocketHandler {private final WebSocketUtils webSocketUtils;public ChatWebSocketHandler(WebSocketUtils webSocketUtils) {this.webSocketUtils = webSocketUtils;}@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {// 使用用戶的唯一標識符注冊會話String userIdentifier = retrieveUserIdentifier(session);webSocketUtils.registerSession(userIdentifier, session);}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 處理接收到的消息,例如在聊天室中廣播String userIdentifier = retrieveUserIdentifier(session);webSocketUtils.sendMessageToAllUsers("User " + userIdentifier + " says: " + message.getPayload());}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {// 移除會話String userIdentifier = retrieveUserIdentifier(session);webSocketUtils.removeSession(userIdentifier);}private String retrieveUserIdentifier(WebSocketSession session) {// 根據實際情況提取用戶標識符,這里假設使用 WebSocketSession 的 IDreturn session.getId();}
}
在 ChatWebSocketHandler 中,我們處理了連接建立和關閉事件,并在這些事件發生時調用 WebSocketUtils 的方法注冊或移除會話。我們還實現了 handleTextMessage 方法來處理接收到的文本消息。
WebSocketConfig
最后,我們需要配置 WebSocket 端點:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {private final ChatWebSocketHandler chatWebSocketHandler;public WebSocketConfig(WebSocketUtils webSocketUtils) {this.chatWebSocketHandler = new ChatWebSocketHandler(webSocketUtils);}@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(chatWebSocketHandler, "/ws/chat").setAllowedOrigins("*");}
}
在 WebSocketConfig 配置類中,我們注冊了 ChatWebSocketHandler 到 /ws/chat 路徑。這樣,客戶端就可以通過這個路徑來建立 WebSocket 連接。
這個示例展示了如何使用 WebSocketUtils 工具類來管理 WebSocket 會話,并通過 REST 控制器端點發送消息。客戶端可以連接到 WebSocket 端點,并使用提供的 REST 端點發送和接收消息。
總結:
總的來說,WebSocket 在需要快速、實時、雙向通信的應用中提供了顯著的優勢,例如在線游戲、聊天應用、實時數據監控和協作工具。然而,不是所有的場景都需要 WebSocket 的能力,對于不需要實時通信的應用,傳統的 HTTP 請求或 HTTP 輪詢可能仍然是合適的。