1. 程式人生 > >淺析web端的訊息推送原理

淺析web端的訊息推送原理

開發十年,就只剩下這套架構體系了! >>>   


轉載本文需註明出處:EAWorld,違者必究。

引言:

在網際網路高速發展的時代裡,web應用大有取代桌面應用的趨勢,不必再去繁瑣的安裝各種軟體,只需一款主流瀏覽器即可完成大部分常規操作,這些原因都在吸引著軟體廠商和消費者。而隨著各大廠商瀏覽器版本的迭代,前端技術的不斷革新,訊息推送用到的場景也越來越多了。

收發郵件提醒,線上IM聊天,自動化辦公提示等等,web系統裡總是能見到訊息推送的應用。訊息推送用好了能增強使用者體驗,用不好則會起相反的效果。在司空見慣的使用過程中,有沒有對其中的原理產生興趣呢?實現訊息推送有N種解決方案,本文針對其中的幾種,進行原理性的講解並附有簡單的程式碼實現。

目錄:



一、什麼是訊息推送
二、web端的訊息推送
三、實現個性化的推送
 

一、什麼是訊息推送

 

  • 經典場景1



當我在官網觀望猶豫時,突然看到了上面訊息,一位神祕的徐老闆竟然爆出了麻痺戒指!!我的天,於是我果斷開始了遊戲!這訊息很及時!
 

  • 經典場景2



當我拿起手機不知幹嘛時收到了這條招女婿的訊息.......瞬間來了精神

上述兩種場景,是生活中很常見的場景,通過圖文描述,應該已經清楚了推送的場景,也引出了兩大推送種類,web端訊息推送和移動端訊息推送。接下來對訊息推送進行具體的解釋。

概念:

訊息推送(Push)指運營人員通過自己的產品或第三方工具對使用者當前網頁或移動裝置進行的主動訊息推送。使用者可以在網頁上或移動裝置鎖定螢幕和通知欄看到push訊息通知。以此來實現使用者的多層次需求,使得使用者能夠自己設定所需要的資訊頻道,得到即時訊息,簡單說就是一種定製資訊的實現方式。我們平時瀏覽郵箱時突然彈出訊息提示收到新郵件就屬於web端訊息推送,在手機鎖屏上看到的微信訊息等等都屬於APP訊息推送。 
 

二、web端的訊息推送


這一章節主要對幾種訊息推送的方式進行原理性的講解,並貼出簡單實現的程式碼。

主要介紹其中的五種實現方式:短輪詢、Comet、Flash XMLSocket、Server-sent、WebSocket。

1、短輪詢

指在特定的的時間間隔(如每10秒),由瀏覽器對伺服器發出HTTP request,然後由伺服器返回最新的資料給客戶端的瀏覽器。瀏覽器做處理後進行顯示。無論後端此時是否有新的訊息產生,都會進行響應。字面上看,這種方式是最簡單的。這種方式的優點是,後端編寫非常簡單,邏輯不復雜。但是缺點是請求中大部分中是無用的,浪費了頻寬和伺服器資源。總結來說,簡單粗暴,適用於小型(偷懶)應用。

基於Jquery的ajax前端程式碼:

<body>
    <div id="push"></div>
    <script>
        $(function () {
            setInterval(function () {
                getMsg(function (res) {
                    $("#push").append("<p>" + res +"</p>");
                })
            },10000);
        });

        function getMsg(handler){
            $.ajax({
                url:"/ShortPollingServlet",
                type:"post",
                success:function (res) {
                    handler(res)
                }
            });
        }
</script>
</body>

servlet簡單實現後端程式碼:

public class ShortPollingServlet extends HttpServlet {

    public static int count = 0;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //模擬業務程式碼
        count++;
        response.getWriter().print("msg:" + count );
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request,response);
    }
}

2、Comet



包括了長輪詢和長連線,長輪詢是客戶端向伺服器傳送Ajax請求,伺服器接到請求後hold住連線,直到有新訊息才返回響應資訊並關閉連線,客戶端處理完響應資訊後再向伺服器傳送新的請求;長連線是在頁面中的iframe傳送請求到服務端,服務端hold住請求並不斷將需要返回前端的資料封裝成呼叫javascript函式的形式響應到前端,前端不斷收到響應並處理。Comet的實現原理和短輪詢相比,很明顯少了很多無用請求,減少了頻寬壓力,實現起來比短輪詢複雜一丟丟。比用短輪詢的同學有夢想時,就可以用Comet來實現自己的推送。

長輪詢的優點很明顯,在無訊息的情況下不會頻繁的請求,耗費資小並且實現了服務端主動向前端推送的功能,但是伺服器hold連線會消耗資源,返回資料順序無保證,難於管理維護。WebQQ(好像掛了)就是這樣實現的。

基於Jquery的ajax前端程式碼:

<body>
    <div id="push"></div>
    <script>
        $(function () {
            getMsg();
        });

        function getMsg() {
            $.ajax({
                url:"/LongPollingServlet",
                type:"post",
                success:function (res) {
                    $("#push").append("<p>" + res +"</p>");
                    getMsg();
                }
            });
        }
</script>
</body>

servlet簡單實現後端程式碼:

public class LongPollingServlet extends HttpServlet {

    public static int count = 0;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        count++;
        //睡眠時間模擬業務操作等待時間
        double random = Math.round(Math.random()*10);
        long sleepTime = new Double(random).longValue();
        try{
            Thread.sleep(sleepTime*1000);
            response.getWriter().print("msg:" + count + " after " + sleepTime + "seconds servicing");
        }catch (Exception e){
            e.printStackTrace();
        }

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request,response);
    }
}

長連線優點是訊息即是到達,不發無用請求,管理起來也相對方便。缺點是服務端維護一個長連線會增加開銷。比如Gmail聊天(沒用過)就是這樣實現的。

基於Jquery的ajax前端程式碼:

<head>
  <title>pushPage</title>
  <script type="text/javascript">
    function loadData(msg) {
      var newChild = document.createElement("p");
      newChild.innerHTML = msg;
      document.getElementById("push").appendChild(newChild);
    }
</script>
</head>
<body>
<div id="push"></div>
<iframe src="/LongConnServlet" frameborder="0" name="longConn"></iframe>
</body>

servlet簡單實現後端程式碼:

public class LongConnServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        boolean flag = true;
        int i = 0;
        while (flag){
            try {
                //模擬每1秒查詢一次資料庫,看是否有新的訊息可以推送
                Thread.sleep(1*1000);
            }catch (Exception e){
                e.printStackTrace();
            }

            String pushMsg = "push msg : " + i;
            response.setContentType("text/html;charset=GBK");
            response.getWriter().write("<script type='text/javascript'>parent.loadData('" + pushMsg + "')</script>");
            response.flushBuffer();
            i++;
            if(i==5){
                flag = false;
            }
        }
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request,response);
    }
}

3、Flash XMLSocket

在 HTML 頁面中內嵌入一個使用了 XMLSocket 類的 Flash 程式。JavaScript 通過呼叫此 Flash 程式提供的socket介面與伺服器端的socket進行通訊。JavaScript 在收到伺服器端以 XML 格式傳送的資訊後可以很容易地控制 HTML 頁面的內容顯示。


原理示意圖(引用http://t.cn/Ex6CYHk)

利用Flash XML Socket實現”伺服器推”技術前提:

(1)Flash提供了XMLSocket類,伺服器利用Socket向Flash傳送資料;
(2)JavaScript和Flash的緊密結合JavaScript和Flash可以相互呼叫。

優點是實現了socket通訊,不再利用無狀態的http進行偽推送。但是缺點更明顯:

1.客戶端必須安裝 Flash 播放器;
2.因為 XMLSocket 沒有 HTTP 隧道功能,XMLSocket 類不能自動穿過防火牆;
3.因為是使用套介面,需要設定一個通訊埠,防火牆、代理伺服器也可能對非 HTTP 通道埠進行限制。

這種方案在一些網路聊天室,網路互動遊戲中已得到廣泛使用。不進行程式碼示例。(可參考http://t.cn/aezSch)

4、Server-sent

伺服器推指的是HTML5規範中提供的服務端事件EventSource,瀏覽器在實現了該規範的前提下建立一個EventSource連線後,便可收到服務端的傳送的訊息,實現一個單向通訊。客戶端進行監聽,並對響應的資訊處理顯示。該種方式已經實現了服務端主動推送至前端的功能。優點是在單項傳輸資料的場景中完全滿足需求,開發人員擴充套件起來基本不需要改後端程式碼,直接用現有框架和技術就可以整合。

基於HTML5的Server-sent事件:

<head>
    <title>Title</title>
    <script>
        var source = new EventSource("/ServerSentServlet");//建立一個新的 EventSource物件,
        source.onmessage = function (evt) {//每接收到一次更新,就會發生 onmessage事件
            var newChild = document.createElement("p");
            newChild.innerHTML = evt.data;
            document.getElementById("push").appendChild(newChild);
        }
</script>
</head>
<body>
    <div id="push"></div>
</body>

servlet簡單實現後端程式碼:

public class ServerSentServlet extends HttpServlet {

    public static int count = 0;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        count++;
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Type", "text/event-stream");//設定伺服器端事件流
        response.setHeader("Cache-Control","no-cache");//規定不對頁面進行快取
        response.setHeader("Pragma","no-cache");
        response.setDateHeader("Expires",0);
        PrintWriter pw = response.getWriter();
        pw.println("retry: 5000"); //設定請求間隔時間
        pw.println("data: " + "msg:" + count +"\n\n");
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request,response);
    }
}

5、WebSocket

WebSocket是HTML5下一種新的協議,是基於TCP的應用層協議,只需要一次連線,便可以實現全雙工通訊,客戶端和服務端可以相互主動傳送訊息。客戶端進行監聽,並對響應的訊息處理顯示。這個技術相信基本都聽說過,就算沒寫過程式碼,也大概知道幹嘛的。通過名字就能知道,這是一個Socket連線,一個能在瀏覽器上用的Socket連線。是HTML5標準中的一個內容,瀏覽器通過javascript指令碼手動建立一個TCP連線與服務端進行通訊。優點是雙向通訊,都可以主動傳送訊息,既可以滿足“問”+“答”的響應機制,也可以實現主動推送的功能。缺點就是編碼相對來說會多點,服務端處理更復雜(我覺得當一條有情懷的鹹魚就應該用這個!)。

前端程式碼:

<body>
    <div id="push"></div>
</body>
<script>
    $(function () {
        var webSocket = new WebSocket("ws://localhost:8080/ws");
        webSocket.onmessage = function (ev) {
            $("#push").append("<p>" + ev.data +"</p>");
        }
    })
</script>

基於註解簡單實現後端程式碼:

@ServerEndpoint("/ws")
public class MyWebSocket {

    private Session session;

    public MyWebSocket() {

    }

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        System.out.println("someone connect");
        int count = 1;
        while (count<=5){
            //睡眠時間模擬業務操作等待時間
            double random = Math.round(Math.random()*10);
            long sleepTime = new Double(random).longValue();
            try {
                Thread.sleep(sleepTime*1000);
                session.getBasicRemote().sendText("msg:" + count +" from server after" + sleepTime + " seconds");
            }catch (Exception e){
                e.printStackTrace();
            }
            count++;
        }

    }

    @OnError
    public void onError(Throwable t){
        System.out.println("something error");
    }
}

以上是對五種推送方式原理的簡單講解和程式碼的實現。
 

三、實現個性化的推送 

上面說了很多原理,也給出了簡單的程式碼實現,但是在實際生產過程中,肯定不能用上面的程式碼,針對自己系統的應用場景選擇合適的推送方案才是合理的,因此最後簡單說一下實現個性化推送的兩種方式。第一種很簡單,直接使用第三方實現的推送,無需複雜的開發運維,直接可以使用。第二種就是自己封裝,可以選擇如今較為火熱的WebSocket來實現系統的推送。



1、第三方

在這裡推薦一個第三方推送平臺,GoEasy。

推薦理由是GoEasy的理念符合我們的選擇(可參考http://t.cn/Ex6jg3q):

(1)更簡單的方式將訊息從伺服器端推送至客戶端
(2)更簡單的方式將訊息從各種客戶端推送至客戶端

GoEasy具體的使用方式這裡不再贅述,詳見官網。對於後端後端開發者,可直接使用Rest方式呼叫推送,對於前端或web開發者,可以從web客戶端用javascript指令碼進行呼叫推送。

2、封裝自己的推送服務

如果是一個老系統進行擴充套件,那麼更推薦使用Server-sent,服務端改動量不會很大。如果是新系統,更推薦websocket,實現的功能功能更全面。

我們以websocket為例,不再貼出具體的程式碼實現。

我們如果需要使用websocket技術實現自己的推送服務,需要注意哪些點,或者說需要踩哪些坑呢,本文最後列出幾點供大家參考:

長連線的心跳處理;
從WebSocket中獲取HttpSession進行使用者相關操作;
服務端調優實現高併發量client同時線上;
服務端維持多使用者的狀態;
群發訊息;
等等等….

最後貼出上述程式碼的git庫地址,所有demo均可執行。環境為jdk1.8+tomcat8。http://t.cn/Ex6TRVZ


關於作者:徐曉明,普元開發工程師,畢業於遼寧科技大學,專注於使用移動開發平臺開發app,負責中國郵政集團移動平臺專案郵我行app開發和後臺開發運維工作。

關於EAWorld:微服務,DevOps,資料治理,移動架構原創技術分享。長