場景
WebSocket
HTTP 協議是一種無狀態的、無連接的、單向的應用層協議。它采用了請求/響應模型。通信請求只能由客戶端發起,服務端對請求做出應答處理。
這種通信模型有一個弊端:HTTP 協議無法實現服務器主動向客戶端發起消息。
這種單向請求的特點,注定了如果服務器有連續的狀態變化,客戶端要獲知就非常麻煩。大多數 Web 應用程序將通過頻繁的異步 JavaScript 和 XML(AJAX)請求實現長輪詢。輪詢的效率低,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)。
WebSocket 就是這樣發明的。WebSocket 連接允許客戶端和服務器之間進行全雙工通信,以便任一方都可以通過建立的連接將數據推送到另一端。WebSocket 只需要建立一次連接,就可以一直保持連接狀態。這相比於輪詢方式的不停建立連接顯然效率要大大提高。
若依前后端分離版手把手教你本地搭建環境並運行項目:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108465662
在上面將前后端的項目搭建起來之后。進行后台SpringBoot和Vue的WebSocket集成。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
關注公眾號
霸道的程序猿
獲取編程相關電子書、教程推送與免費下載。
實現
SpringBoot集成
首先在pom文件中引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
然后新建一個WebSocket的配置類,用來開啟WebSocket的支持
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
然后新建一個客戶端實體類WebSocketClient用來存儲連接的Session和Uri
import javax.websocket.Session; public class WebSocketClient { // 與某個客戶端的連接會話,需要通過它來給客戶端發送數據 private Session session; //連接的uri private String uri; public Session getSession() { return session; } public void setSession(Session session) { this.session = session; } public String getUri() { return uri; } public void setUri(String uri) { this.uri = uri; } }
然后新建WebSocketService,用來創建和處理連接
import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; @ServerEndpoint(value = "/websocket/{userName}") @Component public class WebSocketService { private static final Logger log = LoggerFactory.getLogger(WebSocketService.class); //靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。 private static int onlineCount = 0; //concurrent包的線程安全Set,用來存放每個客戶端對應的WebSocketServer對象。 private static ConcurrentHashMap<String, WebSocketClient> webSocketMap = new ConcurrentHashMap<>(); /**與某個客戶端的連接會話,需要通過它來給客戶端發送數據*/ private Session session; /**接收userName*/ private String userName=""; /** * 連接建立成功調用的方法*/ @OnOpen public void onOpen(Session session, @PathParam("userName") String userName) { if(!webSocketMap.containsKey(userName)) { addOnlineCount(); // 在線數 +1 } this.session = session; this.userName= userName; WebSocketClient client = new WebSocketClient(); client.setSession(session); client.setUri(session.getRequestURI().toString()); webSocketMap.put(userName, client); log.info("----------------------------------------------------------------------------"); log.info("用戶連接:"+userName+",當前在線人數為:" + getOnlineCount()); try { sendMessage("來自后台的反饋:連接成功"); } catch (IOException e) { log.error("用戶:"+userName+",網絡異常!!!!!!"); } } /** * 連接關閉調用的方法 */ @OnClose public void onClose() { if(webSocketMap.containsKey(userName)){ webSocketMap.remove(userName); if(webSocketMap.size()>0) { //從set中刪除 subOnlineCount(); } } log.info("----------------------------------------------------------------------------"); log.info(userName+"用戶退出,當前在線人數為:" + getOnlineCount()); } /** * 收到客戶端消息后調用的方法 * * @param message 客戶端發送過來的消息*/ @OnMessage public void onMessage(String message, Session session) { log.info("收到用戶消息:"+userName+",報文:"+message); //可以群發消息 //消息保存到數據庫、redis if(StringUtils.isNotBlank(message)){ } } /** * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("用戶錯誤:"+this.userName+",原因:"+error.getMessage()); error.printStackTrace(); } /** * 連接服務器成功后主動推送 */ public void sendMessage(String message) throws IOException { synchronized (session){ this.session.getBasicRemote().sendText(message); } } /** * 向指定客戶端發送消息 * @param userName * @param message */ public static void sendMessage(String userName,String message){ try { WebSocketClient webSocketClient = webSocketMap.get(userName); if(webSocketClient!=null){ webSocketClient.getSession().getBasicRemote().sendText(message); } } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e.getMessage()); } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketService.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketService.onlineCount--; } public static void setOnlineCount(int onlineCount) { WebSocketService.onlineCount = onlineCount; } public static ConcurrentHashMap<String, WebSocketClient> getWebSocketMap() { return webSocketMap; } public static void setWebSocketMap(ConcurrentHashMap<String, WebSocketClient> webSocketMap) { WebSocketService.webSocketMap = webSocketMap; } public Session getSession() { return session; } public void setSession(Session session) { this.session = session; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } }
注意這里的引用的WebSocketClient是上面自己新建的用來存儲連接相關信息的實體類
然后@ServerEndpoint(value = "/websocket/{userName}")
這里的就是WebSocket連接的地址,后面的{userName}是用來接收前端傳遞的參數,用作不同標識,進而做不同的處理
然后下面的OnOpen注解的方法是連接建立成功后調用的方法,這里做了用戶在線數的計數處理,可以根據自己
的業務需要去處理。
然后OnClose注解的是連接關系調用的方法。
然后OnMessage注解的是客戶端發來消息時的回調方法,里面根據自己需要去處理數據
sendMessage方法在客戶端連接到服務器時使用當前會話來給客戶端推送一個反饋消息
然后向指定客戶端發送消息使用的是sendMessage方法,傳遞的是用戶的唯一標識和消息內容
新建一個Controller接口用來測試消息的推送
import com.ruoyi.web.websocket.WebSocketService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/websocket") public class WebSocketController { @GetMapping("/pushone") public void pushone() { WebSocketService.sendMessage("badao","公眾號:霸道的程序猿"); } }
如果是普通的SpringBoot項目則后台到此就可以了,然后因為是使用上面若依搭建的框架,所以需要將ws的url和接口的url放開權限認證
Vue項目集成
新建一個WebSocket組件,這里放在componets目錄下
然后聲明一些變量
data() { return { // ws是否啟動 wsIsRun: false, // 定義ws對象 webSocket: null, // ws請求鏈接(類似於ws后台地址) ws: '', // ws定時器 wsTimer: null, } },
在mounted函數中執行初始化websocket連接的方法
async mounted() { this.wsIsRun = true this.wsInit() },
在執行初始化的方法實現中
const wsuri = 'ws://你的電腦ip:7777/websocket/badao' this.ws = wsuri if (!this.wsIsRun) return // 銷毀ws this.wsDestroy() // 初始化ws this.webSocket = new WebSocket(this.ws) // ws連接建立時觸發 this.webSocket.addEventListener('open', this.wsOpenHanler) // ws服務端給客戶端推送消息 this.webSocket.addEventListener('message', this.wsMessageHanler) // ws通信發生錯誤時觸發 this.webSocket.addEventListener('error', this.wsErrorHanler) // ws關閉時觸發 this.webSocket.addEventListener('close', this.wsCloseHanler) // 檢查ws連接狀態,readyState值為0表示尚未連接,1表示建立連接,2正在關閉連接,3已經關閉或無法打開 clearInterval(this.wsTimer) this.wsTimer = setInterval(() => { if (this.webSocket.readyState === 1) { clearInterval(this.wsTimer) } else { console.log('ws建立連接失敗') this.wsInit() } }, 3000) },
進行websocket連接並傳遞badao參數,並且設置相應的回調處理方法。
最后設置了一個3秒的定時器去定時檢查websocket的連接狀態。
此組件還添加了一個按鈕並設置其點擊事件用來給服務端推送消息
sendDataToServer() { if (this.webSocket.readyState === 1) { this.webSocket.send('來自前端的數據') } else { throw Error('服務未連接') } },
完整的WebSocket組件代碼
<template> <el-button @click="sendDataToServer" >給后台發送消息</el-button> </template> <script> export default { name: "WebSocket", data() { return { // ws是否啟動 wsIsRun: false, // 定義ws對象 webSocket: null, // ws請求鏈接(類似於ws后台地址) ws: '', // ws定時器 wsTimer: null, } }, async mounted() { this.wsIsRun = true this.wsInit() }, methods: { sendDataToServer() { if (this.webSocket.readyState === 1) { this.webSocket.send('來自前端的數據') } else { throw Error('服務未連接') } }, /** * 初始化ws */ wsInit() { const wsuri = 'ws://10.229.36.158:7777/websocket/badao' this.ws = wsuri if (!this.wsIsRun) return // 銷毀ws this.wsDestroy() // 初始化ws this.webSocket = new WebSocket(this.ws) // ws連接建立時觸發 this.webSocket.addEventListener('open', this.wsOpenHanler) // ws服務端給客戶端推送消息 this.webSocket.addEventListener('message', this.wsMessageHanler) // ws通信發生錯誤時觸發 this.webSocket.addEventListener('error', this.wsErrorHanler) // ws關閉時觸發 this.webSocket.addEventListener('close', this.wsCloseHanler) // 檢查ws連接狀態,readyState值為0表示尚未連接,1表示建立連接,2正在關閉連接,3已經關閉或無法打開 clearInterval(this.wsTimer) this.wsTimer = setInterval(() => { if (this.webSocket.readyState === 1) { clearInterval(this.wsTimer) } else { console.log('ws建立連接失敗') this.wsInit() } }, 3000) }, wsOpenHanler(event) { console.log('ws建立連接成功') }, wsMessageHanler(e) { console.log('wsMessageHanler') console.log(e) //const redata = JSON.parse(e.data) //console.log(redata) }, /** * ws通信發生錯誤 */ wsErrorHanler(event) { console.log(event, '通信發生錯誤') this.wsInit() }, /** * ws關閉 */ wsCloseHanler(event) { console.log(event, 'ws關閉') this.wsInit() }, /** * 銷毀ws */ wsDestroy() { if (this.webSocket !== null) { this.webSocket.removeEventListener('open', this.wsOpenHanler) this.webSocket.removeEventListener('message', this.wsMessageHanler) this.webSocket.removeEventListener('error', this.wsErrorHanler) this.webSocket.removeEventListener('close', this.wsCloseHanler) this.webSocket.close() this.webSocket = null clearInterval(this.wsTimer) } }, } } </script> <style scoped> </style>
然后在首頁中引用該組件
<template> <div class="app-container home"> <el-row :gutter="20"> websocket推送 <WebSocket></WebSocket> </el-row> <el-divider /> </div> </template> import WebSocket from '@/components/WebSocket/WebSocket' export default { name: "index", components: { WebSocket },
效果
首先運行后台SpringBoot服務,然后運行前端項目並登錄到首頁,打開控制台輸出
可以看到websocket建立連接成功,並且在后台也有輸出
然后后台給前端推送消息,調用后台發送消息的接口
然后就可以在前端收到后台的推送
然后點擊前端的給后台發送消息按鈕
則在后台的回調方法中收到發送的消息