2008 年的夏天,偶然在網上閒逛的時候發現了 Comet 技術,人云亦云間,姑且認爲它是由 Dojo 的 Alex Russell 在 2006 年提出。在閱讀了大量的資料後,萌發出寫篇 blog 來講明什麼是 Comet 的想法。哪知道這個想法到了半年後的今天才提筆,除了繁忙的工做拖延外,還有 Comet 自己帶來的困惑。javascript
Comet 能帶來生產力的提高是有目共睹的。如今假設有 1000 個用戶在使用某軟件,輪詢 (polling) 和 Comet 的設定都是 1s 、 10s 、 100s 的潛伏期,那麼在相同的潛伏期內, Comet 所須要的帶寬更小,以下圖:html
不只僅是在帶寬上的優點,每一個用戶所真正感覺到的響應時間(潛伏期)更短,給人的感受也就更加的實時,以下圖:java
再引用一篇 IBMDW 上的譯文《使用 Jetty 和 Direct Web Remoting 編寫可擴展的 Comet 應用程序》,其中說到:吸引人們使用 Comet 策略的其中一個優勢是其顯而易見的高效性。客戶機不會像使用輪詢方法那樣生成煩人的通訊量,而且事件發生後可當即發佈給客戶機。web
上面一遍一遍的說到 Comet 技術的優點,那麼咱們能夠替換現有的技術結構了?不幸的是,近半年的擦邊球式的關注使我對 Comet 的理解愈加的糊塗,甚至有人說 Comet 這個名詞已被濫用。去年的一篇博文,《 The definition of Comet? 》使 Comet 更加撲朔迷離,甚至在維基百科上你們也對準確的 Comet 定義產生爭論。仍是等牛人們爭論清楚再修改維基百科吧,在這裏我想仍是引用維基百科對 Comet 的定義:服務器推模式 (HTTP server push 、 streaming) 以及長輪詢 (long polling) ,這兩種模式都是 Comet 的實現。ajax
除了對 Comet 的準肯定義尚缺少有效的定論外, Comet 還存在很多技術難題,隨着 Tomcat 6 、 Jetty 6 的發佈,他們基於 NIO 各自實現了異步 Servlet 機制。有興趣的看官能夠分別實現這兩個容器的 Comet ,至少我還沒玩轉。apache
在編寫服務器端的代碼上面,我很困惑, http://tomcat.apache.org/tomcat-6.0-doc/aio.html 這裏演示瞭如何在 Tomcat 6 中實現異步 Servlet ;咱們再把目光換到 Jetty 6 上,仍是前面提到的那篇 IBMDW 譯文,若是你和我同樣無聊,能夠下載那邊文章的 sample 代碼。我驚奇的發現每一個廠商對異步 Servlet 的封裝是不一樣的,一個傻傻的問題:個人 Comet 服務器端的代碼可移植麼?至今我還在問這個問題!好吧,業界有規範麼?有固然有,不過看起來有些爭論會發生——那就是 Servlet 3.0 規範 (JSR-315) , Servlet 3.0 正在公開預覽,它明確的支持了異步 Servlet ,《 Servlet 3.0 公開預覽版引起爭論》,又讓我高興不起來了:「來自 RedHat 的 Bill Burke 寫的一篇博文,其中他批評了 Jetty 6 中的異步 servlet 實現 ......Greg Wilkins 宣佈他致力於 Servlet 3.0 異步 servlet 的一個實現 ...... 雖然還須要更多測試,可是這個代碼已經實現了基本的異步行爲,不須要很複雜的從新分發請求或者前遞方法。我相信這表明了 3.0 的合理折中方案。在咱們從 3.0 的簡單子集裏得到經驗以後,若是須要更多的特性,能夠添加到 3.1 中 ........」 。牛人們還在作最佳範例,口水仗也還要繼續打,看來要嚐到 Comet 的甜頭是很困難的。 STOP !我已經不想再分析如何寫客戶端的代碼了,什麼 dojo 、 extJs 、 DWR 、 ZK....... 都有本身的實現。我認爲這一切都要等 Servelt 3.0 正式發佈之後,如何編寫客戶端代碼才能明朗點。瀏覽器
如今拋開繞來繞去的爭執吧,既然 Ajax+Servlet 實現 Comet 很困難,何不換個思惟呢。我這裏卻是有個小小的 sample ,說明如何在 Adobe BlazeDS 中實現長輪詢模式。關於 BlazeDS ,能夠在這裏找到些信息。爲了說明什麼是長輪詢,首先來看看什麼是輪詢,既在必定間隔期內由 web 客戶端發起請求到服務器端取回數據,以下圖所示:tomcat
![](http://static.javashuo.com/static/loading.gif)
至於輪詢的缺點,在前面的論述中已有覆蓋,至於優勢你們能夠 google 一把,我以爲最大的優勢就是技術上很好實現,下面是個 Ajax 輪詢的例子,這是一個簡單的聊天室,首先是 chat.html 代碼,想必這些代碼網上一抓就一大把,支持至少 IE6 、 IE7 、 FF3 瀏覽器,讓人煩心的是亂碼問題,在傳遞到 Servlet 以前要 encodeURI 一下 : 服務器
<! DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd" >
<!--
chat page
author rosen jiang
since 2008/07/29
-->
< html >
< head >
< meta http-equiv ="content-type" content ="text/html; charset=utf-8" >
< script type ="text/javascript" >
// servlets url
var url = " http://127.0.0.1:8080/ajaxTest/Ajax " ;
// bs version
var version = navigator.appName + " " + navigator.appVersion;
// if is IE
var isIE = false ;
if (version.indexOf( " MSIE 6 " ) > 0 || version.indexOf( " MSIE 7 " ) > 0 ){
isIE = true ;
}
// Httprequest object
var Httprequest = function () {}
// creatHttprequest function of Httprequest
Httprequest.prototype.creatHttprequest = function (){
var request = false ;
// init XMLHTTP or XMLHttpRequest
if (isIE) {
try {
request = new ActiveXObject( " Msxml2.XMLHTTP " );
} catch (e) {
try {
request = new ActiveXObject( " Microsoft.XMLHTTP " );
} catch (e) {}
}
} else { // Mozilla bs etc.
request = new XMLHttpRequest();
}
if ( ! request) {
return false ;
}
return request;
}
// sendMsg function of Httprequest
Httprequest.prototype.sendMsg = function (msg){
var http_request = this .creatHttprequest();
var reslult = "" ;
var methed = false ;
if (http_request) {
if (isIE) {
http_request.onreadystatechange =
function (){ // callBack function
if (http_request.readyState == 4 ) {
if (http_request.status == 200 ) {
reslult = http_request.responseText;
} else {
alert( " 您所請求的頁面有異常。 " );
}
}
};
} else {
http_request.onload =
function (){ // callBack function of Mozilla bs etc.
if (http_request.readyState == 4 ) {
if (http_request.status == 200 ) {
reslult = http_request.responseText;
} else {
alert( " 您所請求的頁面有異常。 " );
}
}
};
}
// send msg
if (msg != null && msg != "" ){
request_url = url + " ? " + Math.random() + " &msg= " + msg;
// encodeing utf-8 Character
request_url = encodeURI(request_url);
http_request.open( " GET " , request_url, false );
} else {
http_request.open( " GET " , url + " ? " + Math.random(), false );
}
http_request.setRequestHeader( " Content-type " , " charset=utf-8; " );
http_request.send( null );
}
return reslult;
}
</ script >
</ head >
< body >
< div >
< input type ="text" id ="sendMsg" ></ input >
< input type ="button" value ="發送消息" onclick ="send()" />
< br />< br />
< div style ="width:470px;overflow:auto;height:413px;border-style:solid;border-width:1px;font-size:12pt;" >
< div id ="msg_content" ></ div >
< div id ="msg_end" style ="height:0px; overflow:hidden" > </ div >
</ div >
</ div >
</ body >
< script type ="text/javascript" >
var data_comp = "" ;
// send button click
function send(){
var sendMsg = document.getElementById( " sendMsg " );
var hq = new Httprequest();
hq.sendMsg(sendMsg.value);
sendMsg.value = "" ;
}
// processing wnen message recevied
function writeData(){
var msg_content = document.getElementById( " msg_content " );
var msg_end = document.getElementById( " msg_end " );
var hq = new Httprequest();
var value = hq.sendMsg();
if (data_comp != value){
data_comp = value;
msg_content.innerHTML = value;
msg_end.scrollIntoView();
}
setTimeout( " writeData() " , 1000 );
}
// init load writeData
onload = writeData;
</ script >
</ html >
接下來是 Servlet ,若是你是用的 Tomcat ,在這裏注意下編碼問題,不然又是亂碼,另外我使用 LinkedList 實現了一個隊列,該隊列的最大長度是 30 ,也就是最多能保存 30 條聊天信息,舊的將被丟棄,另外新的客戶端進來後能讀取到最近的信息: app
package org.rosenjiang.ajax;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
*
* @author rosen jiang
* @since 2009/02/06
*
*/
public class Ajax extends HttpServlet {
private static final long serialVersionUID = 1L ;
// the length of queue
private static final int QUEUE_LENGTH = 30 ;
// queue body
private static LinkedList < String > queue = new LinkedList < String > ();
/**
* response chat content
*
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// parse msg content
String msg = request.getParameter( " msg " );
SimpleDateFormat sdf = new SimpleDateFormat( " yyyy-MM-dd HH:mm:ss " );
// push to the queue
if (msg != null && ! msg.equals( "" )) {
byte [] b = msg.getBytes( " ISO_8859_1 " );
msg = sdf.format( new Date()) + " " + new String(b, " utf-8 " ) + " <br> " ;
if (queue.size() == QUEUE_LENGTH){
queue.removeFirst();
}
queue.addLast(msg);
}
// response client
response.setContentType( " text/html " );
response.setCharacterEncoding( " utf-8 " );
PrintWriter out = response.getWriter();
msg = "" ;
// loop queue
for ( int i = 0 ; i < queue.size(); i ++ ){
msg = queue.get(i);
out.println(msg == null ? "" : msg);
}
out.flush();
out.close();
}
/**
* The doPost method of the servlet.
*
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this .doGet(request, response);
}
}
打開瀏覽器,實驗下效果,將就用吧,稍微有些延遲。仍是看看長輪詢吧,長輪詢有三個顯著的特徵:
1. 服務器端會阻塞請求直到有數據傳遞或超時才返回。
2. 客戶端響應處理函數會在處理完服務器返回的信息後,再次發出請求,從新創建鏈接。
3. 當客戶端處理接收的數據、從新創建鏈接時,服務器端可能有新的數據到達;這些信息會被服務器端保存直到客戶端從新創建鏈接,客戶端會一次把當前服務器端全部的信息取回。
下圖很好的說明了以上特徵:
既然關注的是 BlazeDS 如何實現長輪詢,那麼有必要稍微瞭解下。 BlazeDS 包含了兩個重要的服務,進行遠端方法調用的 RPC service 和傳遞異步消息的 Messaging Service ,咱們即將探討的長輪詢屬於 Messaging Service 。 Messaging Service 使用 producer consumer 模式來分別定義消息的發送者 (producer) 和消費者 (consumer) ,具體到 Flex 代碼,有 Producer 和 Consumer 兩個組件對應。在廣闊的互聯網上有不少 BlazeDS 入門的中文教材,我就再也不廢話了。假設你已經裝好 BlazeDS ,打開 WEB-INF/flex/services-config.xml 文件,在 channels 節點內加一個 channel 聲明長輪詢頻道,關於 channel 和 endpoint 請參閱 About channels and endpoints 章節:
< channel-definition id ="long-polling-amf" class ="mx.messaging.channels.AMFChannel" >
< endpoint url ="http://{server.name}:{server.port}/{context.root}/messagebroker/longamfpolling" class ="flex.messaging.endpoints.AMFEndpoint" />
< properties >
< polling-enabled > true </ polling-enabled >
< wait-interval-millis > 60000 </ wait-interval-millis >
< polling-interval-millis > 0 </ polling-interval-millis >
< max-waiting-poll-requests > 150 </ max-waiting-poll-requests >
</ properties >
</ channel-definition >
如何實現長輪詢的玄機就在上面的 properties 節點內, polling-enabled = true ,打開輪詢模式; wait-interval-millis = 6000 服務器端的潛伏期,也就是服務器會保持與客戶端的鏈接,直到超時或有新消息返回(恩,看來這就是長輪詢了); polling-interval-millis = 0 表示客戶端請求服務器端的間隔期, 0 表示沒有任何的延遲; max-waiting-poll-requests = 150 表示服務器能承受的最大長鏈接用戶數,超過這個限制,新的客戶端就會轉變爲普通的輪詢方式(至於這個數值最大能有多大,這和你的 web 服務器設置有關了,而 web 服務器的最大鏈接數就和操做系統有關了,這方面的話題不在本文內探討)。
其實這樣設置以後,長輪詢的代碼已經實現了一半了。恩,不錯!看起來比異步 Servlet 實現起來簡單多了。不過要實現和以前 Ajax 輪詢同樣的效果,還得實現本身的 ServiceAdapter ,這就是 Adapter 的用處:
package org.rosenjiang.flex;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import flex.messaging.io.amf.ASObject;
import flex.messaging.messages.Message;
import flex.messaging.services.MessageService;
import flex.messaging.services.ServiceAdapter;
/**
*
* @author rosen jiang
* @since 2009/02/06
*
*/
public class MyMessageAdapter extends ServiceAdapter {
// the length of queue
private static final int QUEUE_LENGTH = 30 ;
// queue body
private static LinkedList < String > queue = new LinkedList < String > ();
/**
* invoke method
*
* @param message Message
* @return Object
*/
public Object invoke(Message message) {
SimpleDateFormat sdf = new SimpleDateFormat( " yyyy-MM-dd HH:mm:ss " );
MessageService msgService = (MessageService) getDestination()
.getService();
// message Object
ASObject ao = (ASObject) message.getBody();
// chat message
String msg = (String) ao.get( " chatMessage " );
if (msg != null && ! msg.equals( "" )) {
msg = sdf.format( new Date()) + " " + msg + " \r " ;
if (queue.size() == QUEUE_LENGTH){
queue.removeFirst();
}
queue.addLast(msg);
}
msg = "" ;
// loop queue
for ( int i = 0 ; i < queue.size(); i ++ ){
String chatData = queue.get(i);
if (chatData != null ) {
msg += chatData;
}
}
ao.put( " chatMessage " , msg);
message.setBody(ao);
msgService.pushMessageToClients(message, false );
return null ;
}
}
接下來註冊該 Adapter ,打開 WEB-INF/flex/messaging-config.xml 文件,在 adapters 節點內加入一個 adapter-definition 來聲明自定義 Adapter :
< adapter-definition id ="myad" class ="org.rosenjiang.flex.MyMessageAdapter" />
接着定義一個 destination ,以便 Flex 客戶端能訂閱聊天室,組裝好以前定義的長輪詢頻道和 adapter :
< destination id ="chat" >
< channels >
< channel ref ="long-polling-amf" />
</ channels >
< adapter ref ="myad" />
</ destination >
服務器端就算搞定了,接着搞定 Flex 那邊的代碼吧,灰常灰常的簡單。先到 Building your client-side application 學習如何建立和 BlazeDS 通信的 Flex 項目。而後在 chat.mxml 中寫下:
<? xml version="1.0" encoding="utf-8" ?>
< mx:Application xmlns:mx ="http://www.adobe.com/2006/mxml" creationComplete ="consumer.subscribe();send()" >
< mx:Script >
<![CDATA[
import mx.messaging.messages.AsyncMessage;
import mx.messaging.messages.IMessage;
private function send():void
{
var message:IMessage = new AsyncMessage();
message.body.chatMessage = msg.text;
producer.send(message);
msg.text = "";
}
private function messageHandler(message:IMessage):void
{
log.text = message.body.chatMessage + "\n";
}
]]>
</ mx:Script >
< mx:Producer id ="producer" destination ="chat" />
< mx:Consumer id ="consumer" destination ="chat" message ="messageHandler(event.message)" />
< mx:Panel title ="Chat" width ="100%" height ="100%" >
< mx:TextArea id ="log" width ="100%" height ="100%" />
< mx:ControlBar >
< mx:TextInput id ="msg" width ="100%" enter ="send()" />
< mx:Button label ="Send" click ="send()" />
</ mx:ControlBar >
</ mx:Panel >
</ mx:Application >
以前咱們說到的 Producer 和 Consumer 組件在這裏出現了,因爲咱們要訂閱的是同一個聊天室,因此 destination="chat" ,而 Consumer 組件則註冊回調函數 messageHandler() ,處理異步消息的到來。當打開這個聊天客戶端的時候,在 creationComplete 初始化完成後,當即進行 consumer.subscribe() ,其實接下來應該就能直接收到服務器端回饋的聊天記錄了,可是我沒仔細學習如何監聽客戶端的訂閱,因此在這裏我直接 send() 了一個空消息以便服務器端能回饋已有的聊天記錄,接下來我就不用再講解了,都能看懂。
如今打開瀏覽器,感覺下長輪詢的效果吧。不過遇到個問題,若是 FF 同時開兩個聊天窗口,第二個打開的會有延遲感, IE 也是,按照牛人們的說法,當一個瀏覽器開兩個以上長鏈接的時候纔會有延遲感,不解。 BlazeDS 的長輪詢也不是十全十美,有人說它不是真正的「實時」 The Truth About BlazeDS and Push Messaging ,隨即引起出口水仗,裏面提到的 RTMP 協議在 2009 年 1 月已開源,相信之後 BlazeDS 會更「實時」;接着又有人說 BlazeDS 不是非阻塞式的,這個問題後來也沒人來對應。罷了,畢竟BlazeDS纔開源不久,容忍一下吧。最後,我想說的是,不論 BlazeDS 到底有什麼問題,至少實現起來是輕鬆的,在 Servlet 3.0 沒發佈以前,是個不錯的選擇。
請注意!引用、轉貼本文應註明原做者:Rosen Jiang 以及出處: http://www.blogjava.net/rosen