Comet另一种形式为长轮询(long polling),客户端会与服务器建立一个持久的连接,直到服务器端有数据发送过来,服务器端断开,客户端处理完推送的数据,会再次发起一个持久的连接,循环往复。

和流(Streaming)区别主要在于,在一次长连接中,服务器端只推送一次,然后断开连接。

其实现形式大概可分文AJAX长轮询和JAVASCRIPT轮询两种。

AJAX方式请求长轮询

服务器端可以返回原始的数据,或格式化的JSON、XML、JAVASCRIPT信息,客户端可拥有更多的处理自由。在形式上和传统意义上的AJAX GET方式大致一样,只不过下一次的轮询需要等到上一次的轮询数据返回处理完方可进行。

这里给出客户端的小示范:

jQuery 1.5 with long poll 
 
$(function (){
function log(resp){
$("#tip").html("" + resp + "");
}
log("loading");
// 去除缓存
$.ajaxSetup({ cache: false });
function initGet(){
$.get("getNextTime")
.success(function(resp){
log(resp);
}).error(function(){
log("ERROR!");
}).done(initGet);
}
initGet();
});

基于jQuery 1.5,事件链,比以往更简洁明了,尤其是在done方法中又一次调用自身,棒极了。

服务器端很简单,每1秒输出一次当前系统时间:

/**
* 简单模拟每秒输出一次当前系统时间,精细到毫秒
*
* @author yongboy
* @date 2011-2-11
* @version 1.0
*/
@WebServlet("/getNextTime")
public class GetNextTimeServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
response.setContentType("text/plain");
PrintWriter out = response.getWriter();
out.print(DateFormatUtils.format(System.currentTimeMillis(),
"yyyy-MM-dd HH:mm:ss SSS"));
out.flush();
out.close();
}
}

JAVASCRIPT标签轮询(Script tag long polling)

引用的JAVASCRIPT脚本文件是可以跨域的,再加上JSONP,更为强大了。

这里给出一个演示,实现功能类似上面。

客户端清单:

jQuery 1.5 with JSONP FOR Comet 
 
$(function (){
function log(resp){
$("#tip").html("" resp "");
}
log("loading");
function jsonp(){
$.getJSON('http://192.168.0.99:8080/servlet3/getNextTime2?callback=?').success(function(resp){
log("now time : " resp);
}).done(jsonp);
// 以下方式也是合法的
/*
$.getJSON('http://192.168.0.99:8080/servlet3/getNextTime2?callback=?',function(date){
log(date);
}).done(jsonp);
*/
}
jsonp();
});

服务器端清单:

/**
* JSONP形式简单模拟每秒输出一次当前系统时间,精细到毫秒
* @author yongboy
* @date 2011-2-11
* @version 1.0
*/
@WebServlet("/getNextTime2")
public class GetNextTimeServlet2 extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
String callback = request.getParameter("callback");
if(StringUtils.isBlank(callback)){
callback = "showResult";
}
PrintWriter out = response.getWriter();
StringBuilder resultBuilder = new StringBuilder();
resultBuilder
.append(callback)
.append("('")
.append(DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd HH:mm:ss SSS"))
.append("');");
out.print(resultBuilder);
out.flush();
out.close();
}
}

每次请求时,都会在HTML头部生成一个JS网络地址(实现跨域):

http://192.168.0.99:8080/servlet3/getNextTime2?callback=jQuery150832738454006945_1297761629067&_=1297761631777

我们不用指定,jquery会自动生成一个随机的函数名。

请求上面地址服务器端在有新的数据输出时,生成的回调JS函数:

jQuery150832738454006945_1297761629067('2011-02-15 17:20:33 921');

从下面截图可以看到这一点。

生成相应内容:

 

不过,长连接情况下,浏览器认为当前JS文件还没有加载完,会一直显示正在加载中。

上面的JSONP例子太简单,服务器和客户端的连接等待时间不过1秒钟时间左右,若在真实环境下,需要设置一个超时时间。

以往可能会有人告诉你,在客户端需要设置一个超时时间,若指定期限内服务器没有数据返回,则需要通知服务器端程序,断开连接,然后自身再次发起一个新的连接请求。

但在Servlet 3.0 异步连接的规范中,我们就不用那么劳心费神,那般麻烦了。掉换个儿,让服务器端通知客户端超时啦,赶紧重新连接啊。

在前面几篇博客中,演示了一个伪真实场景,博客主添加博客,然后主动以流形式推送给终端用户。这里采用JSONP跨域长轮询连接形式。

客户端:

Comet JSONP %u63A8%u9001%u6D4B%u8BD5  
String.prototype.template=function(){
var args=arguments;
return this.replace(/\{(\d )\}/g, function(m, i){
return args[i];
});
}
var html = '
  
'
   

    {0} 
  
'   
'
   

    last date : {1} 
  
'   
'
'
';  
function showContent(json) {
$("#showDiv").prepend(html.template(json.content, json.date));
}
function initJsonp(){
$.getJSON('http://192.168.0.99/servlet3/getjsonpnew?callback=?').success(function(json){
if(json.state == 1){
showContent(json);
}else{
initJsonp();
return;
}
}).done(initJsonp)
.error(function(){
alert("error!");
});
}
$(function (){
initJsonp();
});
  

   loading ...


服务器端接收长轮询连接请求:

/**
* JSONP获取最新信息
*
* @author yongboy
* @date 2011-2-17
* @version 1.0
*/
@WebServlet(urlPatterns = "/getjsonpnew", asyncSupported = true)
public class GetNewJsonpBlogPosts extends HttpServlet {
private static final long serialVersionUID = 565698895565656L;
private static final Log log = LogFactory.getLog(GetNewJsonpBlogPosts.class);
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setHeader("Cache-Control", "private");
response.setHeader("Pragma", "no-cache");
response.setHeader("Connection", "Keep-Alive");
response.setHeader("Proxy-Connection", "Keep-Alive");
response.setContentType("text/javascript;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
String timeoutStr = request.getParameter("timeout");
long timeout;
if (StringUtils.isNumeric(timeoutStr)) {
timeout = Long.parseLong(timeoutStr);
} else {
// 设置1分钟
timeout = 1L * 60L * 1000L;
}
log.info("new request ...");
final HttpServletResponse finalResponse = response;
final HttpServletRequest finalRequest = request;
final AsyncContext ac = request.startAsync(request, finalResponse);
// 设置成长久链接
ac.setTimeout(timeout);
ac.addListener(new AsyncListener() {
public void onComplete(AsyncEvent event) throws IOException {
log.info("onComplete Event!");
NewBlogJsonpListener.ASYNC_AJAX_QUEUE.remove(ac);
}
public void onTimeout(AsyncEvent event) throws IOException {
// 尝试向客户端发送超时方法调用,客户端会再次请求/blogpush,周而复始
log.info("onTimeout Event!");
// 通知客户端再次进行重连
final PrintWriter writer = finalResponse.getWriter();
String outString = finalRequest.getParameter("callback")
+ "({state:0,error:'timeout is now'});";
writer.println(outString);
writer.flush();
writer.close();
NewBlogJsonpListener.ASYNC_AJAX_QUEUE.remove(ac);
}
public void onError(AsyncEvent event) throws IOException {
log.info("onError Event!");
NewBlogJsonpListener.ASYNC_AJAX_QUEUE.remove(ac);
}
public void onStartAsync(AsyncEvent event) throws IOException {
log.info("onStartAsync Event!");
}
});
NewBlogJsonpListener.ASYNC_AJAX_QUEUE.add(ac);
}
}

很显然,在有博客数据到达时,会通知到已注册的客户端。

注意,推送数据之后,需要调用当前的异步上下文complete()函数:

/**
* 监听器单独线程推送到客户端
*
* @author yongboy
* @date 2011-2-17
* @version 1.0
*/
@WebListener
public class NewBlogJsonpListener implements ServletContextListener {
private static final Log log = LogFactory.getLog(NewBlogListener.class);
public static final BlockingQueue BLOG_QUEUE = new LinkedBlockingQueue();
public static final Queue ASYNC_AJAX_QUEUE = new ConcurrentLinkedQueue();
public void contextDestroyed(ServletContextEvent arg0) {
log.info("context is destroyed!");
}
public void contextInitialized(ServletContextEvent servletContextEvent) {
log.info("context is initialized!");
// 启动一个线程处理线程队列
new Thread(runnable).start();
}
private Runnable runnable = new Runnable() {
public void run() {
boolean isDone = true;
while (isDone) {
if (!BLOG_QUEUE.isEmpty()) {
try {
log.info("ASYNC_AJAX_QUEUE size : "
+ ASYNC_AJAX_QUEUE.size());
MicBlog blog = BLOG_QUEUE.take();
if (ASYNC_AJAX_QUEUE.isEmpty()) {
continue;
}
String targetJSON = buildJsonString(blog);
for (AsyncContext context : ASYNC_AJAX_QUEUE) {
if (context == null) {
log.info("the current ASYNC_AJAX_QUEUE is null now !");
continue;
}
log.info(context.toString());
PrintWriter out = context.getResponse().getWriter();
if (out == null) {
log.info("the current ASYNC_AJAX_QUEUE's PrintWriter is null !");
continue;
}
out.println(context.getRequest().getParameter(
"callback")
+ "(" + targetJSON + ");");
out.flush();
// 通知,执行完成函数
context.complete();
}
} catch (Exception e) {
e.printStackTrace();
isDone = false;
}
}
}
}
};
private static String buildJsonString(MicBlog blog) {
Map info = new HashMap();
info.put("state", 1);
info.put("content", blog.getContent());
info.put("date",
DateFormatUtils.format(blog.getPubDate(), "HH:mm:ss SSS"));
JSONObject jsonObject = JSONObject.fromObject(info);
return jsonObject.toString();
}
}

长连接的超时时间,有人贴切的形容为“心跳频率”,联动着两端,需要根据环境设定具体值。

JSONP方式无论是轮询还是长轮询,都是可以很容易把目标程序融合到第三方网站,当前的个人主页、博客等很多小挂件大多也是采用这种形式进行跨域获取数据的。