詳解springboot集成websocket的兩種實(shí)現(xiàn)方式
WebSocket跟常規(guī)的http協(xié)議的區(qū)別和優(yōu)缺點(diǎn)這里大概描述一下
一、websocket與http
http協(xié)議是用在應(yīng)用層的協(xié)議,他是基于tcp協(xié)議的,http協(xié)議建立鏈接也必須要有三次握手才能發(fā)送信息。http鏈接分為短鏈接,長鏈接,短鏈接是每次請(qǐng)求都要三次握手才能發(fā)送自己的信息。即每一個(gè)request對(duì)應(yīng)一個(gè)response。長鏈接是在一定的期限內(nèi)保持鏈接。保持TCP連接不斷開。客戶端與服務(wù)器通信,必須要有客戶端發(fā)起然后服務(wù)器返回結(jié)果??蛻舳耸侵鲃?dòng)的,服務(wù)器是被動(dòng)的。
WebSocket是HTML5中的協(xié)議, 他是為了解決客戶端發(fā)起多個(gè)http請(qǐng)求到服務(wù)器資源瀏覽器必須要經(jīng)過長時(shí)間的輪訓(xùn)問題而生的,他實(shí)現(xiàn)了多路復(fù)用,他是全雙工通信。在webSocket協(xié)議下客服端和瀏覽器可以同時(shí)發(fā)送信息。
二、HTTP的長連接與websocket的持久連接
HTTP1.1的連接默認(rèn)使用長連接(persistent connection),
即在一定的期限內(nèi)保持鏈接,客戶端會(huì)需要在短時(shí)間內(nèi)向服務(wù)端請(qǐng)求大量的資源,保持TCP連接不斷開??蛻舳伺c服務(wù)器通信,必須要有客戶端發(fā)起然后服務(wù)器返回結(jié)果??蛻舳耸侵鲃?dòng)的,服務(wù)器是被動(dòng)的。
在一個(gè)TCP連接上可以傳輸多個(gè)Request/Response消息對(duì),所以本質(zhì)上還是Request/Response消息對(duì),仍然會(huì)造成資源的浪費(fèi)、實(shí)時(shí)性不強(qiáng)等問題。
如果不是持續(xù)連接,即短連接,那么每個(gè)資源都要建立一個(gè)新的連接,HTTP底層使用的是TCP,那么每次都要使用三次握手建立TCP連接,即每一個(gè)request對(duì)應(yīng)一個(gè)response,將造成極大的資源浪費(fèi)。
長輪詢,即客戶端發(fā)送一個(gè)超時(shí)時(shí)間很長的Request,服務(wù)器hold住這個(gè)連接,在有新數(shù)據(jù)到達(dá)時(shí)返回Response
websocket的持久連接 只需建立一次Request/Response消息對(duì),之后都是TCP連接,避免了需要多次建立Request/Response消息對(duì)而產(chǎn)生的冗余頭部信息。
Websocket只需要一次HTTP握手,所以說整個(gè)通訊過程是建立在一次連接/狀態(tài)中,而且websocket可以實(shí)現(xiàn)服務(wù)端主動(dòng)聯(lián)系客戶端,這是http做不到的。
springboot集成websocket的不同實(shí)現(xiàn)方式:
pom添加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
因涉及到j(luò)s連接服務(wù)端,所以也寫了對(duì)應(yīng)的html,這里集成下thymeleaf模板,前后分離的項(xiàng)目這一塊全都是前端做的
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
配置文件:
server: port: 8885 #添加Thymeleaf配置 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html mode: HTML5 encoding: UTF-8 content-type: text/html
1:自定義WebSocketServer,使用底層的websocket方法,提供對(duì)應(yīng)的onOpen、onClose、onMessage、onError方法
1.1:添加webSocketConfig配置類
/** * 開啟WebSocket支持 * Created by huiyunfei on 2019/5/31. */ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
1.2:添加webSocketServer服務(wù)端類
package com.example.admin.web; /** * Created by huiyunfei on 2019/5/31. */ @ServerEndpoint("/websocket/{sid}") @Component @Slf4j public class WebSocketServer { //靜態(tài)變量,用來記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計(jì)成線程安全的。 private static int onlineCount = 0; //concurrent包的線程安全Set,用來存放每個(gè)客戶端對(duì)應(yīng)的MyWebSocket對(duì)象。 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); //與某個(gè)客戶端的連接會(huì)話,需要通過它來給客戶端發(fā)送數(shù)據(jù) private Session session; //接收sid private String sid=""; */ /** * 連接建立成功調(diào)用的方法*//* @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { this.session = session; webSocketSet.add(this); //加入set中 addOnlineCount(); //在線數(shù)加1 log.info("有新窗口開始監(jiān)聽:"+sid+",當(dāng)前在線人數(shù)為" + getOnlineCount()); this.sid=sid; try { sendMessage("連接成功"); } catch (IOException e) { log.error("websocket IO異常"); } } */ /** * 連接關(guān)閉調(diào)用的方法 *//* @OnClose public void onClose() { webSocketSet.remove(this); //從set中刪除 subOnlineCount(); //在線數(shù)減1 log.info("有一連接關(guān)閉!當(dāng)前在線人數(shù)為" + getOnlineCount()); } */ /** * 收到客戶端消息后調(diào)用的方法 * * @param message 客戶端發(fā)送過來的消息*//* @OnMessage public void onMessage(String message, Session session) { log.info("收到來自窗口"+sid+"的信息:"+message); //群發(fā)消息 for (WebSocketServer item : webSocketSet) { try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } */ /** * * @param session * @param error *//* @OnError public void onError(Session session, Throwable error) { log.error("發(fā)生錯(cuò)誤"); error.printStackTrace(); } */ /** * 實(shí)現(xiàn)服務(wù)器主動(dòng)推送 *//* public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } */ /** * 群發(fā)自定義消息 * *//* public static void sendInfo(String message,@PathParam("sid") String sid) throws IOException { log.info("推送消息到窗口"+sid+",推送內(nèi)容:"+message); for (WebSocketServer item : webSocketSet) { try { //這里可以設(shè)定只推送給這個(gè)sid的,為null則全部推送 if(sid==null) { item.sendMessage(message); }else if(item.sid.equals(sid)){ item.sendMessage(message); } } catch (IOException e) { continue; } } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() { return webSocketSet; } }
1.3:添加對(duì)應(yīng)的controller
@Controller @RequestMapping("/system") public class SystemController { //頁面請(qǐng)求 @GetMapping("/index/{userId}") public ModelAndView socket(@PathVariable String userId) { ModelAndView mav=new ModelAndView("/socket1"); mav.addObject("userId", userId); return mav; } //推送數(shù)據(jù)接口 @ResponseBody @RequestMapping("/socket/push/{cid}") public Map pushToWeb(@PathVariable String cid, String message) { Map result = new HashMap(); try { WebSocketServer.sendInfo(message,cid); result.put("code", 200); result.put("msg", "success"); } catch (IOException e) { e.printStackTrace(); } return result; }
1.4:提供socket1.html頁面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"></meta> <title>Title</title> </head> <body> hello world! </body> <script> var socket; if(typeof(WebSocket) == "undefined") { console.log("您的瀏覽器不支持WebSocket"); }else{ console.log("您的瀏覽器支持WebSocket"); //實(shí)現(xiàn)化WebSocket對(duì)象,指定要連接的服務(wù)器地址與端口 建立連接 //等同于 index = new WebSocket("ws://localhost:8885/websocket/2"); //socket = new WebSocket("${basePath}websocket/${cid}".replace("http","ws")); //打開事件 index.onopen = function() { console.log("Socket 已打開"); //socket.send("這是來自客戶端的消息" + location.href + new Date()); }; //獲得消息事件 index.onmessage = function(msg) { console.log(msg.data); //發(fā)現(xiàn)消息進(jìn)入 開始處理前端觸發(fā)邏輯 }; //關(guān)閉事件 index.onclose = function() { console.log("Socket已關(guān)閉"); }; //發(fā)生了錯(cuò)誤事件 index.onerror = function() { alert("Socket發(fā)生了錯(cuò)誤"); //此時(shí)可以嘗試刷新頁面 } //離開頁面時(shí),關(guān)閉socket //jquery1.8中已經(jīng)被廢棄,3.0中已經(jīng)移除 // $(window).unload(function(){ // socket.close(); //}); } </script> </html>
總結(jié):
瀏覽器debug訪問 localhost:8885/system/index/1跳轉(zhuǎn)到socket1.html,js自動(dòng)連接server并傳遞cid到服務(wù)端,服務(wù)端對(duì)應(yīng)的推送消息到客戶端頁面(cid區(qū)分不同的請(qǐng)求,server里提供的有群發(fā)消息方法)
2.1:基于STOMP協(xié)議的WebSocket
使用STOMP的好處在于,它完全就是一種消息隊(duì)列模式,你可以使用生產(chǎn)者與消費(fèi)者的思想來認(rèn)識(shí)它,發(fā)送消息的是生產(chǎn)者,接收消息的是消費(fèi)者。而消費(fèi)者可以通過訂閱不同的destination,來獲得不同的推送消息,不需要開發(fā)人員去管理這些訂閱與推送目的地之前的關(guān)系,spring官網(wǎng)就有一個(gè)簡單的spring-boot的stomp-demo,如果是基于springboot,大家可以根據(jù)spring上面的教程試著去寫一個(gè)簡單的demo。
提供websocketConfig配置類
/** * @Description: registerStompEndpoints(StompEndpointRegistry registry) configureMessageBroker(MessageBrokerRegistry config) 這個(gè)方法的作用是定義消息代理,通俗一點(diǎn)講就是設(shè)置消息連接請(qǐng)求的各種規(guī)范信息。 registry.enableSimpleBroker("/topic")表示客戶端訂閱地址的前綴信息,也就是客戶端接收服務(wù)端消息的地址的前綴信息(比較繞,看完整個(gè)例子,大概就能明白了) registry.setApplicationDestinationPrefixes("/app")指服務(wù)端接收地址的前綴,意思就是說客戶端給服務(wù)端發(fā)消息的地址的前綴 * @Author:hui.yunfei@qq.com * @Date: 2019/5/31 */ @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { // 這個(gè)方法的作用是添加一個(gè)服務(wù)端點(diǎn),來接收客戶端的連接。 // registry.addEndpoint("/socket")表示添加了一個(gè)/socket端點(diǎn),客戶端就可以通過這個(gè)端點(diǎn)來進(jìn)行連接。 // withSockJS()的作用是開啟SockJS支持, @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/socket").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { //表示客戶端訂閱地址的前綴信息,也就是客戶端接收服務(wù)端消息的地址的前綴信息 registry.enableSimpleBroker("/topic"); //指服務(wù)端接收地址的前綴,意思就是說客戶端給服務(wù)端發(fā)消息的地址的前綴 registry.setApplicationDestinationPrefixes("/app"); } }
2.2:controller提供對(duì)應(yīng)請(qǐng)求的接口
//頁面請(qǐng)求 @GetMapping("/socket2") public ModelAndView socket2() {//@PathVariable String userId ModelAndView mav=new ModelAndView("html/socket2"); //mav.addObject("userId", userId); return mav; } /** * @Description:這個(gè)方法是接收客戶端發(fā)送功公告的WebSocket請(qǐng)求,使用的是@MessageMapping * @Author:hui.yunfei@qq.com * @Date: 2019/5/31 */ @MessageMapping("/change-notice")//客戶端訪問服務(wù)端的時(shí)候config中配置的服務(wù)端接收前綴也要加上 例:/app/change-notice @SendTo("/topic/notice")//config中配置的訂閱前綴記得要加上 public CustomMessage greeting(CustomMessage message){ System.out.println("服務(wù)端接收到消息:"+message.toString()); //我們使用這個(gè)方法進(jìn)行消息的轉(zhuǎn)發(fā)發(fā)送! //this.simpMessagingTemplate.convertAndSend("/topic/notice", value);(可以使用定時(shí)器定時(shí)發(fā)送消息到客戶端) // @Scheduled(fixedDelay = 1000L) // public void time() { // messagingTemplate.convertAndSend("/system/time", new Date().toString()); // } //也可以使用sendTo發(fā)送 return message; }
2.3:提供socket2.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>Spring Boot+WebSocket+廣播式</title> </head> <body onload="disconnect()"> <noscript><h2 style="color: #ff0000">貌似你的瀏覽器不支持websocket</h2></noscript> <div> <div> <button id="connect" onclick="connect();">連接</button> <button id="disconnect" disabled="disabled" onclick="disconnect();">斷開連接</button> </div> <div id="conversationDiv"> <label>輸入你的名字</label><input type="text" id="name" /> <button id="sendName" onclick="sendName();">發(fā)送</button> <p id="response"></p> </div> </div> <script th:src="@{/js/sockjs.min.js}"></script> <script th:src="@{/js/stomp.min.js}"></script> <script th:src="@{/js/jquery-3.2.1.min.js}"></script> <script type="text/javascript"> var stompClient = null; function setConnected(connected) { document.getElementById('connect').disabled = connected; document.getElementById('disconnect').disabled = !connected; document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden'; $('#response').html(); } function connect() { var socket = new SockJS('/socket'); //1 stompClient = Stomp.over(socket);//2 stompClient.connect({}, function(frame) {//3 setConnected(true); console.log('開始進(jìn)行連接Connected: ' + frame); stompClient.subscribe('/topic/notice', function(respnose){ //4 showResponse(JSON.parse(respnose.body).responseMessage); }); }); } function disconnect() { if (stompClient != null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { var name = $('#name').val(); stompClient.send("/app/change-notice", {}, JSON.stringify({ 'name': name }));//5 } function showResponse(message) { var response = $("#response"); response.html(message); } </script> </body> </html>
2.4:對(duì)應(yīng)的js引用可以去網(wǎng)上下載
2.5:瀏覽器debug訪問localhost:8885/system/socket2,點(diǎn)擊連接連接到服務(wù)器,數(shù)據(jù)內(nèi)容可以推送到服務(wù)器以及服務(wù)器消息回推。
2.6:實(shí)現(xiàn)前端和服務(wù)端的輪訓(xùn)可以頁面Ajax輪訓(xùn)也可以后端添加定時(shí)器
@Component @EnableScheduling public class TimeTask { private static Logger logger = LoggerFactory.getLogger(TimeTask.class); @Scheduled(cron = "0/20 * * * * ?") public void test(){ System.err.println("********* 定時(shí)任務(wù)執(zhí)行 **************"); CopyOnWriteArraySet<WebSocketServer> webSocketSet = WebSocketServer.getWebSocketSet(); int i = 0 ; webSocketSet.forEach(c->{ try { c.sendMessage(" 定時(shí)發(fā)送 " + new Date().toLocaleString()); } catch (IOException e) { e.printStackTrace(); } }); System.err.println("/n 定時(shí)任務(wù)完成......."); } }
代碼在https://github.com/huiyunfei/spring-cloud.git 的admin項(xiàng)目里
基于STOMP協(xié)議的廣播模式和點(diǎn)對(duì)點(diǎn)模式消息推送可參考:
https://www.cnblogs.com/hhhshct/p/8849449.html
https://www.cnblogs.com/jmcui/p/8999998.html
到此這篇關(guān)于springboot集成websocket的兩種實(shí)現(xiàn)方式的文章就介紹到這了,更多相關(guān)springboot集成websocket內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot集成WebSocket的兩種方式(JDK內(nèi)置版和Spring封裝版)
- springboot業(yè)務(wù)功能實(shí)戰(zhàn)之告別輪詢websocket的集成使用
- springboot集成websocket的四種方式小結(jié)
- SpringBoot2.0集成WebSocket實(shí)現(xiàn)后臺(tái)向前端推送信息
- SpringBoot集成WebSocket實(shí)現(xiàn)后臺(tái)向前端推送信息的示例
- SpringBoot集成WebSocket長連接實(shí)際應(yīng)用詳解
- SpringBoot集成WebSocket遇到的問題及解決
相關(guān)文章
windows 部署JAVA環(huán)境安裝iDea的詳細(xì)步驟
這篇文章主要介紹了windows 部署JAVA環(huán)境安裝iDea的詳細(xì)步驟,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08詳解Spring MVC攔截器實(shí)現(xiàn)session控制
這篇文章主要介紹了詳解Spring MVC攔截器實(shí)現(xiàn)session控制,使用session監(jiān)聽,重復(fù)登錄后,強(qiáng)制之前登錄的session過期。有興趣的可以了解一下。2017-01-01Spring代理對(duì)象導(dǎo)致的獲取不到原生對(duì)象注解的解決
本文主要介紹了Spring代理對(duì)象導(dǎo)致的獲取不到原生對(duì)象注解的解決,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04Spring Security基于HttpRequest配置權(quán)限示例詳解
這篇文章主要介紹了Spring Security基于HttpRequest配置權(quán)限示例詳解,我們?cè)谂渲弥信渲玫膗rl被封裝成RequestMatcher,而hasRole被封裝成AuthorityAuthorizationManager,本文結(jié)合示例代碼講解的非常詳細(xì),需要的朋友可以參考下2024-03-03Spring Boot+Mybatis+Druid+PageHelper實(shí)現(xiàn)多數(shù)據(jù)源并分頁的方法
這篇文章主要給大家介紹了關(guān)于Spring Boot+Mybatis+Druid+PageHelper實(shí)現(xiàn)多數(shù)據(jù)源并分頁的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們來一起看看吧2018-05-05Spring Boot3整合Mybatis Plus的詳細(xì)過程(數(shù)據(jù)庫為MySQL)
這篇文章主要介紹了Spring Boot3整合Mybatis Plus的詳細(xì)過程(數(shù)據(jù)庫為MySQL),本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-07-07IDEA-Maven項(xiàng)目的jdk版本設(shè)置方法
我們需要設(shè)置jdk的版本,不然會(huì)提示導(dǎo)致語法錯(cuò)誤,這篇文章主要介紹了IDEA-Maven項(xiàng)目的jdk版本設(shè)置方法,小編覺得不錯(cuò),一起來了解一下2019-04-04