2008 年的夏天,偶然在網上閑逛的時候發現了 Comet 技術,人云亦云間,姑且認為它是由 Dojo 的 Alex Russell 在 2006 年提出。在閱讀了大量的資料后,萌發出寫篇 blog 來說明什么是 Comet 的想法。哪知道這個想法到了半年后的今天才提筆,除了繁忙的工作拖延外,還有 Comet 本身帶來的困惑。
Comet 能帶來生產力的提升是有目共睹的。現在假設有 1000 個用戶在使用某軟件,輪詢 (polling) 和 Comet 的設定都是 1s 、 10s 、 100s 的潛伏期,那么在相同的潛伏期內, Comet 所需要的帶寬更小,如下圖:
不僅僅是在帶寬上的優勢,每個用戶所真正感受到的響應時間(潛伏期)更短,給人的感覺也就更加的實時,如下圖:
再引用一篇 IBMDW 上的譯文《使用 Jetty 和 Direct Web Remoting 編寫可擴展的 Comet 應用程序》,其中說到:吸引人們使用 Comet 策略的其中一個優點是其顯而易見的高效性。客戶機不會像使用輪詢方法那樣生成煩人的通信量,并且事件發生后可立即發布給客戶機。
上面一遍一遍的說到 Comet 技術的優勢,那么我們可以替換現有的技術結構了?不幸的是,近半年的擦邊球式的關注使我對 Comet 的理解越發的糊涂,甚至有人說 Comet 這個名詞已被濫用。去年的一篇博文,《 The definition of Comet? 》使 Comet 更加撲朔迷離,甚至在維基百科上大家也對準確的 Comet 定義產生爭論。還是等牛人們爭論清楚再修改維基百科吧,在這里我想還是引用維基百科對 Comet 的定義:服務器推模式 (HTTP server push 、 streaming) 以及長輪詢 (long polling) ,這兩種模式都是 Comet 的實現。
除了對 Comet 的準確定義尚缺乏有效的定論外, Comet 還存在不少技術難題,隨著 Tomcat 6 、 Jetty 6 的發布,他們基于 NIO 各自實現了異步 Servlet 機制。有興趣的看官可以分別實現這兩個容器的 Comet ,至少我還沒玩轉。
在編寫服務器端的代碼上面,我很困惑, 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 客戶端發起請求到服務器端取回數據,如下圖所示:
?????????????????????????
至于輪詢的缺點,在前面的論述中已有覆蓋,至于優點大家可以 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 條聊天信息,舊的將被丟棄,另外新的客戶端進來后能讀取到最近的信息:
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://m.tkk7.com/rosen
Powered by: BlogJava Copyright © Rosen