背景
- HTTP 協議有一個缺陷:通信只能由客戶端發起,HTTP 協議做不到服務器主動向客戶端推送信息
- WebSocket協議是基於TCP的一種新的網絡協議。它實現了瀏覽器與服務器全雙工(full-duplex)通信——允許服務器主動發送信息給客戶端
- 舉例來說,我們想要查詢當前的排隊情況,只能是頁面輪詢向服務器發出請求,服務器返回查詢結果。輪詢的效率低,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)。因此WebSocket 就是這樣發明的。
SpringBoot的WebSocket
引入MAVEN依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
WebSocketConfig
啟用websocket支持很簡單,直接一個配置類搞定。
@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
WebSocketServer
websocket和socket類似,有客戶端和服務端,客戶端就是pc、app等,服務端就是我們后端了。因為WebSocket是類似客戶端服務端的形式(采用ws協議),那么這里的WebSocketServer其實就相當於一個ws協議的Controller,直接使用注解
@ServerEndpoint(value = “/websocket/{appNo}”)和@Component啟用即可,然后在里面實現@OnOpen,@OnClose, @OnMessage,@OnError等方法即可。
package com.dongzhengafc.facesign.websocket; 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; /** * @Author: TheBigBlue * @Description: 向app端實時推送業務狀態信息 * @Date: 2019/7/16 **/ //由於是websocket 所以原本是@RestController的http形式 //直接替換成@ServerEndpoint即可,作用是一樣的 就是指定一個地址 //表示定義一個websocket的Server端 @Component @ServerEndpoint(value = "/websocket/{appNo}") public class WebSocketController { private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketController.class); /** * @Author: TheBigBlue * @Description: 加入連接 * @Date: 2019/7/16 * @Param appNo: 申請單號 * @Param relTyp: 關系人類型 * @Param session: * @Return: **/ @OnOpen public void onOpen(@PathParam("appNo") String appNo, Session session) { LOGGER.info("[" + appNo + "]加入連接!"); WebSocketUtil.addSession(appNo, session); } /** * @Author: TheBigBlue * @Description: 斷開連接 * @Date: 2019/7/16 * @Param appNo: * @Param relTyp: * @Param session: * @Return: **/ @OnClose public void onClose(@PathParam("appNo") String appNo, Session session) { LOGGER.info("[" + appNo + "]斷開連接!"); WebSocketUtil.remoteSession(appNo); } /** * @Author: TheBigBlue * @Description: 發送消息 * @Date: 2019/7/16 * @Param appNo: 申請單號 * @Param relTyp: 關系人類型 * @Param message: 消息 * @Return: **/ @OnMessage public void OnMessage(@PathParam("appNo") String appNo, String message) { String messageInfo = "服務器對[" + appNo + "]發送消息:" + message; LOGGER.info(messageInfo); Session session = WebSocketUtil.ONLINE_SESSION.get(appNo); if("heart".equalsIgnoreCase(message)){ LOGGER.info("客戶端向服務端發送心跳"); //向客戶端發送心跳連接成功 message = "success"; } //發送普通信息 WebSocketUtil.sendMessage(session, message); } @OnError public void onError(Session session, Throwable throwable) { LOGGER.error(session.getId() + "異常:", throwable); try { session.close(); } catch (IOException e) { e.printStackTrace(); } throwable.printStackTrace(); } }
package com.dongzhengafc.facesign.websocket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.websocket.RemoteEndpoint.Async; import javax.websocket.Session; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; /** * @Author: TheBigBlue * @Description: * @Date: 2019/7/16 **/ public class WebSocketUtil { private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUtil.class); /** * @Author: TheBigBlue * @Description: 使用map進行存儲在線的session * @Date: 2019/7/16 **/ public static final Map<String, Session> ONLINE_SESSION = new ConcurrentHashMap<>(); /** * @Author: TheBigBlue * @Description: 添加Session * @Date: 2019/7/16 * @Param userKey: * @Param session: * @Return: **/ public static void addSession(String userKey, Session session) { ONLINE_SESSION.put(userKey, session); } public static void remoteSession(String userKey) { ONLINE_SESSION.remove(userKey); } /** * @Author: TheBigBlue * @Description: 向某個用戶發送消息 * @Date: 2019/7/16 * @Param session: * @Param message: * @Return: **/ public static Boolean sendMessage(Session session, String message) { if (session == null) { return false; } // getAsyncRemote()和getBasicRemote()異步與同步 Async async = session.getAsyncRemote(); //發送消息 Future<Void> future = async.sendText(message); boolean done = future.isDone(); LOGGER.info("服務器發送消息給客戶端" + session.getId() + "的消息:" + message + ",狀態為:" + done); return done; } }
推送消息
推送消息,可以自己寫接口調用,或者前端發起,或者通過第三方工具連接。
- 自己寫接口調用
package com.dongzhengafc.facesign.websocket; import com.dongzhengafc.facesign.base.api.JsonResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author: TheBigBlue * @Description: 向客戶端推送業務狀態信息 * @Date: 2019/7/16 **/ @RestController @RequestMapping("/socket") public class WebSocketPushController { @Autowired private WebSocketController webSocketController; /** * @Author: TheBigBlue * @Description: * @Date: 2019/7/16 * @Param appNo: 發送的用戶名 * @Param relTyp: 發送的用戶名 * @Param message: 發送的信息 * @Return: **/ @RequestMapping("/push") public JsonResponse pushToWeb(String appNo, String message) { webSocketController.OnMessage(appNo, message); return JsonResponse.success(); } }
- 前端請求連接,發送信息。
<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("${basePath}websocket/${cid}".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>
- 第三方工具連接:http://www.websocket-test.com/
相關問題
1. 打war包部署tomcat報錯
Application startup failed
java.lang.IllegalStateException: Failed to register @ServerEndpoint class:
- 原因:SpringBoot Run As 可以快速啟動項目,且能夠即時刷新。其原因是SpringBoot擁有一個內置的Tomcat,此Tomcat的版本可在pom.xml中指定。每次我們使用SpringBoot Run As啟動項目時,我們的web容器即就是這個內置的Tomcat。此刻web容器連同項目本身都是由Spring進行代理。而當我們將項目打成war包,部署在服務器上的某個Tomcat下時。此刻我們的項目將會交由這個Tomcat去管理。因為外部Tomcat的優先級高於Spring內置Tomcat。問題就在這里。當我們在IDE內使用 SpringBoot Run As去啟動時,Spring會幫我們找到內置Tomcat lib中的javax.websocket包加載使用。所以項目正常運行。而當我們將打好的war包放在外部Tomcat上進行啟動時。Tomcat管理器根據之前的Javax.websocket包的路徑找不到對應的ServerEndpoint類資源文件,因此自然會注冊失敗。
- 解決:pom.xml 引入依賴
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency>
- 部署發現問題仍然存在,這是因為當我們使用外部Tomcat時,項目的管理權將會由Spring交接至Tomcat。 而Tomcat7及后續版本是對websocket直接支持的,且我們所使用的jar包也是tomcat提供的。 但是我們在WebSocketConfig中將ServerEndpointExporter指定給Spring管理。而部署后ServerEndpoint是需要Tomcat直接管理才能生效的。所以此時即就是此包的管理權交接失敗,那肯定不能成功了。最后我們需要將WebSocketConfig中的bean配置注釋掉。然后再打包上傳部署測試。一切正常!
//@Configuration //public class WebSocketConfig { // // @Bean // public ServerEndpointExporter serverEndpointExporter() { // return new ServerEndpointExporter(); // } // //}
2. Websocket在1分鍾后自動斷開連接報錯EOFException
- 這是因為websocket長連接有默認的超時時間(1分鍾,由proxy_read_timeout決定),就是超過一定的時間沒有發送任何消息,連接會自動斷開。解決辦法就是讓瀏覽器每隔一定時間(要小於超時時間)發送一個心跳。
- 或者部署到服務器后,nginx 代理默認配置了訪問超時時間為90s,我們可以修改這個值。nginx 通過在客戶端和后端服務器之間建立起一條隧道來支持WebSocket。為了使nginx可以將來自客戶端的Upgrade請求發送給后端服務器,Upgrade和Connection的頭信息必須被顯式的設置,一旦我們完成以上設置,nginx就可以處理WebSocket連接了。注意,必須要有proxy_set_header Host h o s t : host:host:server_port; 這個配置,否則會報403錯誤。
location /web/count { proxy_pass http://tomcat-server; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host:$server_port; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }