WEB消息推送-原理篇

這篇文章主要講述B/S架構中服務器「推送」消息給瀏覽器。內容涉及ajax論詢(polling),comet(streaming,long polling)。後面會附上源代碼。javascript

最近在工做有這麼一個需求,須要在門戶首頁獲取服務器「推送」過來的消息,通常首先想到的是用ajax。本着好奇的精神,到網上查了一下,相關方面的知識,收穫還真不小,記錄下分享給你們。html

通常要實現網頁的消息提醒,不外乎兩種狀況:前端

  1. 客戶端主動定時的去拿服務器端,有消息就提醒(polling);
  2. 服務器主動"推送"消息給客戶端,這裏說的主動推送,並非真的,而是客戶端申請了須要顯示消息提醒的信息,而服務端暫時沒給客戶端答覆,把請求hold住了。。(comet)。

"服務器推"推技術簡介

基於HTTP長鏈接的"服務器推"技術
  • 基於html file流(streaming,瀏覽器不兼容)
  • iframe streaming(streming的擴展,瀏覽器兼容)
  • 基於ajax長輪詢(long-polling,瀏覽器兼容)
基於客戶端套接口的"服務器推"技術
  • Flash XML Socket
  • Java Applet 套接口這兩種都不是咱們這篇文章要說的主題,並且小林也沒往這方面研究,由於,偶的應用是跑在weblogic的j2ee程序。

示例環境

eclipse+tomcatjava

struts1.3+jsp+jqueryjquery

本代碼中全部示例都是在eclipse+tomcat下運行經過的,瀏覽器使用ie9+chrome進行測試,運用了struts+jquery框架,來輔助實現。若是你熟悉strust的配置,能夠跳過下面,直接看polling。web

web.xml的配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
  <display-name>test</display-name>
  <servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
    <init-param>
      <param-name>config</param-name>
      <param-value>/WEB-INF/struts-config/struts-config-push.xml</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>
</web-app>ajax

struts的配置chrome

新建/WEB-INF/struts-config/struts-config-push.xml加入以下內容數據庫

  <?xml version="1.0" encoding="UTF-8"?>
   <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
   "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
 <struts-config>
    <action-mappings>
      <action path="/push/comet" parameter="method" type="com.linjunlong.test.push.action.CometAction" ></action>
    </action-mappings>
 </struts-config>apache

polling

 在介紹comet以前,先介紹一些傳統的ajax輪詢(polling),輪詢最簡單也最容易實現,每隔一段時間向服務器發送查詢,有更新再觸發相關事件。對於前端,使用js的setInterval以AJAX或者JSONP的方式按期向服務器發送request。他可讓用戶不須要刷新瀏覽器,也能夠即時的獲取服務器更新。

前端jsp代碼

咱們新建一個在/WebContent/push下新建一個polling.jsp頁面,把jquery腳本複製到/WebContent/static/js/jquery-1.8.0.min.js下。下面是polling.jsp代碼,腳本部分咱們設置每3秒進行一次輪詢。

<%@ page language="java" contentType="text/html; charset=GBK"
pageEncoding="GBK"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=GBK">
  <script type="text/javascript" src="../../static/js/jquery-1.8.0.min.js"></script>
  <title >polling test</title>
</head>
<body>
  <div id="message"></div>
</body>
<script type="text/javascript">
  var polling = function(){
    $.post('../comet.do?method=polling', function(data, textStatus){
      $("#message").append(data+"<br>");
    });
  };
  interval = setInterval(polling, 3000);
</script>
</html>

後端action代碼

咱們在com.linjunlong.test.push.action.CometAction中添加polling方法

public ActionForward polling(ActionMapping mapping,ActionForm form,HttpServletRequest request,HttpServletResponse response) throws Exception{
  System.out.println("-----CometAction.polling.start");
  PrintWriter writer = response.getWriter();
  //TODO 編寫一些CRUD的代碼進行數據庫查詢,看看用戶需不須要彈出消息提醒
  writer.print("your hava a new message");
  writer.flush();
  writer.close();
  System.out.println("-----CometAction.polling.end");
  return null;
}

效果展現

當咱們啓動tomcat,在瀏覽器中輸入http://localhost:8080/test/push/comet/polling.jsp,瀏覽器上就不斷的顯示咱們的ajax從後臺獲取的信息了。

image

下面是chrome developer tool中url請求的信息,能夠看到,ajax輪詢就是不斷的在後端進行訪問。。若是服務器反映慢一點。。前面一個請求還沒相應完,後面一個請求已經發送。會怎麼樣呢?

image

採用這種方式要獲取即便的消息推送,並且應用可能須要集羣,小林想估計要弄一個隊列表,而後模塊有須要向某我的推送一條消息的話,就須要插入一條信息到數據庫,而後客戶端ajax訪問後臺,後臺進行數據庫查詢,看當前用戶在隊列表裏是否有記錄,有的話,就取出來,返回給ajax,而後刪除數據庫中的記錄。。。(這些都是小林想固然的啦,偶還沒開始作。。。)

經過chrome的開發工具能夠看到,瀏覽器不斷的向後臺進行請求(若是用戶多的話,這得要多大的併發啊,估計壓力測試,服務器直接掛了。)。並且每次請求服務器端不必定有數據返回,用在聊天系統還好說,小林只是想在門戶首頁弄個提醒而已啊,您有新的短消息,您有新的郵件- -。。。這種也許開一天瀏覽器都不必定有一條消息的- -。

comet

基於Comet的技術主要分爲流(streaming)方式和長輪詢(long-polling)方式。 首先看Comet這個單詞,不少地方都會說到,它是「彗星」的意思,顧名思義,彗星有個長長的尾巴,以此來講明客戶端發起的請求是長連的。即用戶發起請求後就掛起,等待服務器返回數據,在此期間不會斷開鏈接。流方式和長輪詢方式的區別就是:對於流方式,客戶端發起鏈接就不會斷開鏈接,而是由服務器端進行控制。當服務器端有更新時,刷新數據,客戶端進行更新;而對於長輪詢,當服務器端有更新返回,客戶端先斷開鏈接,進行處理,而後從新發起鏈接。 會有同窗問,爲何須要流(streaming)和長輪詢(long-polling)兩種方式呢?是由於:對於流方式,有諸多限制。若是使用AJAX方式,須要判斷XMLHttpRequest 的 readystate,即readystate==3時(數據仍在傳輸),客戶端能夠讀取數據,而不用關閉鏈接。問題也在這裏,IE 在 readystate 爲 3 時,不能讀取服務器返回的數據,因此目前 IE 不支持基於 Streaming AJAX,而長輪詢因爲是普通的AJAX請求,因此沒有瀏覽器兼容問題。另外,因爲使用streaming方式,控制權在服務器端,而且在長鏈接期間,並無客戶端到服務器端的數據,因此不能根據客戶端的數據進行即時的適應(好比檢查cookie等等),而對於long polling方式,在每次斷開鏈接以後能夠進行判斷。因此綜合來講,long polling是如今比較主流的作法(如facebook,Plurk)。 接下來,咱們就來對流(streaming)和長輪詢(long-polling)兩種方式進行演示。

streaming

前端jsp代碼

/test/WebContent/push/comet/streaming.jsp,腳本中有個遊標pos由於服務器端是一段一段的發送消息過來的。

<%@ page language="java" contentType="text/html; charset=GBK"
pageEncoding="GBK"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=GBK">
  <script type="text/javascript" src="../../static/js/jquery-1.8.0.min.js"></script>
  <title >streaming test</title>
</head>
<body>
  <div id="message"></div>
</body>
<script type="text/javascript">
  try {
    var request = new XMLHttpRequest();
  } catch (e) {
    alert("Browser doesn't support window.XMLHttpRequest");
  }

  var pos = 0;
  request.onreadystatechange = function () {
    if (request.readyState === 3) {
      var data = request.responseText;
      $("#message").append(data.substring(pos));
      pos = data.length;
    }
  };
  request.open("POST", "../comet.do?method=streaming", true);
  request.send(null);
</script>
</html>

後端action代碼

咱們在com.linjunlong.test.push.action.CometAction中添加polling方法

public ActionForward streaming(ActionMapping mapping,ActionForm form,HttpServletRequest request,HttpServletResponse response) throws Exception{
  System.out.println("-----CometAction.streaming.start");
  StreamingThread st = new StreamingThread(response);
  st.run();
  System.out.println("-----CometAction.streaming.end");
  return null;
}

下面是StreamingThread的代碼。

public class StreamingThread extends Thread {
  private HttpServletResponse response = null;

  public StreamingThread(HttpServletResponse response){
    this.response = response;
  }

  @Override
  public void run() {
    try{
      String message = "your hava a new message";
      PrintWriter writer = response.getWriter();
      for(int i = 0 ,max = message.length(); i < max ; i++) {
        writer.print(message.substring(i,i+1));
        writer.flush();
        sleep(1000);
      }
      writer.close();
    }catch (Exception e) {}
  }
}

StreamingThread邏輯上是把咱們想要輸出的內容一個一個輸出,每輸出一個字,而後就休眠1秒鐘,其實這個類想表達的意思是,服務器端接收到客戶端想要獲取信息的請求,能夠先不作任何操做,只要永遠不調用writer.close(); 服務器端就隨時能夠給客戶端發送消息。這裏的精髓是writer.flush(); sleep(1000);

效果展現

在瀏覽器中輸入http://localhost:8080/test/push/comet/streaming.jsp,瀏覽器上就一個字一個字的顯示咱們從後端取得的信息了。

image

image

這裏能夠看到這裏請求數只有一個,可是請求時間卻很長,在這很長的時間裏,服務器只要一有消息就能夠主動的推送消息過來。不過缺點就是。瀏覽器不兼容(ie下沒法實現),爲了達到瀏覽器兼容,因而就有了下面的即便iframe-streaming

iframe-streaming

這也是早先的經常使用作法。首先咱們在頁面裏放置一個iframe,它的src設置爲一個長鏈接的請求地址。Server端的代碼基本一致,只是輸出的格式改成HTML,用來輸出一行行的Inline Javascript。因爲輸出就獲得執行,所以就少了存儲遊標(pos)的過程。

前端jsp代碼

/test/WebContent/push/comet/iframe.jsp中編寫下面代碼

 

<%@ page language="java" contentType="text/html; charset=GBK"
pageEncoding="GBK"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=GBK">
  <script type="text/javascript" src="../../static/js/jquery-1.8.0.min.js"></script>
  <title >streaming test</title>
</head>
<body>
  <div id="message"></div>
  <iframe id="iframe" src="about:blank" style="display: none;" ></iframe>
</body>
<script type="text/javascript">
  var add_content = function(str){
    $("#message").append(str);
  };
  $(document).ready(function(){
    $("#iframe")[0].src="../comet.do?method=iframe";
  });
</script>
</html>

能夠看到咱們在這jsp中定義了一個隱藏的iframe,他的地址是空的,由於在ie下,若是你寫入一個地址,那麼瀏覽器就會一直打轉,給人一種頁面還未加載萬的假象,因而這裏小林採用延遲加載的方式去等頁面加載完再初始化請求地址

後端action代碼

咱們在com.linjunlong.test.push.action.CometAction中添加iframe方法

public ActionForward iframe(ActionMapping mapping,ActionForm form,HttpServletRequest request,HttpServletResponse response) throws Exception{
  System.out.println("-----CometAction.iframe.start");
  IframeThread st = new IframeThread(response);
  st.run();
  System.out.println("-----CometAction.iframe.end");
  return null;
}

下面是IframeThread代碼,與streaming邏輯上同樣,只是輸出的時候採用返回html腳本片斷的方式,調用父頁面的add_content 函數進行進行消息的添加,界面上的顯示效果和streaming方式無異。

public class IframeThread extends Thread {
  private HttpServletResponse response = null;

  public IframeThread(HttpServletResponse response){
    this.response = response;
  }

  @Override
  public void run() {
    try{
      String message = "your hava a new message";
      PrintWriter writer = response.getWriter();
      for(int i = 0 ,max = message.length(); i < max ; i++) {
        writer.print("<script>parent.add_content('"+message.substring(i,i+1)+"');</script>");
        writer.flush();
        sleep(1000);
      }
      writer.close();
    }catch (Exception e) {
    // TODO: handle exception
    }
  }
}

image

用這種方式能夠解決跨瀏覽器問題。

long-polling

長輪詢是如今最爲經常使用的方式,和流方式的區別就是服務器端在接到請求後掛起,有更新時返回鏈接即斷掉,而後客戶端再發起新的鏈接。不少大型網站都用這種技術。

前端jsp代碼

/test/WebContent/push/comet/long.jsp中編寫下面代碼

<%@ page language="java" contentType="text/html; charset=GBK"
pageEncoding="GBK"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=GBK">
  <script type="text/javascript" src="../../static/js/jquery-1.8.0.min.js"></script>
  <title >polling test</title>
</head>
<body>
  <div id="message"></div>
</body>
<script type="text/javascript">
  var updater = {
    poll: function(){
      $.ajax({

      url: "../comet.do?method=longPolling",
      type: "POST",
      dataType: "text",
      success: updater.onSuccess,
      error: updater.onError

      });
    },
    onSuccess: function(data, dataStatus){
      try{
        $("#message").append(data);
      }
      catch(e){
        updater.onError();
        return;
      }
      interval = window.setTimeout(updater.poll, 0);
    },
    onError: function(){
      console.log("Poll error;");
    }
  };
  updater.poll();
</script>
</html>

 

後臺action代碼

public ActionForward longPolling(ActionMapping mapping,ActionForm form,HttpServletRequest request,HttpServletResponse response) throws Exception{
  System.out.println("-----CometAction.longPolling.start");
  PrintWriter writer = response.getWriter();
  Thread longThread = new Thread() {
    public void run() {
      try {
        //這裏模擬全局事件監聽,若是其餘模塊有須要發送消息就發送一個事件,而後休眠中止,發送消息。
        sleep(5000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    };
  };
  longThread.run();
  writer.print("your hava a new message");
  writer.flush();
  writer.close();
  System.out.println("-----CometAction.longPolling.end");
  return null;
 }

這裏咱們代碼中,模擬型的休息5秒鐘(其實要表達的意思是,這裏讓服務器hold住這個請求訪問,等待服務器有消息了,就推送給用戶)

效果

image

這裏每次請求恰好5秒鐘左右,可是實際運用中可能不止這麼久。

WebSocket:將來方向

如今,不少網站爲了實現即時通信,所用的技術都是輪詢。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對伺服器發出HTTP request,而後由伺服器返回最新的數據給客戶端的瀏覽器。這種傳統的模式帶來很明顯的缺點,即瀏覽器須要不斷的向伺服器發出請求,然而HTTP request 的header是很是長的,裏面包含的數據可能只是一個很小的值,這樣會佔用不少的帶寬和服務器資源。

而比較新的技術去作輪詢的效果是comet,使用了AJAX。但這種技術雖然可達到雙向通訊,但依然須要發出請求,並且在Comet中,廣泛採用了長連接,這也會大量消耗服務器帶寬和資源。

面對這種情況,HTML5定義了WebSocket協議,能更好的節省服務器資源和帶寬並達到實時通信。

總結

文章只是簡單的演示了實現消息推送的方式,並無比較系統的解決如何進行消息推送。實際過程當中若是咱們想要用於實戰,可能要考慮客戶端和服務器端直接的交流,服務器的壓力,全局消息隊列等等等。。。。。。。

最後附上源代碼下載

相關文章
相關標籤/搜索