Ajax轮询以及Comet模式—写在Servlet 3.0发布之前(转)

时间:2023-03-08 20:05:39

2008 年的夏天,偶然在网上闲逛的时候发现了 Comet 技术,人云亦云间,姑且认为它是由 Dojo 的 Alex Russell 在 2006 年提出。在阅读了大量的资料后,萌发出写篇 blog 来说明什么是 Comet 的想法。哪知道这个想法到了半年后的今天才提笔,除了繁忙的工作拖延外,还有 Comet 本身带来的困惑。

Comet 能带来生产力的提升是有目共睹的。现在假设有 1000 个用户在使用某软件,轮询 (polling) 和 Comet 的设定都是 1s 、 10s 、 100s 的潜伏期,那么在相同的潜伏期内, Comet 所需要的带宽更小,如下图:

Ajax轮询以及Comet模式—写在Servlet 3.0发布之前(转)

不仅仅是在带宽上的优势,每个用户所真正感受到的响应时间(潜伏期)更短,给人的感觉也就更加的实时,如下图:

Ajax轮询以及Comet模式—写在Servlet 3.0发布之前(转)

再引用一篇 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 客户端发起请求到服务器端取回数据,如下图所示:

Ajax轮询以及Comet模式—写在Servlet 3.0发布之前(转)

至于轮询的缺点,在前面的论述中已有覆盖,至于优点大家可以 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 
=

;
    
//
 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
=

; 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.

当客户端处理接收的数据、重新建立连接时,服务器端可能有新的数据到达;这些信息会被服务器端保存直到客户端重新建立连接,客户端会一次把当前服务器端所有的信息取回。

下图很好的说明了以上特征:

Ajax轮询以及Comet模式—写在Servlet 3.0发布之前(转)

既然关注的是
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
>

</
wait-interval-millis
>

<
polling-interval-millis
>

</
polling-interval-millis
>

<
max-waiting-poll-requests
>

</
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 
=

;
    
//
 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
=

; 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