1. 程式人生 > >使用SpringBoot快速搭建WebSocket實現訊息推送

使用SpringBoot快速搭建WebSocket實現訊息推送

本文旨在幫助未掌握此技能的小白掃清障礙,快速搭建websocket訊息推送服務,高手請繞行。謝謝!

首先,筆者的寫作背景也是一名剛剛打通websocket訊息推送服務的小白。在連續幾日的蒐集資料下,最終在沒有找到一個完整的解決方案的情況下。摸索出正確的結果,倍感不易的同時,希望能夠記錄下自己心路歷程的同時真正幫助到那些正在此處掙扎的道友。

筆者此前參考了眾多資料之後,最終採用的是@Bean註冊ServerEndpointExporter,並註解@ServerEndpoint的方式,簡單直接。參考前輩長樂忘憂的個人部落格:

然而還是在實現的過程中出現一系列問題,下面會詳細描述,具體實現步驟如下。

首先介紹使用SpringBoot內建Tomcat的配置方式,我們按照流程編輯序號如下:

1、配置SpringBootMaven環境
pom檔案中,需要引入spring-boot-starter-websocket的資源包

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version
>
1.3.5.RELEASE</version> </dependency>

2、配置ServerEndpointExporter的元件註冊。
這個bean會自動註冊使用了@ServerEndpoint註解宣告的Websocket endpoint。

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

3、實現類的編碼。
此處程式碼原型依然來源於參考文件,後經筆者調整完善,修改內容如下:
1>完善了統計連線人數,每次關閉會話時都會無限-1的bug。
此bug導致所有異常連線的關閉都會將線上人數-1。某些情況下是@onOpen執行沒有成功的,但卻會執行@onClose,於是會造成線上人數統計異常。
2>添加了對業務登入使用者和會話使用者的繫結。
根據參考文件原始碼,可操作連線成功的會話使用者,即websocket session。但是實際開發中,我們操作的是當前業務使用者,即資料庫user table中的使用者。因此,此處需要將業務使用者與會話使用者繫結,從而實現1對1或多對多的靈活自由訊息互動。
實現方式是:在@ServerEndpoint註解時,value值可設定路徑引數。而在連線成功,即@onOpen執行時可接收此引數,接收方式為@PathParam。通過此引數即可將業務使用者與會話使用者繫結。
3>基於業務特點以及實用/安全性等考慮,筆者對程式碼做出一個原則性限制,即一個業務使用者同時只能連線成功一個會話使用者。若讀者不需要此限制,則自行修改程式碼即可。
4>添加了幾個工具方法:
(1)getcurrentWenSocket(Integer fuserid)根據當前登入的業務使用者,獲取其對應的會話使用者的websocket物件。方便於“系統–>使用者”的面對點定點訊息推送。
(2)sendMessage(Integer fuserid,String message) 給指定使用者傳送訊息,即集成了(1)方法,可直接呼叫此方法給當前業務使用者推送訊息,可用於傳送私信。
(3)sendMessageList(List fuseridList, String message) 給指定業務使用者列表傳送訊息,即集成了方法(2),此方法可給指定群體推送同一條訊息,可用於傳送區域性公告。
(4)sendMessageAll(String message) 給所有線上使用者傳送訊息。此方法可用於傳送全網公告。
程式碼如下:

package com.iking.tms.util;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;


/**
 * websocket
 * @author hufx
 * @version 1.0
 * @date 2017年6月2日上午10:27:40
 */
@ServerEndpoint(value = "/websocket/{fuserid}")
@Component
public class WebSocket {
    private static Logger log = LoggerFactory.getLogger(WebSocket.class);

    private static int onlineCount = 0;                             //靜態變數,用來記錄當前線上連線數。應該把它設計成執行緒安全的。

    private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<WebSocket>(); //concurrent包的執行緒安全Set,用來存放每個客戶端對應的MyWebSocket物件。

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

    private Integer fuserid;                                        //儲存當前登入使用者ID

    /**
     * 連線建立成功呼叫的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "fuserid") Integer fuserid) {
        try {
            this.session = session;                                 //設定當前session
            this.fuserid = fuserid;
            WebSocket _this = getcurrentWenSocket(this.fuserid);    //當前登入使用者校驗  每個使用者同時只能連線一次
            if(_this != null){
                sendMessage("您已有連線資訊,不能重複連線 !");
                return;
            }
            webSocketSet.add(this);                                 //將當前websocket加入set中
            addOnlineCount();                                       //線上數加1
            sendMessage("連線成功!");
            System.out.println("有一新連線!當前線上人數為" + getOnlineCount());
        } catch (IOException e) {
            System.out.println("連線異常!");
            log.error("websocket連線異常  : 登入人ID = " + this.fuserid +" , Exception = " + e.getMessage());
        }
    }

    /**
     * 連線關閉呼叫的方法
     */
    @OnClose
    public void onClose() {
        boolean b = webSocketSet.remove(this);                      //從set中刪除
        if(b && getOnlineCount() > 0){
            subOnlineCount();                                       //線上數減1
        }                   
        System.out.println("有一連線關閉!當前線上人數為" + getOnlineCount());
    }

    /**
     * 收到客戶端訊息後呼叫的方法
     *
     * @param message 客戶端傳送過來的訊息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            WebSocket _this = null;
            for (WebSocket item : webSocketSet) {
                if(item.session.getId() == session.getId()){
                    _this = item;
                }
            }
            if(_this == null){
                this.sendMessage("未連線不能傳送訊息!");
                return;
            }
            System.out.println("來自客戶端的訊息:" + message);
            this.sendMessage("來自服務端的訊息: <已讀> " + message);
        } catch (IOException e) {
            System.out.println("傳送訊息異常!");
            log.error("websocket傳送訊息異常  : 登入人ID = " + this.fuserid +" , Exception = " + e.getMessage());
        }
    }

    /**
     * 發生錯誤時呼叫
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("發生錯誤!");
        log.error("websocket發生錯誤  : 登入人ID = " + this.fuserid);
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocket.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocket.onlineCount--;
    }

    /**
     * 根據當前登入使用者ID獲取他的websocket物件
     * @param fuserid 使用者ID
     * @return
     * MyWebSocket
     * @author hufx
     * @date 2017年6月2日上午10:35:32
     */
    public static WebSocket getcurrentWenSocket(Integer fuserid){
        if(fuserid == null || fuserid < 1 || webSocketSet == null || webSocketSet.size() < 1){
            return null;
        }
        Iterator<WebSocket> iterator = webSocketSet.iterator();
        while (iterator.hasNext()) {
            WebSocket _this = iterator.next();
            if(_this.fuserid == fuserid){
                return _this;
            }
        }
        return null;
    }

    /**
     * 給當前使用者發訊息(單條)
     * @param message 訊息
     * @throws IOException
     * void
     * @author hufx
     * @date 2017年6月1日下午2:05:36
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
        //this.session.getAsyncRemote().sendText(message);
    }

    /**
     * 給指定使用者髮指定訊息(單人單條)
     * @param fuserid   使用者ID
     * @param message   訊息
     * void
     * @author hufx
     * @date 2017年6月2日上午11:13:26
     */
    public static void sendMessage(Integer fuserid,String message){
        try {
            if(fuserid == null || fuserid < 1 || StringUtils.isBlank(message)){
                return;
            }
            WebSocket _this = getcurrentWenSocket(fuserid);
            if(_this == null){
                return;
            }
            _this.sendMessage(message);
        } catch (IOException e) {
            System.out.println("傳送訊息異常!");
        }
    }

   /**
    * 給指定人群發訊息(單條)
    * @param fuseridList 使用者ID列表
    * @param message 訊息
    * void
    * @author hufx
    * @date 2017年6月2日上午11:25:29
    */
    public static void sendMessageList(List<Integer> fuseridList, String message){
        try{
            if(fuseridList == null || fuseridList.size() < 1 || StringUtils.isBlank(message)){
                return;
            }
            for (Integer fuserid : fuseridList) {
                WebSocket _this = getcurrentWenSocket(fuserid);
                if(_this == null){
                    continue;
                }
                _this.sendMessage(message);
            }
        }catch(Exception e){
            System.out.println("傳送訊息異常!");
            log.error("websocket傳送訊息異常  : 登入人ID = " + fuseridList.toString() +" , Exception = " + e.getMessage());
        }
    }

    /**
     * 給所有線上使用者發訊息(單條)
     * @param message 訊息
     * @throws IOException
     * void
     * @author hufx
     * @date 2017年6月2日上午11:11:05
     */
    public static void sendMessageAll(String message) {
        try {
            if(webSocketSet == null || webSocketSet.size() < 1 || StringUtils.isBlank(message)){
                return;
            }
            for (WebSocket item : webSocketSet) {
                item.sendMessage(message);
            }
        } catch (IOException e) {
            System.out.println("傳送訊息異常!");
            log.error("websocket傳送訊息異常  : Exception = " + e.getMessage());
        }
    }

}

4、前段頁面程式碼
此程式碼為參考文件原始碼。

<!DOCTYPE HTML>
<html>
<head>
    <title>My WebSocket</title>
</head>

<body>
Welcome<br/>
<input id="text" type="text" /><button onclick="send()">Send</button>    <button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>
</body>

<script type="text/javascript">
    var websocket = null;

    //判斷當前瀏覽器是否支援WebSocket
    if('WebSocket' in window){
        websocket = new WebSocket("ws://localhost:8084/websocket");
    }
    else{
        alert('Not support websocket')
    }

    //連線發生錯誤的回撥方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //連線成功建立的回撥方法
    websocket.onopen = function(event){
        setMessageInnerHTML("open");
    }

    //接收到訊息的回撥方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }

    //連線關閉的回撥方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //監聽視窗關閉事件,當視窗關閉時,主動去關閉websocket連線,防止連線還沒斷開就關閉視窗,server端會拋異常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //將訊息顯示在網頁上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //關閉連線
    function closeWebSocket(){
        websocket.close();
    }

    //傳送訊息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
</script>
</html>

截至此時,SpringBoot核心Tomcat啟動的 websocket已經配置完成,可以投入測試。

測試方式:
1>右鍵專案run as Java Applacation 啟動。
2>編輯前端頁面檔案,將new WebSocket(“ws://localhost:8084/websocket/1”);中的路徑引數修改到
本地儲存即可。
3>雙擊使用瀏覽器開啟此頁面,見到下圖即表明連線成功。
這裡寫圖片描述
4>連線成功之後即可傳送訊息內容給後臺。後臺Conlose控制檯即可見到此連線使用者資訊。

接下來會講述maven build 打包時所遇到的問題以及外部Tomcat的websocket 配置調整。

在SpringBoot Run As 啟動方式測試成功之後,最終是要將專案部署到伺服器Web容器中去的。那麼,上述程式碼在部署以及打包時會遇到一系列問題。具體解釋如下:

5、初次打包部署
對上述上述程式碼進行maven build 打包操作,我們以war包為例。具體打包操作此處不做闡述。
上述程式碼打包不會報錯。
但是在部署時,基本上都會遇到一個錯誤:
中文解釋是 “非法堆疊異常”,描述是 “@ServerEndpoint 元件註冊失敗”。

Application startup failed
java.lang.IllegalStateException: Failed to register @ServerEndpoint class: class com.iking.tms.util.WebSocket

究其原因,苦思不得其解。在調查了大量資料後,得出如下結論:
1>、tomcat7以下的版本(包含tomcat7部分版本),均不支援websocket。而@ServerEndpoint為javax.websocket包下的。此包在tomcat7後期版本及後續版本中,tomcat的lib目錄下可見。名稱為:“websocket-api.jar”。
2>、出現此問題的原因。
此時要從SpringBoot的特性說起,我們知道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類資原始檔,因此自然會註冊失敗。
3>、解決方案。
為了解決此問題,我們可以手動將此包引入專案。
有兩種引入方式:
1>>pom.xml 引入
2>>.jar檔案直接引入
6、解決問題。
兩種解決方案均可,但推薦使用.jar檔案直接copy進專案build path即可。
這裡重點說明pom檔案引入
程式碼如下:
pom做如下配置:

       <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>javax.servlet-api</artifactId>
         <version>3.1-b07</version>
       </dependency>

之後再打包。打包時可能會出現“javax.servlet-api不存在”的錯誤。出現此錯誤時,我們需要將此jar的路徑手動引入專案。引入方式如下:
參考文件:

當然,在方便專案遷移,引入是自然是要從Tomcat下找到此jar包儲存在專案目錄下的。因此。推薦使用jar檔案直接build path的方式。

7、再次打包。
此時問題已基本解決,打包成功!
部署!
但是會驚奇的發現,部署時仍會報Failed to register @ServerEndpoint。
梳理後發現原因如下。
8、最終問題得解。
上面有簡述過當我們使用外部Tomcat時,專案的管理權將會由Spring交接至Tomcat。
而Tomcat7及後續版本是對websocket直接支援的,且我們所使用的jar包也是tomcat提供的。
但是我們在WebSocketConfig中將ServerEndpointExporter指定給Spring管理。而部署後ServerEndpoint是需要Tomcat直接管理才能生效的。所以此時即就是此包的管理權交接失敗,那肯定不能成功了。最後我們需要將WebSocketConfig中的bean配置註釋掉。然後再打包上傳部署測試。一切正常!

這裡寫圖片描述

至此,此問題完全解決!

以上為筆者解決SpringBoot實現websocket訊息推送的全部步驟、問題、bug、解決方案以及筆者個人的見解。
文中如有錯誤,還望不吝指正!
筆者郵箱 : [email protected]

相關推薦

使用SpringBoot快速搭建WebSocket實現訊息

本文旨在幫助未掌握此技能的小白掃清障礙,快速搭建websocket訊息推送服務,高手請繞行。謝謝! 首先,筆者的寫作背景也是一名剛剛打通websocket訊息推送服務的小白。在連續幾日的蒐集資料下,最終在沒有找到一個完整的解決方案的情況下。摸索出正確的結果,倍

websocketspringboot使用websocket實現訊息

首先我們引用Spring-boot所帶的websocket依賴: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>s

springboot整合websocket實現訊息.

springboot整合websocket實現訊息推送 1.maven配置 2.書寫後端程式碼 3.書寫前端程式碼 4.測試 1.maven依賴 <dependency> <groupId>org

WebSocket實現訊息

引言 最近專案中需要實現訊息推送需求,首先想到就是用webscket來實現IM,之前瞭解過這個東西,但是很久沒有用了,所以需要來弄個demo熱熱身,這樣在專案中使用的時候,會更靠譜些。先來看一下最後的效果:          一、Socket簡介 Socket又稱"套

SpringBoot中使用Websocket進行訊息

WebsocketConfig.java @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return

[ Spring Boot ] 整合 Websocket 實現訊息框架的設計筆記

前段時間,專案中用Websocket實現了一套後臺向前端推送的Service層搭建,感興趣的童鞋可以瞭解下^_^Maven pom<dependency> <groupId&g

使用Websocket實現訊息(心跳)

0x00 心跳 本來以為寫完了,結果最近和一個同事在討論心跳的事情,這裡再做一個補充。先說我的結論: WebSocket協議已經設計了心跳,這個功能可以到達檢測連結是否可用 心跳是用來檢測連結是否可用的,不一定支援攜帶資料,可要看具體實現 如果非要心跳中帶

(二)websocket實現訊息之基於spring4.0實現

  1、新建springBoot專案,新增依賴        &n

Springboot+websocket+定時器實現訊息

由於最近有個需求,產品即將到期(不同時間段到期)時給後臺使用者按角色推送,功能完成之後在此做個小結 1. 在啟動類中添加註解@EnableScheduling import org.mybatis.spring.annotation.MapperScan; import

Go websocket訊息(視訊彈幕的簡單實現原理)

server.go package main import ( "github.com/gorilla/websocket" "net/http" "socket/impl" "time" ) var ( upGrader = websocket.Upgr

ssm中spring websocket 實現伺服器訊息 以及 一對一聊天

上網看了很多方式,最後覺得這種方式比較簡單易懂,這邊主要有三個類(包括註解的配置檔案)就可以實現後臺內容文末會展示結果例項,如果是你所需要的效果,直接拿去用吧~專案中複製直接用本文根據網上整理並修改!!!本文思路來自:連結現在開始。開始前請確保pom已經引入需要的包首先是配置

Netty+WebSocket簡單實現訊息

依賴 <dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifact

實現websocket 主動訊息,用laravel+Swoole

近來有個需求:想實現一個可以主動觸發訊息推送的功能,這個可以實現向模板訊息那個,給予所有成員傳送自定義訊息,而不需要通過客戶端傳送訊息,服務端上message中監聽傳送的訊息進行做相對於的業務邏輯。 主動訊息推送實現平常我們採用 swoole 來寫 WebSocket 服務可能最多的用到的是open,me

使用websocket進行訊息服務

Websocket主要做訊息推送,簡單,輕巧,比comet好用 入門瞭解:https://www.cnblogs.com/xdp-gacl/p/5193279.html   /** * A Web Socket session represents a conversation bet

Java WebSocket程式設計(二):WebSocket實現主動互動

WebSocket協議 WebSocket協議通訊機制 WebSocket協議是獨立的、基於TCP的協議。其本質是先通過HTTP/HTTPS協議進行握手後建立一個用於交換資料的TCP連線,此後伺服器端與客戶器端通過此TCP連線進行實時通訊。 WebSocket開啟握手

藉助微信第三方實現訊息和提醒

一.前言 近來在負責微信端的專案開發,遇到了一個比較奇特的需求,使用者不想關注本公眾號(可能是怕隱私或者其他等等)但是還想收到推送給的訊息提醒。搜尋良久,最後在同事口中得知微信有專門實現這種功能的公眾號。 二.PushBear 基於微信模板的一對多訊息送達服務 API 就兩個引數  https:/

vue+socket實現訊息

前提:後臺已設定好socket訊息 首先在vue專案中引入socket。在npm下載socket。 npm install vue-socket.io 當然也可以在index.html中直接插入下面這句,但是最好不要這樣做。 <script src='https://cdn.bo

WebSocket實現實時資料到前端

@Component @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer{ @Resource goodsWebSocketHandler handler;

【小程式】如何實現訊息之收集

當訂單狀態變更時,小程式如何實現訊息推送來通知到使用者呢。微信開放了一個叫模板訊息的功能。  要實現訊息推送,分三步走 一、前期配置工作 二、前端工作 要實現推送訊息給使用者,就要有推送碼,官方API介紹提交一次表單有一次推送機會,完成一次支付行為有三次推送機

企業微信簡單實現訊息

廢話不多說,上來就堆程式碼.......... 感覺挺簡單,就不過多解釋,應該一看就懂..... import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.uti