WebSocket的故事(六)—— Springboot中,實現更靈活的WebSocket

xNPE發表於2018-09-29

概述

WebSocket的故事系列計劃分五大篇六章,旨在由淺入深的介紹WebSocket以及在Springboot中如何快速構建和使用WebSocket提供的能力。本系列計劃包含如下幾篇文章:

第一篇,什麼是WebSocket以及它的用途
第二篇,Spring中如何利用STOMP快速構建WebSocket廣播式訊息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的訊息模式(2)
第五篇,Springboot中,實現網頁聊天室之自定義WebSocket訊息代理
第六篇,Springboot中,實現更靈活的WebSocket

本篇的主線

本篇是這個系列的最後一篇,將介紹另一種實現WebSocket的方式。仍然會以一個簡單聊天室為例子進行講述。至此我們也可以根據具體情況,選擇不同的實現方式。

本篇適合的讀者

想了解如何在Springboot上自定義實現更為複雜的WebSocket產品邏輯的各路有志青年。

使用Tomcat提供的WebSocket支援

早在Java EE 7時,就釋出了JSR356規範。Tomcat7.0.47開始,也支援了統一的WebSocket介面。在使用Springboot時,也可以輕鬆的使用Tomcat提供的這些API。今天我們就來體驗一把Tomcat實現的WebSocket

1. 引入依賴

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
複製程式碼

Springboot內建了tomcat,我們直接引入spring的這個高階元件即可。順便多說一句,Springboot的高階元件會自動引用基礎的元件,像spring-boot-starter-websocket就引入了spring-boot-starter-webspring-boot-starter,所以不要重複引入。

2. 使用@ServerEndpoint建立WebSocket Endpoint

首先要注入ServerEndpointExporter,這個Bean會自動註冊使用了@ServerEndpoint註解宣告的WebSocket Endpoint。

package com.draw.wsdraw.websocket;

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();
    }
}
複製程式碼

然後,我們動手實現WebSocket服務的實現類,這裡是WebSocketServer,注意別忘了用@ServerEndpint@Component宣告下。雖然@Component預設是單例模式的,但Springboot還是會為每個WebSocket連線初始化一個Bean,所以可以用一個靜態Map儲存起來。換句話說,每當有一個使用者向伺服器發起連線時,都會建立一個WebSocketServer物件,將此物件按roomId儲存在HashMap中,方便後續使用。

建立ServerEndpoint時,需要對應實現其所需的幾個功能性方法:OnOpen、OnMessage、OnClose、OnError

  • @OnOpen:客戶端向服務端發起建立連線時,服務端呼叫,可傳入的引數為Session(WebSocket的Session)和EndpointConfig。另外,還可以加入帶@PathParam註解的引數。這裡我們註解的引數是roomId,即在建立連線時,攜帶的請求地址上的引數,與我們上一篇中介紹的{INFO}是一樣的作用。
  • @OnMessage:客戶端訊息到來時呼叫,包含會話Session,根據訊息的形式,如果是文字訊息,傳入String型別引數或者Reader,如果是二進位制訊息,傳入byte[]型別引數或者InputStream。
  • @OnClose:當斷開連線,關閉WebSocket時呼叫。
  • @OnError:當發生錯誤時呼叫,傳入異常Session和錯誤資訊。

重寫上述方法,即可實現WebSocket的服務端業務邏輯。

@ServerEndpoint("/webSocket/{roomId}")
@Component
public class WebSocketServer {
    private static ConcurrentHashMap<String, List<WebSocketServer>> webSocketMap =
            new ConcurrentHashMap<>(3);

    //與某個客戶端的連線會話,需要通過它來給客戶端傳送資料
    private Session session;

    //接收roomId
    private String roomId = "";

    /**
     * 連線建立成功呼叫的方法
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config, @PathParam("roomId") String roomId) {
        if (roomId == null || roomId.isEmpty()) return;
        this.session = session;
        this.roomId = roomId;
        addSocketServer2Map(this);
        try {
            sendMessage("連線成功", true);
        } catch (IOException e) {
        }
    }

    /**
     * 連線關閉呼叫的方法
     */
    @OnClose
    public void onClose() {
        List<WebSocketServer> wssList = webSocketMap.get(roomId);
        if (wssList != null) {
            for (WebSocketServer item : wssList) {
                if (item.session.getId().equals(session.getId())) {
                    wssList.remove(item);
                    if (wssList.isEmpty()) {
                        webSocketMap.remove(roomId);
                    }
                    break;
                }
            }
        }
    }
    
    /**
     * 收到客戶端訊息後呼叫的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        //群發訊息
        String msg = filterMessage(message);
        if (msg != null) {
            sendInfo(msg, roomId, session);
        }
    }

    /**
     * 發生錯誤時,呼叫的方法
     */
    @OnError
    public void onError(Session session, Throwable error) {
    }

複製程式碼

這樣,服務端的程式碼就實現完了,這裡僅貼出來部分原始碼,文後會給出專案原始碼地址。

3. 實現客戶端頁面

<script type="text/javascript">
    var ws;
    
    function setConnected(connected){
        document.getElementById('connect').disabled = connected;
        document.getElementById('disconnect').disabled = !connected;
        document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
        $("#response").html();
    }

    function connect(){
        var roomId = $('#roomId').val();
        ws = new WebSocket('ws://localhost:8080/webSocket/' + roomId);
        ws.onopen = WSonOpen;
        ws.onmessage = WSonMessage;
        ws.onclose = WSonClose;
        ws.onerror = WSonError;
    }

    function WSonOpen() {
        var message = {
            name:'Server',
            chatContent:'成功連線'
        }
        setConnected(true);
        showResponse(message)
    };

    function WSonMessage(event) {
        var message = {
            name:'Server',
            chatContent:event.data
        }
        showResponse(message)
    };

    function WSonClose() {
        var message = {
            name:'Server',
            chatContent:'已斷開'
        }
        showResponse(message)
    };

    function WSonError() {
        var message = {
            name:'Server',
            chatContent:'連線錯誤!'
        }
        showResponse(message)
    };

    function disconnect(){
        ws.close()
        setConnected(false);
        console.log("Disconnected");
    }

    function sendMessage(){
        var chatContent = $("#chatContent").val();
        var roomId = $('#roomId').val();
        ws.send(JSON.stringify({'roomId':roomId,'chatContent':chatContent}))
    }

    function showResponse(message){
         var response = $("#response").val();
         $("#response").val(response+message.name+': '+message.chatContent+'\n');
    }
</script>
複製程式碼

客戶端頁面實現了簡單的連線、斷開和訊息傳送功能。這部分就不詳細介紹了。

4. 演示截圖

WebSocket的故事(六)—— Springboot中,實現更靈活的WebSocket

原始碼地址

本篇的原始碼地址:

另一種WebSocket的實現方式

總結

本篇直接使用了Tomcat提供的WebSocket,也是一種相對靈活的實現方式,只需要按照上述步驟來實現即可。集中精力編寫業務邏輯程式碼。

整個一個系列下來,我們介紹了幾種實現WebSocket的方式,有的整合度高,有些相對靈活。大家可以按實際業務需求來選取合適的方式。至此,這個系列就結束了。感謝大家閱讀。

小銘出品,必屬精品

歡迎關注xNPE技術論壇,更多原創乾貨每日推送。

WebSocket的故事(六)—— Springboot中,實現更靈活的WebSocket

相關文章