javascript
springboot 使用 Spring Boot WebSocket 创建聊天室 2-11
什么是 WebSocket
?
WebSocket 協議是基于 TCP 的一種網絡協議,它實現了瀏覽?與服務?全雙工(Full-duplex)通信—允許服務?主動發送信息給客戶端。
以前,很多網站為了實現推送技術,所用的技術都是輪詢。輪詢是在特定的的時間隔(如每 1?秒),由瀏覽?對服務?發出 HTTP?請求,然后由服務?返回最新的數據給客戶端的瀏覽?。這種傳統的模式帶來很明顯的缺點,即瀏覽?需要不斷地向服務?發出請求,然?而 HTTP?請求可能包含較?長的頭部,其中真正有效的數據可能只是很小的一部分,顯然這樣會浪費很多的帶寬等資源。
在這種情況下,HTML 5 定義了 WebSocket 協議,能更好得節省服務?資源和帶寬,并且能夠更實時地進行通訊。WebSocket 協議在 2008 年年誕生,2011 年成為國際標準,現在主流的瀏覽?都已經支持。
它的最大特點就是,服務?可以主動向客戶端推送信息,客戶端也可以主動向服務?發送信息,是真正的雙向平等對話,屬于服務?推送技術的一種。在 WebSocket API 中,瀏覽?和服務?只需要完成一次握手,兩者之間就直接可以創建持久性的連接,并進行雙向數據傳輸。
優點
- 較少的控制開銷。在連接創建后,服務?和客戶端之間交換數據時,用于協議控制的數據包頭部相對較小。在不包含擴展的情況下,對于服務?到客戶端的內容,此頭部大小只有 2 至 10 字節(和數據包長度有關);對于客戶端到服務?的內容,此頭部還需要加上額外的 4 字節的掩碼。相對于 HTTP 請求每次都要攜帶完整的頭部,此項開銷顯著減少了。
-
更強的實時性。由于協議是全雙工的,所以服務?可以隨時主動給客戶端下發數據。相對于 HTTP 請求需要等待客戶端發起請求服務端才能響應,延遲明顯更少;即使是和 Comet 等類似的長輪詢比較,其也能在短時間內更多次地傳遞數據。
-
保持連接狀態。與 HTTP 不同的是,Websocket 需要先創建連接,這就使得其成為一種有狀態的協議,之后通信時可以省略略部分狀態信息,而 HTTP 請求可能需要在每個請求都攜帶狀態信息(如身份認證等)。
-
更好的二進制支持。Websocket 定義了二進制幀,相對 HTTP,可以更輕松地處理二進制內容。 可以支持擴展。Websocket ?定義了擴展,用戶可以擴展協議、實現部分自定義的子協議。如部分瀏覽??持壓縮等。
-
更好的壓縮效果。相對于 ?HTTP ?壓縮,Websocket ??在適當的擴展支持下,可以沿用之前內容的上下文,在傳遞類似的數據時,可以顯著地提高壓縮率。
WebSocket 在握手之后便直接基于 TCP 進行消息通信,但 WebSocket 只是 TCP 上面非常輕的一層,它僅將TCP 的字節流轉換成消息流(文本或二進制),至于怎么解析這些消息的內容完全依賴于應用本身。因此為了協助 Client 與 Server 進行消息格式的協商,WebSocket 在握手的時候保留了一個子協議字段。
Stomp 和 WebSocket
?
STOMP?即 Simple?(or?Streaming)?Text?Orientated?Messaging?Protocol,簡單(流)文本定向消息協議,它提供了一個可互操作的連接格式,允許 STOMP?客戶端與任意 STOMP?消息代理(Broker)進行交互。STOMP?協議由于設計簡單,易于開發客戶端,因此在多種語言和多種平臺上得到了廣泛的應用。
STOMP 協議并不是為 Websocket 設計的,它是屬于消息隊列的一種協議,它和 Amqp、Jms 平級。只不過由于它的簡單性恰巧可以用于定義 Websocket 的消息體格式。可以這么理理解,Websocket 結合 Stomp 子協議段,來讓客戶端和服務?在通信上定義的消息內容達成一致。
STOMP 協議分為客戶端和服務端,具體如下。
?
STOMP 服務端
STOMP 服務端被設計為客戶端可以向其發送消息的一組目標地址。STOMP 協議并沒有規定目標地址的格式,它由使用協議的應用自己來定義。例如,/topic/a、/queue/a、queue-a 對于 STOMP 協議來說都是正確的。應用可以
自己規定不同的格式以此來表明不同格式代表的含義。比如應用自己可以定義以 ?/topic ?打頭的為發布訂閱模式,消息會被所有消費者客戶端收到,以 /user 開頭的為點對點模式,只會被一個消費者客戶端收到。
?
STOMP 客戶端
對于 STOMP 協議來說,客戶端會扮演下列兩種角色的任意一種:
- 作為生產者,通過 SEND 幀發送消息到指定的地址;
- 作為消費者,通過發送 ??SUBSCRIBE ??幀到已知地址來進行消息訂閱,而當生產者發送消息到這個訂閱地址后,訂閱該地址的其他消費者會受到通過 MESSAGE 幀收到該消息。
實際上,WebSocket??結合 ?STOMP??相當于構建了一個消息分發隊列,客戶端可以在上述兩個角色間轉換,訂閱機制保證了一個客戶端消息可以通過服務器廣播到多個其他客戶端,作為生產者,又可以通過服務?來發送點對點消 ?息。
STOMP 幀結構
COMMANDheader1:value1 header2:value2Body^@其中,^@ 表示行結束符。
一個 STOMP 幀由三部分組成:命令、Header(頭信息)、Body(消息體)。
- 命令使用 UTF-8 編碼格式,命令有 SEND、SUBSCRIBE、MESSAGE、CONNECT、CONNECTED 等。
- Header 也使用 UTF-8 編碼格式,它類似 HTTP 的 Header,有 content-length、content-type 等。
- Body 可以是二進制也可以是文本,注意 Body 與 Header 間通過一個空行(EOL)來分隔。
來看一個實際的幀例子:
SEND destination:/broker/roomId/1 content-length:57{“type":"OUT","content":"ossxxxxx-wq-yyyyyyyy"}- 第 1 行:表明此幀為 SEND 幀,是 COMMAND 字段。
- 第?2?行:Header?字段,消息要發送的目的地址,是相對地址。
- 第?3?行:Header?字段,消息體字符?長度。
- 第 4 行:空行,間隔 Header 與 Body。
- 第?5 行:消息體,為自定義的 JSON 結構。
更多 STOMP 協議細節,可以參考 STOMP 官網。
?
WebSocket 事件
Websocket 使用 ws 或 wss 的統一資源標志符,類似于 HTTPS,其中 wss 表示在 TLS 之上的 Websocket。例如:
ws://example.com/wsapi wss://secure.example.com/Websocket 使用和 HTTP 相同的 TCP 端口,可以繞過大多數防火墻的限制。默認情況下,Websocket 協議使用80 端口;運行在 TLS 之上時,默認使用 443 端口。
| 事件 | 事件處理程序 | 描述 |
| open | Sokcket onopen | 連接建立時觸發 |
| message | Sokcket onopen | 客戶端接收服務端數據時觸發 |
| error | Sokcket onerror | 通訊發生錯誤時觸發 |
| close | Sokcket onclose | 鏈接關閉時觸發 |
下?是一個頁面使用 Websocket 的示例:
var ws = new WebSocket("ws://localhost:8080");ws.onopen = function(evt) { console.log("Connection open ..."); ws.send("Hello WebSockets!"); };ws.onmessage = function(evt) { console.log( "Received Message: " + evt.data); ws.close(); };ws.onclose = function(evt) { console.log("Connection closed.");Spring Boot 提供了 Websocket 組件 spring-boot-starter-websocket,用來支持在 Spring Boot 環境下對
Websocket 的使用。
?
Websocket 聊天室
?
Websocket 雙相通訊的特性非常適合開發在線聊天室,這里以在線多人聊天室為示例,演示 Spring Boot Websocket 的使用。
首先我們梳理一下聊天室都有什么功能:
- 支持用戶加?聊天室,對應到 Websocket?技術就是建立連接 onopen
- 支持用戶退出聊天室,對應到 Websocket?技術就是關閉連接 onclose
- 支持用戶在聊天室發送消息,對應到 Websocket 技術就是調用 onmessage 發送消息
- 支持異常時提示,對應到 Websocket 技術 onerror
頁面開發
?
利用前端框架 Bootstrap 渲染頁面,使用 HTML 搭建頁面結構,完整頁面內容如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>chat room websocket</title> <link rel="stylesheet" href="bootstrap.min.css"> <script src="jquery-3.2.1.min.js" ></script> </head> <body class="container" style="width: 60%"> <div class="form-group" ></br> <h5>聊天室</h5> <textarea id="message_content" class="form-control" readonly="readonly" cols="50" rows="10"></textarea> </div> <div class="form-group" > <label for="in_user_name">?用戶姓名 </label> <input id="in_user_name" value="" class="form-control" /></br> <button id="user_join" class="btn btn-success" >加?入聊天室</button> <button id="user_exit" class="btn btn-warning" >離開聊天室</button> </div> <div class="form-group" > <label for="in_room_msg" >群發消息 </label> <input id="in_room_msg" value="" class="form-control" /></br> <button id="user_send_all" class="btn btn-info" >發送消息</button> </div> </body> </html>最上面使用 textarea 畫一個對話框,用來顯示聊天室的內容;中間部分添加用戶加入聊天室和離開聊天室的按鈕, 按鈕上面是輸入用戶名的入口;頁面最下面添加發送消息的入口,頁面顯示效果如下:
接下來在頁面添加 WebSocket 通訊代碼:
<script type="text/javascript"> $(document).ready(function(){ var urlPrefix ='ws://localhost:8080/chat-room/'; var ws = null; $('#user_join').click(function(){ var username = $('#in_user_name').val(); var url = urlPrefix + username; ws = new WebSocket(url); ws.onopen = function () { console.log("建?立 websocket 連接..."); }; ws.onmessage = function(event){ //服務端發送的消息 $('#message_content').append(event.data+'\n'); }; ws.onclose = function(){ $('#message_content').append('?用戶['+username+'] 已經離開聊天室!'); console.log("關閉 websocket 連接..."); } }); //客戶端發送消息到服務器? $('#user_send_all').click(function(){ var msg = $('#in_room_msg').val(); if(ws){ ws.send(msg); } }); // 退出聊天室 $('#user_exit').click(function(){ if(ws){ ws.close(); } }); }) </script>這段代碼的功能主要是監聽三個按鈕的點擊事件,當用戶登錄、離開、發送消息是調用對應的 WebSocket??事件, 將信息傳送給服務端。同時打開頁面時創建了 ?WebSocket?對象,頁面會監控 ?WebSocket?事件,如果后端服務和前端通訊室將對應的信息展示在頁面。
?
服務端開發
引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>主要添加 Web 和 Websocket 組件。
?
啟動類
?
啟動類需要添加 @EnableWebSocket 開啟 WebSocket 功能。
@EnableWebSocket @SpringBootApplication public class WebSocketApplication {public static void main(String[] args) { SpringApplication.run(WebSocketApplication.class, args); }@Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }請求接收
?
在創建服務端消息接收功能之前,我們先創建一個 ??WebSocketUtils ???工具類,用來存儲聊天室在線的用戶信息,以及發送消息的功能。?首先定義一個全局變量 ONLINE_USER_SESSIONS 用來存儲在線用戶,使?用ConcurrentHashMap 提升高并發時效率。
public static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<> ();封裝消息發送方法,在發送之前首先判單用戶是否存在再進行發送:
public static void sendMessage(Session session, String message) { if (session == null) { return; } final RemoteEndpoint.Basic basic = session.getBasicRemote(); if (basic == null) { return; } try { basic.sendText(message); } catch (IOException e) { logger.error("sendMessage IOException ",e); } }聊天室的消息是所有在線用戶可見,因此每次消息的觸發實際上是遍歷所有在線用戶,給每個在線用戶發送消息。
public static void sendMessageAll(String message) { ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, message)) ;其 中,
是 JDK 1.8 forEach 的簡潔寫法。
這樣我們在創建 ChatRoomServerEndpoint 類的時候就可以直接將工具類的方法和全局變量導入:
import static com.neo.utils.WebSocketUtils.ONLINE_USER_SESSIONS; import static com.neo.utils.WebSocketUtils.sendMessageAll;接收類上需要添加 @ServerEndpoint("url") 代表監聽此地址的 WebSocket 信息。
@RestController @ServerEndpoint("/chat-room/{username}") public class ChatRoomServerEndpoint { }?用戶登錄聊天室時,將用戶信息添加到 ONLINE_USER_SESSIONS 中,同時通知聊天室中的人。
@OnOpen public void openSession(@PathParam("username") String username, Session session) { ONLINE_USER_SESSIONS.put(username, session); String message = "歡迎用戶[" + username + "] 來到聊天室!"; logger.info("用戶登錄:"+message); sendMessageAll(message); }其中,@OnOpen 注解和前端的 onopen 事件一致,表示用戶建立連接時觸發。當聊天室某個用戶發送消息時,將此消息同步給聊天室所有人。
@OnMessage public void onMessage(@PathParam("username") String username, String message) { logger.info("發送消息:"+message); sendMessageAll("?用戶[" + username + "] : " + message); }其中,@OnMessage 監聽發送消息的事件。
?
當用戶離開聊天室后,需要將用戶信息從 ONLINE_USER_SESSIONS 移除,并且通知到在線的其他用戶:
@OnClose public void onClose(@PathParam("username") String username, Session session) { //當前的Session 移除 ONLINE_USER_SESSIONS.remove(username); //并且通知其他?人當前?用戶已經離開聊天室了了 sendMessageAll("?用戶[" + username + "] 已經離開聊天室了了!"); try { session.close(); } catch (IOException e) { logger.error("onClose error",e); } }其中,@OnClose 監聽?用戶斷開連接事件。
?
當 WebSocket 連接出現異常時,出觸發 @OnError ?事件,可以在此?方法內記錄下錯誤的異常信息,并關閉用戶連接。
@OnError public void onError(Session session, Throwable throwable) { try { session.close(); } catch (IOException e) { logger.error("onError excepiton",e); } logger.info("Throwable msg "+throwable.getMessage()); }到此我們服務端內容就開發完畢了。
測試
啟動 spring-boot-websocket 項目,在瀏覽?中輸入地址 ?http://localhost:8080/ ?打開兩個頁面進行測試。在第一個頁面中以用戶“小王”登錄聊天室,第二個頁面以“小張”登錄聊天室。
小王:你是誰? 小張:你猜 小王:我猜好了! 小張:你猜的什么 小王:你猜? 小張:…大家在兩個頁面模式小王和小張對話,可以看到兩個頁面的展示效果,頁面都可實時無刷新展示最新聊天內容,頁
面最終展示效果如下:
總結
?
這節課?首先介紹了 WebSocket,以及 WebSocket 的相關特性和優點,Spring Boot 提供了 WebSocket 對應的組件包,因此很容易讓我們集成在項目中。利用 WebSocket 可以雙向通訊的特點做了一個簡易版的聊天室,來驗證WebSocket 相關特性,通過示例實踐發現 WebSocket 雙向通訊機制非常高效簡潔,特別適合在服務端和客戶端通訊較多的場景下使用,相比以前的輪詢?方式更加優雅易用。
點擊這?下載源碼
總結
以上是生活随笔為你收集整理的springboot 使用 Spring Boot WebSocket 创建聊天室 2-11的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LGA1155、LGA1156、LGA1
- 下一篇: web网页设计实例作业 ——电影泰坦尼克