1. 程式人生 > >使用 Spring Boot WebSocket 建立聊天室

使用 Spring Boot WebSocket 建立聊天室

1    第2-10課:使用Spring Boot WebSocket建立聊天室

1.1.1       什麼是 WebSocket

以前,很多網站為了實現推送技術,所用的技術都是輪詢。輪詢是在特定的的時間間隔(如每 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 在適當的擴充套件支援下,可以沿用之前內容的上下文,在傳遞類似的資料時,可以顯著地提高壓縮率。

1.2     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 的使用。

1.3     Websocket 聊天室

Websocket 雙相通訊的特性非常適合開發線上聊天室,這裡以線上多人聊天室為示例,演示 Spring Boot Websocket 的使用。

首先我們梳理一下聊天室都有什麼功能:

  • 支援使用者加入聊天室,對應到 Websocket 技術就是建立連線 onopen
  • 支援使用者退出聊天室,對應到 Websocket 技術就是關閉連線 onclose
  • 支援使用者在聊天室傳送訊息,對應到 Websocket 技術就是呼叫 onmessage 傳送訊息
  • 支援異常時提示,對應到 Websocket 技術 onerror

1.4     頁面開發

利用前端框架 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">使用者姓名 &nbsp;</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" >群發訊息 &nbsp;</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 事件,如果後端服務和前端通訊室將對應的資訊展示在頁面。

1.5     服務端開發

1.5.1   引入依賴

<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 元件。

1.5.2   啟動類

啟動類需要新增 @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();
    }
}

1.5.3   請求接收

在建立服務端訊息接收功能之前,我們先建立一個 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));
}

其中,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());
}

到此我們服務端內容就開發完畢了。

1.6     測試

啟動 spring-boot-websocket 專案,在瀏覽器中輸入地址 http://localhost:8080/ 開啟兩個頁面進行測試。

在第一個頁面中以使用者“小王”登入聊天室,第二個頁面以“小張”登入聊天室。

小王:你是誰?
小張:你猜
小王:我猜好了!
小張:你猜的什麼
小王:你猜?
小張:…

小張離開聊天室…

大家在兩個頁面模式小王和小張對話,可以看到兩個頁面的展示效果,頁面都可實時無重新整理展示最新聊天內容,頁面最終展示效果如下:

 

1.7     總結

這節課首先介紹了 WebSocket,以及 WebSocket 的相關特性和優點,Spring Boot 提供了 WebSocket 對應的元件包,因此很容易讓我們整合在專案中。利用 WebSocket 可以雙向通訊的特點做了一個簡易版的聊天室,來驗證 WebSocket 相關特性,通過示例實踐發現 WebSocket 雙向通訊機制非常高效簡潔,特別適合在服務端和客戶端通訊較多的場景下使用,相比以前的輪詢方式更加優雅易用。

點選這裡下載原始碼