訊息推送(一)Comet介紹
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
轉載請註明:小林部落格 » 訊息推送(一)Comet介紹
這篇文章主要講述B/S架構中伺服器“推送”訊息給瀏覽器。內容涉及ajax論詢(polling),comet(streaming,long polling)。後面會附上原始碼。
小林最近在工作有這麼一個需求,需要在門戶首頁獲取伺服器“推送”過來的訊息,像小林這種菜鳥,一般首先想到的是用ajax。本著好奇的精神,到網上查了一下,相關方面的知識,收穫還真不小,於是就記錄下分享給大家。
一般要實現網頁的訊息提醒,不外乎兩種情況(小林想當然的):
- 客戶端主動定時的去拿伺服器端,有訊息就提醒(polling);
- 伺服器主動"推送"訊息給客戶端,這裡說的主動推送,並不是真的,而是客戶端申請了需要顯示訊息提醒的資訊,而服務端暫時沒給客戶端答覆,把請求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的配置,可以跳過下面,直接看
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>
新建/WEB-INF/struts-config/struts-config-push.xml加入如下內容struts的配置
<?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方法
[java]
view plain
copy
print
?
- <code class="language-javascript"> 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;
- }</code>
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) { // TODO: handle exception } } }
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協議,能更好的節省伺服器資源和頻寬並達到實時通訊。關於WebSocket想了解更多,請看這裡
總結
小林這篇文章只是簡單的演示了實現訊息推送的方式,並沒有比較系統的解決如何進行訊息推送。實際過程中如果我們想要用於實戰,可能要考慮客戶端和伺服器端直接的交流,伺服器的壓力,全域性訊息佇列等等等。。。。。。最後希望這篇文章還能有下一篇,在這裡先給自己挖個坑。。