Servlet 3.0筆記之異步請求Comet推送長輪詢(long polling)篇
Comet另一種形式為長輪詢(long polling),客戶端會與服務器建立一個持久的連接,直到服務器端有數據發送過來,服務器端斷開,客戶端處理完推送的數據,會再次發起一個持久的連接,循環往復。
和流(Streaming)區別主要在于,在一次長連接中,服務器端只推送一次,然后斷開連接。
其實現形式大概可分文AJAX長輪詢和JAVASCRIPT輪詢兩種。
- AJAX方式請求長輪詢
服務器端可以返回原始的數據,或格式化的JSON、XML、JAVASCRIPT信息,客戶端可擁有更多的處理自由。在形式上和傳統意義上的AJAX GET方式大致一樣,只不過下一次的輪詢需要等到上一次的輪詢數據返回處理完方可進行。
這里給出客戶端的小示范:<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
基于jQuery 1.5,事件鏈,比以往更簡潔明了,尤其是在done方法中又一次調用自身,棒極了。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>jQuery 1.5 with long poll</title>
</head>
<body>
<div id="tip"></div>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.min.js"></script>
<script type="text/javascript">
$(function (){
function log(resp){
$("#tip").html("<b>" + resp + "</b>");
}
log("loading");
// 去除緩存
$.ajaxSetup({ cache: false });
function initGet(){
$.get("getNextTime")
.success(function(resp){
log(resp);
}).error(function(){
log("ERROR!");
}).done(initGet);
}
initGet();
});
</script>
</body>
</html>
服務器端很簡單,每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,更為強大了。
這里給出一個演示,實現功能類似上面。
客戶端清單:<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>jQuery 1.5 with JSONP FOR Comet</title>
</head>
<body>
<div id="tip"></div>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.min.js"></script>
<script type="text/javascript">
$(function (){
function log(resp){
$("#tip").html("<b>" resp "</b>");
}
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();
});
</script>
</body>
</html>
服務器端清單:/**
* 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跨域長輪詢連接形式。
客戶端:
<html>
<head>
<title>Comet JSONP %u63A8%u9001%u6D4B%u8BD5</title>
<meta http-equiv="X-UA-Compatible" content="IE=8" />
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
<meta name="author" content="yongboy@gmail.com"/>
<meta name="keywords" content="servlet3, comet, ajax"/>
<meta name="description" content=""/>
<link type="text/css" rel="stylesheet" href="css/main.css"/>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.min.js"></script>
<script type="text/javascript">
String.prototype.template=function(){
var args=arguments;
return this.replace(/\{(\d )\}/g, function(m, i){
return args[i];
});
}
var html = '<div class="logDiv">'
'<div class="contentDiv">{0}</div>'
'<div class="tipDiv">last date : {1}</div>'
'<div class="clear"> </div>'
'</div>';
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();
});
</script>
</head>
<body style="margin: 0; overflow: hidden">
<div id="showDiv" class="inputStyle">loading ...</div>
</body>
</html>
服務器端接收長輪詢連接請求:
/**
* 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 BlockingQueueBLOG_QUEUE = new LinkedBlockingQueue ();
public static final QueueASYNC_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) {
Mapinfo = 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方式無論是輪詢還是長輪詢,都是可以很容易把目標程序融合到第三方網站,當前的個人主頁、博客等很多小掛件大多也是采用這種形式進行跨域獲取數據的。
posted on 2011-02-17 18:03 nieyong 閱讀(8730) 評論(0) 編輯 收藏 所屬分類: Servlet3