一、什么是webSocket
WebSocket是HTML5新增的協議,它的目的是在瀏覽器和服務器之間建立一個不受限的雙向通信的通道,比如說,服務器可以在任意時刻發送消息給瀏覽器。
傳統的Http協議連接是有瀏覽器發起的單通信通道,而服務器沒有辦法向客戶端發送消息。這樣一來,要在瀏覽器中搞一個實時聊天,在線炒股(不鼓勵),或者在線多人游戲的話就沒法實現了,只能借助Flash這些插件。
也有人說,HTTP協議其實也能實現啊,比如用輪詢或者Comet。輪詢是指瀏覽器通過JavaScript啟動一個定時器,然后以固定的間隔給服務器發請求,詢問服務器有沒有新消息。這個機制的缺點一是實時性不夠,二是頻繁的請求會給服務器帶來極大的壓力。
Comet本質上也是輪詢,但是在沒有消息的情況下,服務器先拖一段時間,等到有消息了再回復。這個機制暫時地解決了實時性問題,但是它帶來了新的問題:以多線程模式運行的服務器會讓大部分線程大部分時間都處於掛起狀態,極大地浪費服務器資源。另外,一個HTTP連接在長時間沒有數據傳輸的情況下,鏈路上的任何一個網關都可能關閉這個連接,而網關是我們不可控的,這就要求Comet連接必須定期發一些ping數據表示連接“正常工作”。
以上兩種機制都治標不治本,所以,HTML5推出了WebSocket標准,讓瀏覽器和服務器之間可以建立無限制的全雙工通信,任何一方都可以主動發消息給對方。
WebSocket協議
WebSocket協議並不是全新的協議,而是利用了HTTP協議來建立連接。我們來看看WebSocket連接是如何創建的。
首先,WebSocket連接必須由瀏覽器發起,因為請求協議是一個標准的HTTP請求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1 Host: localhost Upgrade: websocket Connection: Upgrade Origin: http://localhost:3000 Sec-WebSocket-Key: client-random-string Sec-WebSocket-Version: 13
該請求和普通的HTTP請求有幾點不同:
- GET請求的地址不是類似
/path/
,而是以ws://
開頭的地址; - 請求頭
Upgrade: websocket
和Connection: Upgrade
表示這個連接將要被轉換為WebSocket連接; Sec-WebSocket-Key
是用於標識這個連接,並非用於加密數據;Sec-WebSocket-Version
指定了WebSocket的協議版本。
隨后,服務器如果接受該請求,就會返回如下響應:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
該響應代碼101
表示本次連接的HTTP協議即將被更改,更改后的協議就是Upgrade: websocket
指定的WebSocket協議。
版本號和子協議規定了雙方能理解的數據格式,以及是否支持壓縮等等。如果僅使用WebSocket的API,就不需要關心這些。
現在,一個WebSocket連接就建立成功,瀏覽器和服務器就可以隨時主動發送消息給對方。消息有兩種,一種是文本,一種是二進制數據。通常,我們可以發送JSON格式的文本,這樣,在瀏覽器處理起來就十分容易。
為什么WebSocket連接可以實現全雙工通信而HTTP連接不行呢?實際上HTTP協議是建立在TCP協議之上的,TCP協議本身就實現了全雙工通信,但是HTTP協議的請求-應答機制限制了全雙工通信。WebSocket連接建立以后,其實只是簡單規定了一下:接下來,咱們通信就不使用HTTP協議了,直接互相發數據吧。
安全的WebSocket連接機制和HTTPS類似。首先,瀏覽器用wss://xxx
創建WebSocket連接時,會先通過HTTPS創建安全的連接,然后,該HTTPS連接升級為WebSocket連接,底層通信走的仍然是安全的SSL/TLS協議。
瀏覽器
很顯然,要支持WebSocket通信,瀏覽器得支持這個協議,這樣才能發出ws://xxx
的請求。目前,支持WebSocket的主流瀏覽器如下:
- Chrome
- Firefox
- IE >= 10
- Sarafi >= 6
- Android >= 4.4
- iOS >= 8
服務器
由於WebSocket是一個協議,服務器具體怎么實現,取決於所用編程語言和框架本身。Node.js本身支持的協議包括TCP協議和HTTP協議,要支持WebSocket協議,需要對Node.js提供的HTTPServer做額外的開發。已經有若干基於Node.js的穩定可靠的WebSocket實現,我們直接用npm安裝使用即可。
二、webSocket與socket區別
WebSocket在建立握手時,數據是通過HTTP傳輸的。但是建立之后,在真正傳輸時候是不需要HTTP協議的。
Socket其實並不是一個協議,而是為了方便使用TCP或UDP而抽象出來的一層,是位於應用層和傳輸控制層之間的一組接口。
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。
當兩台主機通信時,必須通過Socket連接,Socket則利用TCP/IP協議建立TCP連接。TCP連接則更依靠於底層的IP協議,IP協議的連接則依賴於鏈路層等更低層次。
WebSocket則是一個典型的應用層協議。
三、springboot2.x使用webSocket搭建
1、引入maven的依賴到pom文件
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter --> <!--springboot項目依賴包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.2.2.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket --> <!--webSocket依賴包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> <version>2.2.2.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <!--json轉換使用依賴--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency>
2、創建application.yml或properties配置文件
server:
port: 8080
servlet:
context-path: /demo
3、創建啟動類型並且聲明websocket配置
/** * @author niunafei * @function * @email niunafei0315@163.com * @date 2020/5/11 1:08 PM */ @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } /** * 創建webSocket配置對象 * @return */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
4、創建websocket對象
package com.niu.websocket; import com.alibaba.fastjson.JSONObject; 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; /** * @author niunafei * @function * @email niunafei0315@163.com * @date 2020/5/11 1:18 PM */ @ServerEndpoint("/imserver/{userId}") @Component public class WebSocketServer { /** * 靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。 */ private static int onlineCount = 0; /** * concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。 */ private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>(); /** * 與某個客戶端的連接會話,需要通過它來給客戶端發送數據 */ private Session session; /** * 接收userId */ private String userId = ""; /** * 連接建立成功調用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("userId") String userId) { this.session = session; this.userId = userId; if (webSocketMap.containsKey(userId)) { webSocketMap.remove(userId); webSocketMap.put(userId, this); //加入set中 } else { webSocketMap.put(userId, this); //加入set中 addOnlineCount(); //在線數加1 } System.out.println("用戶連接:" + userId + ",當前在線人數為:" + getOnlineCount()); try { sendMessage("連接成功"); } catch (IOException e) { System.out.println("用戶:" + userId + ",網絡異常!!!!!!"); } } /** * 連接關閉調用的方法 */ @OnClose public void onClose() { if (webSocketMap.containsKey(userId)) { webSocketMap.remove(userId); //從set中刪除 subOnlineCount(); } System.out.println("用戶退出:" + userId + ",當前在線人數為:" + getOnlineCount()); } /** * 收到客戶端消息后調用的方法 * * @param message 客戶端發送過來的消息 */ @OnMessage public void onMessage(String message, Session session) { System.out.println("用戶消息:" + userId + ",報文:" + message); //可以群發消息 //消息保存到數據庫、redis if (message == null) { return; } try { //解析發送的報文 JSONObject jsonObject = JSONObject.parseObject(message); //追加發送人(防止串改) jsonObject.put("fromUserId", this.userId); String toUserId = jsonObject.getString("toUserId"); //傳送給對應toUserId用戶的websocket if (toUserId != null && webSocketMap.containsKey(toUserId)) { webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString()); } else { System.out.println("請求的userId:" + toUserId + "不在該服務器上"); //否則不在這個服務器上,發送到mysql或者redis } } catch (Exception e) { e.printStackTrace(); } } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { System.out.println("用戶錯誤:" + this.userId + ",原因:" + error.getMessage()); error.printStackTrace(); } /** * 實現服務器主動推送 */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 發送自定義消息 */ public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException { System.out.println("發送消息到:" + userId + ",報文:" + message); if (userId != null && webSocketMap.containsKey(userId)) { webSocketMap.get(userId).sendMessage(message); } else { System.out.println("用戶" + userId + ",不在線!"); } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }
5、在resources下添加h5的index.html文件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>websocket通訊</title> </head> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script> var socket; function openSocket() { if (typeof(WebSocket) == "undefined") { console.log("您的瀏覽器不支持WebSocket"); } else { console.log("您的瀏覽器支持WebSocket"); //實現化WebSocket對象,指定要連接的服務器地址與端口 建立連接,注意協議為ws var socketUrl = "ws://localhost:8080/demo/imserver/" + $("#userId").val(); console.log(socketUrl); if (socket != null) { socket.close(); socket = null; } socket = new WebSocket(socketUrl); //打開事件 socket.onopen = function () { $("#msg").append(msg.data) console.log("websocket已打開"); }; //獲得消息事件 socket.onmessage = function (msg) { console.log(msg.data); //發現消息進入 開始處理前端觸發邏輯 $("#msg").append("</br>") $("#msg").append(msg.data) }; //關閉事件 socket.onclose = function () { $("#msg").append("</br>") $("#msg").append(msg.data) console.log("websocket已關閉"); }; //發生了錯誤事件 socket.onerror = function () { $("#msg").append("</br>") $("#msg").append(msg.data) console.log("websocket發生了錯誤"); } } } function sendMessage() { if (typeof(WebSocket) == "undefined") { console.log("您的瀏覽器不支持WebSocket"); } else { console.log("您的瀏覽器支持WebSocket"); console.log('{"toUserId":"' + $("#toUserId").val() + '","contentText":"' + $("#contentText").val() + '"}'); socket.send('{"toUserId":"' + $("#toUserId").val() + '","contentText":"' + $("#contentText").val() + '"}'); } } </script> <body> <div><label style="width: 40px;">userId:</label><input id="userId" name="userId" type="text" value="1"></div> <div><label style="width: 40px;">toUserId:</label><input id="toUserId" name="toUserId" type="text" value="2"></div> <div><label style="width: 40px;">消息內容:</label><input id="contentText" name="contentText" type="text" value="你好!webSocket"></div> <div><label style="width: 40px;">操作:</label> <button onclick="openSocket()">開啟socket</button> <button onclick="sendMessage()">發送消息</button> </div> <div id="msg"></div> </body> </html>
6、為啦不出現404的問題在mvc配置處進行修改
package com.niu.websocket; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; /** * @author niunafei * @function * @email niunafei0315@163.com * @date 2020/5/11 1:37 PM */ @Configuration public class MvcConfig extends WebMvcConfigurationSupport { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 這里之所以多了一"/",是為了解決打jar時訪問不到問題 registry.addResourceHandler("/index.html").addResourceLocations("/index.html","classpath:/index.html"); } }
7、啟動項目瀏覽器訪問http://localhost:8080/demo/index.html 進行測試結果如下圖。