前言
傳統的前后端數據交互,都是前端發送請求,后端返回數據,主動權在前端。但是如果想向客戶端推送數據,在原來的協議上來說,是不可能的。只能前端不斷使用Ajax去請求后端,拉去數據。這種做法會很耗費客戶端與服務器的資源。還有就是WebSocket技術,WebSocket協議是基於TCP的一種新的網絡協議。它實現了瀏覽器與服務器全雙工(full-duplex)通信——允許服務器主動發送信息給客戶端。
要使用websocket關鍵是@ServerEndpoint這個注解,該注解是javaee標准中的注解,Tomcat7及以上已經實現,如果使用傳統方法將war包部署到tomcat中,只需要引入javaee標准依賴即可:
<dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency>
如果使用SpringBoot,則直接引入SpringBoot的依賴即可:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
實現過程
項目結構
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.websocket</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 設置為熱部署 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.properties
server.port=8080 server.servlet.context-path=/demo server.servlet.session.timeout=1800 spring.thymeleaf.cache=false spring.thymeleaf.encoding=utf-8 spring.thymeleaf.mode=HTML5 spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html
WebSocketConfig.java
package com.websocket.demo.config; 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(); } }
WebSocketServer.java
package com.websocket.demo.config; 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.CopyOnWriteArraySet; @ServerEndpoint("/websocket/{sid}") @Component public class WebSocketServer { private static Logger logger = LoggerFactory.getLogger(WebSocketServer.class); //靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。 private static int onlineCount = 0; //concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); //與某個客戶端的連接會話,需要通過它來給客戶端發送數據 private Session session; //接收sid private String sid=""; /** * 連接建立成功調用的方法*/ @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { this.session = session; webSocketSet.add(this); //加入set中 addOnlineCount(); //在線數加1 logger.info("有新窗口開始監聽:"+sid+",當前在線人數為" + getOnlineCount()); this.sid=sid; try { sendMessage("連接成功"); } catch (IOException e) { logger.error("websocket IO異常"); } } /** * 連接關閉調用的方法 */ @OnClose public void onClose() { webSocketSet.remove(this); //從set中刪除 subOnlineCount(); //在線數減1 logger.info("有一連接關閉!當前在線人數為" + getOnlineCount()); } /** * 收到客戶端消息后調用的方法 * * @param message 客戶端發送過來的消息*/ @OnMessage public void onMessage(String message, Session session) { logger.info("收到來自窗口"+sid+"的信息:"+message); //群發消息 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) { logger.error("發生錯誤"); error.printStackTrace(); } /** * 實現服務器主動推送 */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 群發自定義消息 * */ public static void sendInfo(String message,@PathParam("sid") String sid) throws IOException { logger.info("推送消息到窗口" + sid + ",推送內容:" + message); for (WebSocketServer item : webSocketSet) { try { //這里可以設定只推送給這個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--; } }
SendMessageController.java
package com.websocket.demo.controller; import com.websocket.demo.config.WebSocketServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; import java.io.IOException; @Controller @RequestMapping("/sendMessage") public class SendMessageController { private Logger logger = LoggerFactory.getLogger(SendMessageController.class); //頁面請求 @GetMapping("/socket/{cid}") public ModelAndView socket(@PathVariable String cid) { ModelAndView mav=new ModelAndView("socket"); mav.addObject("cid", cid); return mav; } //推送數據接口 @ResponseBody @RequestMapping("/socket/push/{cid}") public String pushToWeb(@PathVariable String cid, String message) { try { WebSocketServer.sendInfo(message,cid); } catch (IOException e) { e.printStackTrace(); return "ERROR " + cid + " : send message is error"; } return "SUCCESS " + cid + " : send message is success"; } }
socket.html
<script> var socket; if(typeof(WebSocket) == "undefined") { console.log("您的瀏覽器不支持WebSocket"); } else { console.log("您的瀏覽器支持WebSocket"); //實現化WebSocket對象,指定要連接的服務器地址與端口 建立連接 //等同於socket = new WebSocket("ws://localhost:8083/checkcentersys/websocket/20"); socket = new WebSocket("http://localhost:8080/demo/websocket/20".replace("http","ws")); //打開事件 socket.onopen = function() { console.log("Socket 已打開"); socket.send("這是來自客戶端的消息" + location.href + new Date()); }; //獲得消息事件 socket.onmessage = function(msg) { console.log(msg.data); //發現消息進入 開始處理前端觸發邏輯 }; //關閉事件 socket.onclose = function() { console.log("Socket已關閉"); }; //發生了錯誤事件 socket.onerror = function() { alert("Socket發生了錯誤"); //此時可以嘗試刷新頁面 } //離開頁面時,關閉socket //jquery1.8中已經被廢棄,3.0中已經移除 // $(window).unload(function(){ // socket.close(); //}); } </script>
上述是代碼的全部
調用過程
前端向后端發送數據
通過js調用socket.send(String);即可向后台發送需要的數據,如果涉及到js對象,可以轉變為json字符串發送
后端向前端發送數據
可以通過調用寫好的controller接口向前端發送數據
也可以直接調用WebSocketServer.sendInfo(message,cid);靜態方法發送數據
WebSocketServer.sendInfo(message,cid);
理解WebSocket心跳及重連機制
在使用websocket的過程中,有時候會遇到網絡斷開的情況,但是在網絡斷開的時候服務器端並沒有觸發onclose的事件。這樣會有:服務器會繼續向客戶端發送多余的鏈接,並且這些數據還會丟失。所以就需要一種機制來檢測客戶端和服務端是否處於正常的鏈接狀態。因此就有了websocket的心跳了。還有心跳,說明還活着,沒有心跳說明已經掛掉了。
- 為什么叫心跳包呢?
- 它就像心跳一樣每隔固定的時間發一次,來告訴服務器,我還活着。
- 心跳機制是?
- 心跳機制是每隔一段時間會向服務器發送一個數據包,告訴服務器自己還活着,同時客戶端會確認服務器端是否還活着,如果還活着的話,就會回傳一個數據包給客戶端來確定服務器端也還活着,否則的話,有可能是網絡斷開連接了。需要重連~
Reference:
http://www.webkf.net/article/32/95425.html
http://www.manongjc.com/detail/8-rsdxxtmudepkqfx.html
http://www.demodashi.com/demo/16060.html