WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的協(xié)議。WebSocket使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡(jiǎn)單,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)。在WebSocket API中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。
v原理
很多網(wǎng)站為了實(shí)現(xiàn)推送技術(shù),所用的技術(shù)都是輪詢。輪詢是在特定的時(shí)間間隔(如每1秒),由瀏覽器對(duì)服務(wù)器發(fā)出HTTP請(qǐng)求,然后由服務(wù)器返回最新的數(shù)據(jù)給客戶端的瀏覽器。這種傳統(tǒng)的模式帶來(lái)很明顯的缺點(diǎn),即瀏覽器需要不斷的向服務(wù)器發(fā)出請(qǐng)求,然而HTTP請(qǐng)求可能包含較長(zhǎng)的頭部,其中真正有效的數(shù)據(jù)可能只是很小的一部分,顯然這樣會(huì)浪費(fèi)很多的帶寬等資源。
而比較新的技術(shù)去做輪詢的效果是Comet。這種技術(shù)雖然可以雙向通信,但依然需要反復(fù)發(fā)出請(qǐng)求。而且在Comet中,普遍采用的長(zhǎng)鏈接,也會(huì)消耗服務(wù)器資源。
在這種情況下,HTML5定義了WebSocket協(xié)議,能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實(shí)時(shí)地進(jìn)行通訊。
v架構(gòu)搭建
添加maven引用
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
配置應(yīng)用屬性
server.port=8300
spring.thymeleaf.mode=HTML
spring.thymeleaf.cache=true
spring.thymeleaf.prefix=classpath:/web/
spring.thymeleaf.encoding: UTF-8
spring.thymeleaf.suffix: .html
spring.thymeleaf.check-template-location: true
spring.thymeleaf.template-resolver-order: 1
添加WebSocketConfig
package com.test.config; 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.server.standard.ServerEndpointExporter; /** * @Author chen bo * @Date 2023/10 * @Des */ @Configuration public class WebSocketConfig { /** * bean注冊(cè):會(huì)自動(dòng)掃描帶有@ServerEndpoint注解聲明的Websocket Endpoint(端點(diǎn)),注冊(cè)成為Websocket bean。 * 要注意,如果項(xiàng)目使用外置的servlet容器,而不是直接使用springboot內(nèi)置容器的話,就不要注入ServerEndpointExporter,因?yàn)樗鼘⒂扇萜髯约禾峁┖凸芾怼? */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
添加WebSocket核心類
因?yàn)閃ebSocket是類似客戶端服務(wù)端的形式(采用ws協(xié)議),那么這里的WebSocketServer其實(shí)就相當(dāng)于一個(gè)ws協(xié)議的Controller
直接@ServerEndpoint("/imserver/{userId}")
、@Component
啟用即可,然后在里面實(shí)現(xiàn)@OnOpen
開啟連接,@onClose
關(guān)閉連接,@onMessage
接收消息等方法。
新建一個(gè)ConcurrentHashMap
用于接收當(dāng)前userId的WebSocket或者Session信息,方便IM之間對(duì)userId進(jìn)行推送消息。單機(jī)版實(shí)現(xiàn)到這里就可以。集群版(多個(gè)ws節(jié)點(diǎn))還需要借助 MySQL或者 Redis等進(jìn)行訂閱廣播方式處理,改造對(duì)應(yīng)的 sendMessage方法即可。
package com.test.util; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import com.google.gson.JsonObject; /** * WebSocket的操作類 * html頁(yè)面與之關(guān)聯(lián)的接口 * var reqUrl = "http://localhost:8300/websocket/" + cid; * socket = new WebSocket(reqUrl.replace("http", "ws")); */ @Component @Slf4j @ServerEndpoint("/websocket/{sid}") public class WebSocketServer { /** * 靜態(tài)變量,用來(lái)記錄當(dāng)前在線連接數(shù),線程安全的類。 */ private static AtomicInteger onlineSessionClientCount = new AtomicInteger(0); /** * 存放所有在線的客戶端 */ private static Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>(); /** * 連接sid和連接會(huì)話 */ private String sid; private Session session; /** * 連接建立成功調(diào)用的方法。由前端<code>new WebSocket</code>觸發(fā) * * @param sid 每次頁(yè)面建立連接時(shí)傳入到服務(wù)端的id,比如用戶id等??梢宰远x。 * @param session 與某個(gè)客戶端的連接會(huì)話,需要通過(guò)它來(lái)給客戶端發(fā)送消息 */ @OnOpen public void onOpen(@PathParam("sid") String sid, Session session) { /** * session.getId():當(dāng)前session會(huì)話會(huì)自動(dòng)生成一個(gè)id,從0開始累加的。 */ log.info("連接建立中 ==> session_id = {}, sid = {}", session.getId(), sid); //加入 Map中。將頁(yè)面的sid和session綁定或者session.getId()與session //onlineSessionIdClientMap.put(session.getId(), session); onlineSessionClientMap.put(sid, session); //在線數(shù)加1 onlineSessionClientCount.incrementAndGet(); this.sid = sid; this.session = session; sendToOne(sid, "上線了"); log.info("連接建立成功,當(dāng)前在線數(shù)為:{} ==> 開始監(jiān)聽新連接:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid); } /** * 連接關(guān)閉調(diào)用的方法。由前端<code>socket.close()</code>觸發(fā) * * @param sid * @param session */ @OnClose public void onClose(@PathParam("sid") String sid, Session session) { //onlineSessionIdClientMap.remove(session.getId()); // 從 Map中移除 onlineSessionClientMap.remove(sid); //在線數(shù)減1 onlineSessionClientCount.decrementAndGet(); log.info("連接關(guān)閉成功,當(dāng)前在線數(shù)為:{} ==> 關(guān)閉該連接信息:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid); } /** * 收到客戶端消息后調(diào)用的方法。由前端<code>socket.send</code>觸發(fā) * * 當(dāng)服務(wù)端執(zhí)行toSession.getAsyncRemote().sendText(xxx)后,前端的socket.onmessage得到監(jiān)聽。 * * @param message * @param session */ @OnMessage public void onMessage(String message, Session session) { /** * html界面?zhèn)鬟f來(lái)得數(shù)據(jù)格式,可以自定義. * {"sid":"user","message":"hello websocket"} */ JsonObject jsonObject = JsonParser.parseString(message).getAsJsonObject(); String toSid = jsonObject.get("sid").getAsString(); String msg = jsonObject.get("message").getAsString(); log.info("服務(wù)端收到客戶端消息 ==> fromSid = {}, toSid = {}, message = {}", sid, toSid, message); /** * 模擬約定:如果未指定sid信息,則群發(fā),否則就單獨(dú)發(fā)送 */ if (toSid == null || toSid == "" || "".equalsIgnoreCase(toSid)) { sendToAll(msg); } else { sendToOne(toSid, msg); } } /** * 發(fā)生錯(cuò)誤調(diào)用的方法 * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("WebSocket發(fā)生錯(cuò)誤,錯(cuò)誤信息為:" + error.getMessage()); error.printStackTrace(); } /** * 群發(fā)消息 * * @param message 消息 */ private void sendToAll(String message) { // 遍歷在線map集合 onlineSessionClientMap.forEach((onlineSid, toSession) -> { // 排除掉自己 if (!sid.equalsIgnoreCase(onlineSid)) { log.info("服務(wù)端給客戶端群發(fā)消息 ==> sid = {}, toSid = {}, message = {}", sid, onlineSid, message); toSession.getAsyncRemote().sendText(message); } }); } /** * 指定發(fā)送消息 * * @param toSid * @param message */ private void sendToOne(String toSid, String message) { // 通過(guò)sid查詢map中是否存在 Session toSession = onlineSessionClientMap.get(toSid); if (toSession == null) { log.error("服務(wù)端給客戶端發(fā)送消息 ==> toSid = {} 不存在, message = {}", toSid, message); return; } // 異步發(fā)送 log.info("服務(wù)端給客戶端發(fā)送消息 ==> toSid = {}, message = {}", toSid, message); toSession.getAsyncRemote().sendText(message); /* // 同步發(fā)送 try { toSession.getBasicRemote().sendText(message); } catch (IOException e) { log.error("發(fā)送消息失敗,WebSocket IO異常"); e.printStackTrace(); }*/ } }
添加controller
package com.test.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; /** * @Author chen bo * @Date 2023/10 * @Des */ @Controller public class HomeController { /** * 跳轉(zhuǎn)到websocketDemo.html頁(yè)面,攜帶自定義的cid信息。 * http://localhost:8300/demo/toWebSocketDemo/user * * @param cid * @param model * @return */ @GetMapping("/demo/toWebSocketDemo/{cid}") public String toWebSocketDemo(@PathVariable String cid, Model model) { model.addAttribute("cid", cid); return "index"; } @GetMapping("hello") @ResponseBody public String hi(HttpServletResponse response) { return "Hi"; } }
添加html
注意:html文件添加在application.properties配置的對(duì)應(yīng)目錄中。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>聊天窗口</title> </head> <body> <div> 我的用戶名: <input type="text" th:value="${cid}" readonly="readonly" id="cid"/> </div> <div id="chat-windows" style="width: 400px; height: 300px;overflow: scroll;border: blue 1px solid;"></div> <div>收消息人用戶名:<input id="toUserId" name="toUserId" type="text"></div> <div>輸入你要說(shuō)的話:<input id="contentText" name="contentText" type="text"></div> <div> <button type="button" onclick="sendMessage()">發(fā)送消息</button> </div> </body> <script type="text/javascript"> var socket; if (typeof (WebSocket) == "undefined") { alert("您的瀏覽器不支持WebSocket"); } else { console.log("您的瀏覽器支持WebSocket"); //實(shí)現(xiàn)化WebSocket對(duì)象,指定要連接的服務(wù)器地址與端口 建立連接 var cid = document.getElementById("cid").value; console.log("cid-->" + cid); var reqUrl = "http://localhost:8300/websocket/" + cid; socket = new WebSocket(reqUrl.replace("http", "ws")); //打開事件 socket.onopen = function () { console.log("Socket 已打開"); //socket.send("這是來(lái)自客戶端的消息" + location.href + new Date()); }; //獲得消息事件 socket.onmessage = function (msg) { console.log("onmessage--" + msg.data); //發(fā)現(xiàn)消息進(jìn)入 開始處理前端觸發(fā)邏輯 var chatWindows = document.getElementById("chat-windows"); var pElement = document.createElement('p') pElement.innerText = msg.data; chatWindows.appendChild(pElement); }; //關(guān)閉事件 socket.onclose = function () { console.log("Socket已關(guān)閉"); }; //發(fā)生了錯(cuò)誤事件 socket.onerror = function () { alert("Socket發(fā)生了錯(cuò)誤"); //此時(shí)可以嘗試刷新頁(yè)面 } //離開頁(yè)面時(shí),關(guān)閉socket //jquery1.8中已經(jīng)被廢棄,3.0中已經(jīng)移除 // $(window).unload(function(){ // socket.close(); //}); } function sendMessage() { if (typeof (WebSocket) == "undefined") { alert("您的瀏覽器不支持WebSocket"); } else { var toUserId = document.getElementById('toUserId').value; var contentText = document.getElementById('cid').value + ":" + document.getElementById('contentText').value; var msg = '{"sid":"' + toUserId + '","message":"' + contentText + '"}'; console.log(msg); var chatWindows = document.getElementById("chat-windows"); var chatWindows = document.getElementById("chat-windows"); var pElement = document.createElement('p'); pElement.innerText = "我:" + document.getElementById('contentText').value; chatWindows.appendChild(pElement); socket.send(msg); } } </script> </html>
1對(duì)1模擬演練
啟動(dòng)項(xiàng)目后,在瀏覽器訪問(wèn)http://localhost:8300/demo/toWebSocketDemo/{cid} 跳轉(zhuǎn)到對(duì)應(yīng)頁(yè)面,其中cid是用戶名。
為了便于1對(duì)1測(cè)試,這里我們啟動(dòng)兩個(gè)瀏覽器窗口。
http://localhost:8300/demo/toWebSocketDemo/陽(yáng)光男孩
http://localhost:8300/demo/toWebSocketDemo/水晶女孩
按照要求輸入對(duì)方用戶信息之后,便可以輸入你要說(shuō)的話,暢快聊起來(lái)了。
效果圖如下:
當(dāng)然,如果收消息人用戶名是自己的話,也可以自己給自己發(fā)送數(shù)據(jù)的。
群發(fā)模擬演練
為了便于群發(fā)測(cè)試,這里我們啟動(dòng)3個(gè)瀏覽器窗口。
http://localhost:8300/demo/toWebSocketDemo/陽(yáng)光男孩
http://localhost:8300/demo/toWebSocketDemo/水晶女孩
http://localhost:8300/demo/toWebSocketDemo/路人A
由于sendToAll方法中定義群發(fā)的條件為:當(dāng)不指定 toUserid時(shí),則為群發(fā)。
效果圖如下:
項(xiàng)目架構(gòu)圖如下:
v源碼地址
https://github.com/toutouge/javademosecond
其他參考/學(xué)習(xí)資料:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-711960.html
- https://www.cnblogs.com/xswz/p/10314351.html
- https://www.cnblogs.com/xuwenjin/p/12664650.html
- https://blog.csdn.net/qq_42402854/article/details/130948270
- https://www.cnblogs.com/zhangxinhua/p/11341292.html
作 者:請(qǐng)叫我頭頭哥
出 處:http://www.cnblogs.com/toutou/
關(guān)于作者:專注于基礎(chǔ)平臺(tái)的項(xiàng)目開發(fā)。如有問(wèn)題或建議,請(qǐng)多多賜教!
版權(quán)聲明:本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文鏈接。
特此聲明:所有評(píng)論和私信都會(huì)在第一時(shí)間回復(fù)。也歡迎園子的大大們指正錯(cuò)誤,共同進(jìn)步?;蛘咧苯铀叫盼?
聲援博主:如果您覺得文章對(duì)您有幫助,可以點(diǎn)擊文章右下角【推薦】一下。您的鼓勵(lì)是作者堅(jiān)持原創(chuàng)和持續(xù)寫作的最大動(dòng)力!
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-711960.html
到了這里,關(guān)于SpringBoot進(jìn)階教程(七十七)WebSocket的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!