Ajax輪詢以及Comet模式—寫在Servlet 3.0發佈以前(轉)

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

                         

 

至於輪詢的缺點,在前面的論述中已有覆蓋,至於優勢你們能夠 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

相關文章
相關標籤/搜索