服務器推送消息方法總結及實現(java)

服務器推送消息方法總結及實現(java)

最近在進行web開發時,有用到服務端推送消息這個功能,相信大家在平常開發時,也經常會有這種需求。本文對常用的幾種服務器推送消息方法進行整理和總結,並實現使用流的方式推送消息(java)。

服務器推送消息主要有一下幾種方法:

  • 輪詢
  • http流
  • websocket
  • http2.0

下面對各個方法一一進行介紹。

輪詢

輪詢分爲短輪詢和長輪詢。
短輪詢即瀏覽器定時向服務器發送請求,以此來更新數據的方法。如下圖所示



(圖片來自javascript高級程序設計第三版)

瀏覽器每隔一段時間向服務器發送一次請求,請求瀏覽器想要的數據。嚴格意義上講:短輪詢不是服務器推送的消息,獲取的數據也不是實時的

實現原理:

在瀏覽器使用定時器setTimeout或setInterval即可。不進行講解了。

長輪詢長輪詢是短輪詢的一個翻版,或者叫改進版。瀏覽器向服務器發送一個請求看有沒有數據,有數據就響應,沒數據就保持該請求,知道有數據再返回。瀏覽器在服務器返回數據時再發送一個請求。這樣瀏覽器就可以一直獲取到最新的數據。長輪詢的時間線如下圖所示



(圖片來自javascript高級程序設計第三版)

實現原理:

在請求響應時,再次發送一個數據請求即可。

http流

流不同於上述兩種輪詢,因爲它在頁面的整個生命週期內只使用一個 HTTP 連接。具體來說,就是瀏覽器向服務器發送一個請求,而服務器一直保持連接打開,然後週期性地向瀏覽器發送數據。

實現:

本例以spring boot框架爲基礎,github地址如下:https://github.com/xubaodian/JAVA-SSE

下載該實例,並啓動。該實例端口號爲10000。

我們先對實例進行驗證測試,然後再講解代碼。

測試頁面地址爲:http://localhost:10000/subscribe.html

測試步驟如下:

1、進入http://localhost:10000/subscribe.html,頁面如下圖所示:



左側是訂閱消息的操作和展示頁面,右側是發佈內容的頁面。
2、左側輸入訂閱消息主題,點擊訂閱,訂閱相關主題消息,例如:輸入財經新聞主題FinancialNews,點擊訂閱,這樣就訂閱了財經新聞了。
3、在右側發佈內容頁面輸入主題和內容,點擊發布。這樣就可以發佈內容了。

測試結果如下:



訂閱了財經主題新聞,右側發佈了5條新聞,3條財經新聞,一條天氣新聞,一條時政新聞,訂閱者收到了3條財經新聞推送信息,證明我們工程已經跑起來了,實現了http流推送的最基本功能。

下面,對工程代碼進行分析:

java代碼

該工程使用spring boot框架,項目端口號爲10000,接口代碼如下:

package com.xbd.pushdata.controller;

import com.xbd.pushdata.Utils.ReqContextUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class SubscribeController {

    @RequestMapping("/subscribe")
    public void subscribe(HttpServletRequest req, HttpServletResponse res, @RequestParam("topic") String topic) {
        ReqContextUtils.addSubscrib(topic, req, res);
    }

    @RequestMapping("/publish")
    public void publish(@RequestParam("topic") String topic, @RequestParam("content") String content) {
        ReqContextUtils.publishMessage(topic, content);
    }

}

有兩個接口:

"/subscribe"接口:用於消息訂閱,該接口有一個參數topic,即訂閱的消息主題。

"/publish"接口:發佈消息接口,有兩個參數,topic是發佈消息主題,content是發佈消息內容。

訂閱和發佈消息的才做都封裝在ReqContextUtils類中,ReqContextUtils的代碼如下,代碼中註釋比較多,不再講解了:

package com.xbd.pushdata.Utils;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;

public class ReqContextUtils {
    //超時時間
    private static int DEFAULT_TIME_OUT = 60*60*1000;
    //訂閱列表,存儲所有主題的訂閱請求,每個topic對應一個ArrayList,ArrayList裏該topic的所有訂閱請求
    private static HashMap<String, ArrayList<AsyncContext>> subscribeArray = new LinkedHashMap<>();

    //添加訂閱消息
    public static void addSubscrib(String topic, HttpServletRequest request, HttpServletResponse response) {
        if (null == topic || "".equals(topic)) {
            return;
        }
        //設置響應頭ContentType
        response.setContentType("text/event-stream");
        //設置響應編碼類型
        response.setCharacterEncoding("UTF-8");
        //request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
        //支持異步響應
        //異步這個概念很多地方都有,就像處理文件時,不是一直等待文件讀完,而是讓它去讀,cpu做其它事情,讀完通知cpu來處理即可。
        AsyncContext actx = request.startAsync(request, response);
        actx.setTimeout(DEFAULT_TIME_OUT);
        //添加一些監聽函數
        actx.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent event) throws IOException {
                System.out.println("推送結束");
            }

            @Override
            public void onTimeout(AsyncEvent event) throws IOException {
                System.out.println("推送超時");
            }

            @Override
            public void onError(AsyncEvent event) throws IOException {
                System.out.println("推送錯誤");
            }

            @Override
            public void onStartAsync(AsyncEvent event) throws IOException {
                System.out.println("推送開始");
            }
        });
        //將異步請求存入列表
        ArrayList<AsyncContext> actxList = subscribeArray.get(topic);
        if (null == actxList) {
            actxList = new ArrayList<AsyncContext>();
            subscribeArray.put(topic, actxList);
        }
        actxList.add(actx);
    }

    //獲取訂閱列表
    public static ArrayList<AsyncContext> getSubscribList(String topic) {
        return subscribeArray.get(topic);
    }

    //推送消息
    public static void publishMessage(String topic, String content) {
        //獲取對應topic的訂閱列表
        ArrayList<AsyncContext> actxList = subscribeArray.get(topic);
        if (null != actxList) {
            for(AsyncContext actx :actxList) {
                try {
                    PrintWriter out = actx.getResponse().getWriter();
                    out.print(content);
                    actx.getResponse().flushBuffer();
                    //out.flush();
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }
    }
}

前端代碼

前端代碼如下,主要就是2個請求,代碼中有註釋,不再講解了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>訂閱消息</title>
    <style>
        .left-container {
            float: left;
            width: 350px;
            min-height: 300px;
            border-right: 3px solid #4b4b4b;
        }
        .left-container li{
            text-overflow: ellipsis;
            white-space: nowrap;
            overflow: hidden;
        }
        .right-container{
            padding-left: 30px;
            float: left;
            width: 350px;
        }
    </style>
</head>
<body>
<div class="left-container">
    <label>訂閱主題</label>
    <input type="text" id="topic">
    <button onclick="subscribe()">訂閱</button>
    <div>收到消息如下:</div>
    <ul id="message"></ul>
</div>
<div class="right-container">
    <div>
        <label>消息主題</label>
        <input type="text" id="pub_topic">
    </div>
    <div>
        <label>消息內容</label>
        <input type="text" id="pub_content">
    </div>
    <button onclick="publish()">發佈</button>
    <div>發佈消息和內容如下:</div>
    <ul id="pub_message"></ul>
</div>

<script>
    function subscribe() {
        let topic = document.getElementById('topic').value;
        let url = location.origin + '/subscribe?topic=' + topic;
        send(url, null, process);
    }

    //發送訂閱消息
    function send(url, data, callback) {
        let xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function(){
            //http流的響應時,xhr.readyState爲3
            if (xhr.readyState == 3 || xhr.readyState == 4){
                if (callback) {
                    callback(xhr.responseText);
                }
            }
        };
        xhr.open('get', url, true);
        xhr.send(data);
    }

    let len = 0;
    //處理訂閱消息
    function process(messsage) {
        let li = document.createElement('li');
        li.innerHTML = messsage.substr(len);
        len = messsage.length;
        let ul = document.getElementById('message');
        ul.appendChild(li);
    }

    //發佈消息
    function publish() {
        let topic = document.getElementById('pub_topic').value;
        let content = document.getElementById('pub_content').value;
        let url = location.origin + '/publish?topic=' + topic + '&content=' + content;
        send(url, null, null);
        let li = document.createElement('li');
        li.innerHTML = `發佈主題:${topic}; 發佈內容:${content}`;
        let ul = document.getElementById('pub_message');
        ul.appendChild(li);
    }
</script>
</body>
</html>

webSocket推送消息

Web Sockets 的是在一個單獨的持久連接上提供全雙工、雙向通信。在 JavaScript 中創建了 Web Socket 之後,會有一個 HTTP 請求發送到瀏覽器以發起連接。在取得服務器響應後,建立的連接會從 HTTP 協議升級爲 Web Socket 協議。

使用spring框架可以很容易實現websocket,這是spring實現websocket的官方教程(非常詳細)地址:https://spring.io/guides/gs/messaging-stomp-websocket/ ,需要的可以移步官方網頁學習。

http2.0

http2.0的特點是首部壓縮,多路複用,請求響應管線化,服務器推送等等,這些特點是建立在http2.0流的基礎上的。

具體想要學習http2.0的同學可以上網找下資料,這裏只是提一下,不做過多描述了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章