使用spring boot+WebSocket 實(shí)現(xiàn)定時(shí)消息推送(基于注解)

前言

前面文章也有websocket相關(guān)的文章,為什么這次又要重新寫一篇呢?第一這篇文章需求業(yè)務(wù)場(chǎng)景有些不同,第二這篇文章websocket基本上完全基于注解操作簡(jiǎn)單脏里。
其實(shí)能實(shí)現(xiàn)定時(shí)消息推送的技術(shù)有很多,Dwr虹曙、goeasy迫横、comer4j 、netPush等技術(shù)也可以完全實(shí)現(xiàn)這個(gè)功能.

  • DWR之前文檔的消息推送也有使用到酝碳,但是在實(shí)際項(xiàng)目中表現(xiàn)的并不是很好矾踱,畢竟技術(shù)相對(duì)較老,對(duì)于一些瀏覽器版本兼容性不是很好疏哗,而且容易出現(xiàn)消息丟失的情況呛讲,研究半天源碼改動(dòng)很多無(wú)法解決這個(gè)問(wèn)題。
  • GoEasy 其實(shí)在消息推送方面表現(xiàn)還是比較良好返奉,但是其是收費(fèi)的贝搁,我們項(xiàng)目基本使用開(kāi)源產(chǎn)品對(duì)應(yīng)收費(fèi)產(chǎn)品合規(guī)性檢測(cè)肯定無(wú)法通過(guò),因此沒(méi)有考慮芽偏。
  • 后面兩個(gè)技術(shù)沒(méi)有具體研究過(guò)雷逆,目前覺(jué)得webSocket可以完美解決我現(xiàn)在業(yè)務(wù)需求。

websocket簡(jiǎn)介

websocket協(xié)議是在http協(xié)議上的一種補(bǔ)充協(xié)議污尉,是html5的新特性膀哲,是一種持久化的協(xié)議。其實(shí)websocket和http關(guān)系并不是很大被碗,不過(guò)都是屬于應(yīng)用層的協(xié)議某宪。關(guān)于更多概念大家可以參考下面文章講述的很詳細(xì),接下來(lái)我們就開(kāi)始實(shí)戰(zhàn)锐朴。

https://www.cnblogs.com/fuqiang88/p/5956363.html

websocket 定時(shí)推送

本教程基于springboot為腳手架缩抡,沒(méi)使用過(guò)springboot同學(xué)可以看往期文章,或者直接去spring官網(wǎng)拉一個(gè)springboot基礎(chǔ)項(xiàng)目下來(lái)。

加入依賴

在springboot的項(xiàng)目中添加一下webSocket依賴瞻想,一般一項(xiàng)新技術(shù)的引入在springboot中也只是引用一個(gè)此技術(shù)starter的依賴,其他配置基本springboot幫我們解決了娩嚼。

              <!-- websocket  依賴  -->
            <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

配置

新建一個(gè)Java配置類蘑险,注入ServerEndpointExporter 配置,如果是使用springboot內(nèi)置的tomcat此配置必須岳悟,如果是使用的是外部tomcat容器此步驟請(qǐng)忽略佃迄。看spring源碼中這樣描述贵少,使用此配置可以關(guān)閉servlet容器對(duì)websocket端點(diǎn)的掃描呵俏,這個(gè)暫時(shí)沒(méi)有深入研究。

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

}

核心代碼

接下來(lái)最核心的類其實(shí)就是提供一個(gè)前后端交互的類實(shí)現(xiàn)消息的接收推送滔灶。

  • @ServerEndpoint(value = "/wsdemo") 前端通過(guò)此URI 和后端交互普碎,建立連接
  • @Component 不用說(shuō)將此類交給spring管理
  • @OnOpen websocket建立連接的注解,前端觸發(fā)上面URI時(shí)會(huì)進(jìn)入此注解標(biāo)注的方法录平,和之前關(guān)于DWR文章中的onpage方法類似
  • @OnClose 顧名思義關(guān)閉連接麻车,銷毀session
  • @OnMessage 收到前端傳來(lái)的消息后執(zhí)行的方法
@ServerEndpoint(value = "/wsdemo")
@Component
public class MyWebSocket {
    /**靜態(tài)變量,用來(lái)記錄當(dāng)前在線連接數(shù)斗这。應(yīng)該把它設(shè)計(jì)成線程安全的动猬。*/
    private static int onlineCount = 0;

    /** concurrent包的線程安全Set,用來(lái)存放每個(gè)客戶端對(duì)應(yīng)的MyWebSocket對(duì)象表箭。
        在外部可以獲取此連接的所有websocket對(duì)象赁咙,并能對(duì)其觸發(fā)消息發(fā)送功能,我們的定時(shí)發(fā)送核心功能的實(shí)現(xiàn)在與此變量 */
    private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();

    /**與某個(gè)客戶端的連接會(huì)話免钻,需要通過(guò)它來(lái)給客戶端發(fā)送數(shù)據(jù)*/
    private Session session;

    /**
     * 連接建立成功調(diào)用的方法
     * 
     * 類似dwr的onpage方法彼水,參考之前文章中demo有
     * */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在線數(shù)加1
        System.out.println("有新連接加入!當(dāng)前在線人數(shù)為" + getOnlineCount());
        try {
            sendMessage("連接已建立成功.");
        } catch (Exception e) {
            System.out.println("IO異常");
        }
    }

    /**
     * 連接關(guān)閉調(diào)用的方法
     * 
     * 參考dwrsession摧毀方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);  //連接關(guān)閉后伯襟,將此websocket從set中刪除
        subOnlineCount();           //在線數(shù)減1
        System.out.println("有一連接關(guān)閉猿涨!當(dāng)前在線人數(shù)為" + getOnlineCount());
    }

    /**
     * 收到客戶端消息后調(diào)用的方法
     *
     * @param message 客戶端發(fā)送過(guò)來(lái)的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("來(lái)自客戶端的消息:" + message);
    }

     // 錯(cuò)誤提示
     @OnError
     public void onError(Session session, Throwable error) {
         System.out.println("發(fā)生錯(cuò)誤");
         error.printStackTrace();
     }

     // 發(fā)送消息,在定時(shí)任務(wù)中會(huì)調(diào)用此方法           
     public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
 
     }


  

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

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

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



    public Session getSession() {
        return session;
    }

    public void setSession(Session session) {
        this.session = session;
    }

    public static CopyOnWriteArraySet<MyWebSocket> getWebSocketSet() {
        return webSocketSet;
    }

    public static void setWebSocketSet(CopyOnWriteArraySet<MyWebSocket> webSocketSet) {
        MyWebSocket.webSocketSet = webSocketSet;
    }
}

定時(shí)任務(wù)

使用spring的Schedule建立定時(shí)任務(wù)

  • @EnableScheduling 開(kāi)啟spring定時(shí)任務(wù)功能
  • @Scheduled(cron = "0/10 * * * * ?") 用于標(biāo)識(shí)定時(shí)執(zhí)行的方法姆怪,此處主要方法返回值一定是void叛赚,沒(méi)有入?yún)ⅰ?duì)應(yīng)定時(shí)時(shí)間配置可以百度cron語(yǔ)法稽揭,根據(jù)自己的業(yè)務(wù)選擇合適的周期
    在這類中俺附,我們通過(guò)上面MyWebSocket提供的靜態(tài)方法獲取其中的webSocketSet ,來(lái)獲取所有此業(yè)務(wù)相關(guān)的所有websocketsession溪掀,可以在定時(shí)任務(wù)中對(duì)session內(nèi)容進(jìn)行驗(yàn)證判斷(權(quán)限驗(yàn)證等)事镣,進(jìn)行發(fā)送消息

@Component
@EnableScheduling
public class TimeTask
{
    private static Logger logger = LoggerFactory.getLogger(TimeTask.class);
    @Scheduled(cron = "0/1 * * * * ?")   //每分鐘執(zhí)行一次
    public void test(){
        System.err.println("*********   定時(shí)任務(wù)執(zhí)行   **************");
        CopyOnWriteArraySet<MyWebSocket> webSocketSet = 
     MyWebSocket.getWebSocketSet();
            int i = 0 ;
        webSocketSet.forEach(c->{
            try {
                c.sendMessage("  定時(shí)發(fā)送  " + new Date().toLocaleString());
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        System.err.println("/n 定時(shí)任務(wù)完成.......");
    }

}

前端頁(yè)面

前端頁(yè)面可以參考使用,主要要更改調(diào)用的url為自己項(xiàng)目URL

<!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;

    //判斷當(dāng)前瀏覽器是否支持WebSocket  ,主要此處要更換為自己的地址
    if('WebSocket' in window){
        websocket = new WebSocket("ws://localhost:8886/wsdemo");
    }
    else{
        alert('Not support websocket')
    }

    //連接發(fā)生錯(cuò)誤的回調(diào)方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //連接成功建立的回調(diào)方法
    websocket.onopen = function(event){
        setMessageInnerHTML("open");
    }

    //接收到消息的回調(diào)方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }

    //連接關(guān)閉的回調(diào)方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //監(jiān)聽(tīng)窗口關(guān)閉事件揪胃,當(dāng)窗口關(guān)閉時(shí)璃哟,主動(dòng)去關(guān)閉websocket連接氛琢,防止連接還沒(méi)斷開(kāi)就關(guān)閉窗口,server端會(huì)拋異常随闪。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //將消息顯示在網(wǎng)頁(yè)上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //關(guān)閉連接
    function closeWebSocket(){
        websocket.close();
    }

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

效果演示

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末阳似,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子铐伴,更是在濱河造成了極大的恐慌撮奏,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件当宴,死亡現(xiàn)場(chǎng)離奇詭異畜吊,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)户矢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門玲献,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人逗嫡,你說(shuō)我怎么就攤上這事青自。” “怎么了驱证?”我有些...
    開(kāi)封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵延窜,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我抹锄,道長(zhǎng)逆瑞,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任伙单,我火速辦了婚禮获高,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吻育。我一直安慰自己念秧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布布疼。 她就那樣靜靜地躺著摊趾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪游两。 梳的紋絲不亂的頭發(fā)上砾层,一...
    開(kāi)封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音贱案,去河邊找鬼肛炮。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的侨糟。 我是一名探鬼主播碍扔,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼秕重!你這毒婦竟也來(lái)了蕴忆?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤悲幅,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后站蝠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體汰具,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年菱魔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了留荔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡澜倦,死狀恐怖聚蝶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情藻治,我是刑警寧澤碘勉,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站桩卵,受9級(jí)特大地震影響验靡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜雏节,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一胜嗓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钩乍,春花似錦辞州、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至排作,卻和暖如春牵啦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妄痪。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工哈雏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓裳瘪,卻偏偏與公主長(zhǎng)得像土浸,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子彭羹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容