WebSocket--消息推送框架


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

最近在工作有這么一個需求,需要在門戶首頁獲取服務器“推送”過來的消息,一般首先想到的是用ajax。本着好奇的精神,到網上查了一下,相關方面的知識,收獲還真不小,記錄下分享給大家。

一般要實現網頁的消息提醒,不外乎兩種情況:

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

"服務器推"推技術簡介

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

示例環境

eclipse+tomcat

struts1.3+jsp+jquery

本代碼中所有示例都是在eclipse+tomcat下運行通過的,瀏覽器使用ie9+chrome進行測試,運用了struts+jquery框架,來輔助實現。如果你熟悉strust的配置,可以跳過下面,直接看polling。

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>

struts的配置

新建/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>

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從后台獲取的信息了。

 

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

 

采用這種方式要獲取即使的消息推送,而且應用可能需要集群,小林想估計要弄一個隊列表,然后模塊有需要向某個人推送一條消息的話,就需要插入一條信息到數據庫,然后客戶端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,瀏覽器上就一個字一個字的顯示我們從后端取得的信息了。

 

這里可以看到這里請求數只有一個,但是請求時間卻很長,在這很長的時間里,服務器只要一有消息就可以主動的推送消息過來。不過缺點就是。瀏覽器不兼容(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
    }
  }
}

用這種方式可以解決跨瀏覽器問題。

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住這個請求訪問,等待服務器有消息了,就推送給用戶)

效果

這里每次請求剛好5秒鍾左右,但是實際運用中可能不止這么久。

WebSocket:未來方向

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

而比較新的技術去做輪詢的效果是comet,使用了AJAX。但這種技術雖然可達到雙向通信,但依然需要發出請求,而且在Comet中,普遍采用了長鏈接,這也會大量消耗服務器帶寬和資源。

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

總結

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

最后附上源代碼下載

------------------ 你分享 我學習 ------------------

聲明:本文轉自 http://www.cnblogs.com/divenswu/p/3520043.html 感謝您與我分享!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM