Spring結(jié)合WebSocket實(shí)現(xiàn)實(shí)時(shí)通信的教程詳解
簡介
WebSocket 是基于TCP/IP協(xié)議,獨(dú)立于HTTP協(xié)議的通信協(xié)議。WebSocket 連接允許客戶端和服務(wù)器之間的全雙工通信,以便任何一方都可以通過已建立的連接將數(shù)據(jù)推送到另一方。
我們常用的HTTP是客戶端通過「請求-響應(yīng)」的方式與服務(wù)器建立通信的,必須是客戶端主動觸發(fā)的行為,服務(wù)端只是做好接口被動等待請求。而在某些場景下的動作,是需要服務(wù)端主動觸發(fā)的,比如向客戶端發(fā)送消息、實(shí)時(shí)通訊、遠(yuǎn)程控制等??蛻舳耸遣恢肋@些動作幾時(shí)觸發(fā)的,假如用HTTP的方式,那么設(shè)備端需要不斷輪詢服務(wù)端,這樣的方式對服務(wù)器壓力太大,同時(shí)產(chǎn)生很多無效請求,且具有延遲性。于是才采用可以建立雙向通訊的長連接協(xié)議。通過握手建立連接后,服務(wù)端可以實(shí)時(shí)發(fā)送數(shù)據(jù)與指令到設(shè)備端,服務(wù)器壓力小。
Spring WebSocket是Spring框架的一部分,提供了在Web應(yīng)用程序中實(shí)現(xiàn)實(shí)時(shí)雙向通信的能力。本教程將引導(dǎo)你通過一個(gè)簡單的例子,演示如何使用Spring WebSocket建立一個(gè)實(shí)時(shí)通信應(yīng)用。
準(zhǔn)備工作
確保你的項(xiàng)目中已經(jīng)引入了Spring框架的WebSocket模塊。你可以通過Maven添加以下依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
創(chuàng)建WebSocket配置類(實(shí)現(xiàn)WebSocketConfigurer接口)
首先,創(chuàng)建一個(gè)配置類,用于配置WebSocket的相關(guān)設(shè)置。
package com.ci.erp.human.config; import com.ci.erp.human.handler.WebSocketHandler; import com.ci.erp.human.interceptor.WebSocketHandleInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; /** * * Websocket配置類 * * @author lucky_fd * @since 2024-01-17 */ @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // 注冊websocket處理器和攔截器 registry.addHandler(webSocketHandler(), "/websocket/server") .addInterceptors(webSocketHandleInterceptor()).setAllowedOrigins("*"); registry.addHandler(webSocketHandler(), "/sockjs/server").setAllowedOrigins("*") .addInterceptors(webSocketHandleInterceptor()).withSockJS(); } @Bean public WebSocketHandler webSocketHandler() { return new WebSocketHandler(); } @Bean public WebSocketHandleInterceptor webSocketHandleInterceptor() { return new WebSocketHandleInterceptor(); } }
上面的配置類使用@EnableWebSocket注解啟用WebSocket,并通過registerWebSocketHandlers方法注冊WebSocket處理器。
- registerWebSocketHandlers:這個(gè)方法是向spring容器注冊一個(gè)handler處理器及對應(yīng)映射地址,可以理解成MVC的Handler(控制器方法),websocket客戶端通過請求的url查找處理器進(jìn)行處理
- addInterceptors:攔截器,當(dāng)建立websocket連接的時(shí)候,我們可以通過繼承spring的HttpSessionHandshakeInterceptor來做一些事情。
- setAllowedOrigins:跨域設(shè)置,
*
表示所有域名都可以,不限制, 域包括ip:port, 指定*
可以是任意的域名,不加的話默認(rèn)localhost+本服務(wù)端口 - withSockJS: 這個(gè)是應(yīng)對瀏覽器不支持websocket協(xié)議的時(shí)候降級為輪詢的處理。
創(chuàng)建WebSocket消息處理器(實(shí)現(xiàn)TextWebSocketHandler 接口)
接下來,創(chuàng)建一個(gè)消息處理器,處理客戶端發(fā)送的消息。
package com.ci.erp.human.handler; import cn.hutool.core.util.ObjectUtil; import com.ci.erp.common.core.utils.JsonUtils; import com.ci.erp.human.domain.thirdVo.YYHeartbeat; 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.HashMap; import java.util.Map; /** * * websocket處理類 * 實(shí)現(xiàn)WebSocketHandler接口 * * - websocket建立連接后執(zhí)行afterConnectionEstablished回調(diào)接口 * - websocket關(guān)閉連接后執(zhí)行afterConnectionClosed回調(diào)接口 * - websocket接收客戶端消息執(zhí)行handleTextMessage接口 * - websocket傳輸異常時(shí)執(zhí)行handleTransportError接口 * * @author lucky_fd * @since 2024-01-17 */ public class WebSocketHandler extends TextWebSocketHandler { /** * 存儲websocket客戶端連接 * */ private static final Map<String, WebSocketSession> connections = new HashMap<>(); /** * 建立連接后觸發(fā) * */ @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { System.out.println("成功建立websocket連接"); // 建立連接后將連接以鍵值對方式存儲,便于后期向客戶端發(fā)送消息 // 以客戶端連接的唯一標(biāo)識為key,可以通過客戶端發(fā)送唯一標(biāo)識 connections.put(session.getRemoteAddress().getHostName(), session); System.out.println("當(dāng)前客戶端連接數(shù):" + connections.size()); } /** * 接收消息 * */ @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { System.out.println("收到消息: " + message.getPayload()); // 收到客戶端請求消息后進(jìn)行相應(yīng)業(yè)務(wù)處理,返回結(jié)果 this.sendMessage(session.getRemoteAddress().getHostName(),new TextMessage("收到消息: " + message.getPayload())); } /** * 傳輸異常處理 * */ @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { super.handleTransportError(session, exception); } /** * 關(guān)閉連接時(shí)觸發(fā) * */ @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { System.out.println("觸發(fā)關(guān)閉websocket連接"); // 移除連接 connections.remove(session.getRemoteAddress().getHostName()); } @Override public boolean supportsPartialMessages() { return super.supportsPartialMessages(); } /** * 向連接的客戶端發(fā)送消息 * * @author lucky_fd * @param clientId 客戶端標(biāo)識 * @param message 消息體 **/ public void sendMessage(String clientId, TextMessage message) { for (String client : connections.keySet()) { if (client.equals(clientId)) { try { WebSocketSession session = connections.get(client); // 判斷連接是否正常 if (session.isOpen()) { session.sendMessage(message); } } catch (IOException e) { System.out.println(e.getMessage()); } break; } } } }
通過消息處理器,在開發(fā)中我們就可以實(shí)現(xiàn)向指定客戶端或所有客戶端發(fā)送消息,實(shí)現(xiàn)相應(yīng)業(yè)務(wù)功能。
創(chuàng)建攔截器
攔截器會在握手時(shí)觸發(fā),可以用來進(jìn)行權(quán)限驗(yàn)證
package com.ci.erp.human.interceptor; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; import java.util.Map; /** * * Websocket攔截器類 * * @author lucky_fd * @since 2024-01-17 */ public class WebSocketHandleInterceptor extends HttpSessionHandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { System.out.println("攔截器前置觸發(fā)"); return super.beforeHandshake(request, response, wsHandler, attributes); } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { System.out.println("攔截器后置觸發(fā)"); super.afterHandshake(request, response, wsHandler, ex); } }
創(chuàng)建前端頁面客戶端
最后,創(chuàng)建一個(gè)簡單的HTML頁面,用于接收用戶輸入并顯示實(shí)時(shí)聊天信息。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Spring WebSocket Chat</title> <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script> <script src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.js"></script> </head> <body> 請輸入:<input type="text" id="message" placeholder="Type your message"> <button onclick="sendMessage()">Send</button> <button onclick="websocketClose()">關(guān)閉連接</button> <div id="chat"></div> <script> var socket = null; if ('WebSocket' in window) { // 后端服務(wù)port為22900 socket = new WebSocket("ws://localhost:22900/websocket/server"); } else if ('MozWebSocket' in window) { socket = new MozWebSocket("ws://localhost:22900/websocket/server"); } else { socket = new SockJS("http://localhost:22900/sockjs/server"); } // 接收消息觸發(fā) socket.onmessage = function (event) { showMessage(event.data); }; // 創(chuàng)建連接觸發(fā) socket.onopen = function (event) { console.log(event.type); }; // 連接異常觸發(fā) socket.onerror = function (event) { console.log(event) }; // 關(guān)閉連接觸發(fā) socket.onclose = function (closeEvent) { console.log(closeEvent.reason); }; //發(fā)送消息 function sendMessage() { if (socket.readyState === socket.OPEN) { var message = document.getElementById('message').value; socket.send(message); console.log("發(fā)送成功!"); } else { console.log("連接失敗!"); } } function showMessage(message) { document.getElementById('chat').innerHTML += '<p>' + message + '</p>'; } function websocketClose() { socket.close(); console.log("連接關(guān)閉"); } window.close = function () { socket.onclose(); }; </script> </body> </html>
這個(gè)頁面使用了WebSocket對象來建立連接,并通過onmessage監(jiān)聽收到的消息。通過輸入框發(fā)送消息,將會在頁面上顯示。
測試結(jié)果:
后端日志:
前端界面:
Java客戶端
添加依賴
<dependency> <groupId>org.java-websocket</groupId> <artifactId>Java-WebSocket</artifactId> <version>1.4.0</version> </dependency>
創(chuàng)建客戶端類(繼承WebsocketClient)
package com.river.websocket; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; import java.net.URI; import java.net.URISyntaxException; public class MyWebSocketClient extends WebSocketClient { MyWebSocketClient(String url) throws URISyntaxException { super(new URI(url)); } // 建立連接 @Override public void onOpen(ServerHandshake shake) { System.out.println(shake.getHttpStatusMessage()); } // 接收消息 @Override public void onMessage(String paramString) { System.out.println(paramString); } // 關(guān)閉連接 @Override public void onClose(int paramInt, String paramString, boolean paramBoolean) { System.out.println("關(guān)閉"); } // 連接異常 @Override public void onError(Exception e) { System.out.println("發(fā)生錯(cuò)誤"); } }
測試websocket
package com.river.websocket; import org.java_websocket.enums.ReadyState; import java.net.URISyntaxException; /** * @author lucky_fd * @date 2024-1-17 */ public class Client { public static void main(String[] args) throws URISyntaxException, InterruptedException { MyWebSocketClient client = new MyWebSocketClient("ws://localhost:22900/websocket/server"); client.connect(); while (client.getReadyState() != ReadyState.OPEN) { System.out.println("連接狀態(tài):" + client.getReadyState()); Thread.sleep(100); } client.send("測試數(shù)據(jù)!"); client.close(); } }
到此這篇關(guān)于Spring結(jié)合WebSocket實(shí)現(xiàn)實(shí)時(shí)通信的教程詳解的文章就介紹到這了,更多相關(guān)Spring WebSocket實(shí)時(shí)通信內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解mybatis插入數(shù)據(jù)后返回自增主鍵ID的問題
這篇文章主要介紹了mybatis插入數(shù)據(jù)后返回自增主鍵ID詳解,本文通過場景分析示例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-07-07超個(gè)性修改SpringBoot項(xiàng)目的啟動banner的方法
這篇文章主要介紹了超個(gè)性修改SpringBoot項(xiàng)目的啟動banner的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03Java使用CompletableFuture進(jìn)行非阻塞IO詳解
這篇文章主要介紹了Java使用CompletableFuture進(jìn)行非阻塞IO詳解,CompletableFuture是Java中的一個(gè)類,用于支持異步編程和處理異步任務(wù)的結(jié)果,它提供了一種方便的方式來處理異步操作,并允許我們以非阻塞的方式執(zhí)行任務(wù),需要的朋友可以參考下2023-09-09使用Java自定義注解實(shí)現(xiàn)一個(gè)簡單的令牌桶限流器
限流是在分布式系統(tǒng)中常用的一種策略,它可以有效地控制系統(tǒng)的訪問流量,保證系統(tǒng)的穩(wěn)定性和可靠性,在本文中,我將介紹如何使用Java自定義注解來實(shí)現(xiàn)一個(gè)簡單的令牌桶限流器,需要的朋友可以參考下2023-10-10將ResultSet中得到的一行或多行結(jié)果集封裝成對象的實(shí)例
這篇文章主要介紹了將ResultSet中得到的一行或多行結(jié)果集封裝成對象的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-05-05