天天看點

Web 通信 之 長連接配接、長輪詢(long polling)

基于HTTP的長連接配接,是一種通過長輪詢方式實作"伺服器推"的技術,它彌補了HTTP簡單的請求應答模式的不足,極大地增強了程式的實時性和互動性。

 一、什麼是長連接配接、長輪詢?

用通俗易懂的話來說,就是用戶端不停的向伺服器發送請求以擷取最新的資料資訊。這裡的“不停”其實是有停止的,隻是我們人眼無法分辨是否停止,它隻是一種快速的停下然後又立即開始連接配接而已。

二、長連接配接、長輪詢的應用場景

長連接配接、長輪詢一般應用與WebIM、ChatRoom和一些需要及時互動的網站應用中。其真實案例有:WebQQ、Hi網頁版、Facebook IM等。

如果你對伺服器端的反向Ajax感興趣,可以參考這篇文章 DWR 反向Ajax 伺服器端推的方式:http://www.cnblogs.com/hoojo/category/276235.html

歡迎大家繼續支援和關注我的部落格:

http://hoojo.cnblogs.com

http://blog.csdn.net/IBM_hoojo

也歡迎大家和我交流、探讨IT方面的知識。

email:[email protected]

三、優缺點

輪詢:用戶端定時向伺服器發送Ajax請求,伺服器接到請求後馬上傳回響應資訊并關閉連接配接。 

優點:後端程式編寫比較容易。 

缺點:請求中有大半是無用,浪費帶寬和伺服器資源。 

執行個體:适于小型應用。

長輪詢:用戶端向伺服器發送Ajax請求,伺服器接到請求後hold住連接配接,直到有新消息才傳回響應資訊并關閉連接配接,用戶端處理完響應資訊後再向伺服器發送新的請求。 

優點:在無消息的情況下不會頻繁的請求,耗費資源小。 

缺點:伺服器hold連接配接會消耗資源,傳回資料順序無保證,難于管理維護。 

執行個體:WebQQ、Hi網頁版、Facebook IM。

長連接配接:在頁面裡嵌入一個隐蔵iframe,将這個隐蔵iframe的src屬性設為對一個長連接配接的請求或是采用xhr請求,伺服器端就能源源不斷地往用戶端輸入資料。 

優點:消息即時到達,不發無用請求;管理起來也相對友善。 

缺點:伺服器維護一個長連接配接會增加開銷。 

執行個體:Gmail聊天

Flash Socket:在頁面中内嵌入一個使用了Socket類的 Flash 程式JavaScript通過調用此Flash程式提供的Socket接口與伺服器端的Socket接口進行通信,JavaScript在收到伺服器端傳送的資訊後控制頁面的顯示。 

優點:實作真正的即時通信,而不是僞即時。 

缺點:用戶端必須安裝Flash插件;非HTTP協定,無法自動穿越防火牆。 

執行個體:網絡互動遊戲。

四、實作原理

所謂長連接配接,就是要在用戶端與伺服器之間建立和保持穩定可靠的連接配接。其實它是一種很早就存在的技術,但是由于浏覽器技術的發展比較緩慢,沒有為這種機制的實作提供很好的支援。是以要達到這種效果,需要用戶端和伺服器的程式共同配合來完成。通常的做法是,在伺服器的程式中加入一個死循環,在循環中監測資料的變動。當發現新資料時,立即将其輸出給浏覽器并斷開連接配接,浏覽器在收到資料後,再次發起請求以進入下一個周期,這就是常說的長輪詢(long-polling)方式。如下圖所示,它通常包含以下幾個關鍵過程:
Web 通信 之 長連接配接、長輪詢(long polling)

1. 輪詢的建立 

建立輪詢的過程很簡單,浏覽器發起請求後進入循環等待狀态,此時由于伺服器還未做出應答,是以HTTP也一直處于連接配接狀态中。 

2. 資料的推送 

在循環過程中,伺服器程式對資料變動進行監控,如發現更新,将該資訊輸出給浏覽器,随即斷開連接配接,完成應答過程,實作“伺服器推”。 

3. 輪詢的終止 

輪詢可能在以下3種情況時終止: 

  3.1. 有新資料推送 

   當循環過程中伺服器向浏覽器推送資訊後,應該主動結束程式運作進而讓連接配接斷開,這樣浏覽器才能及時收到資料。 

  3.2. 沒有新資料推送 

   循環不能一直持續下去,應該設定一個最長時限,避免WEB伺服器逾時(Timeout),若一直沒有新資訊,伺服器應主動向浏覽器發送本次輪詢無新資訊的正常響應,并斷開連接配接,這也被稱為“心跳”資訊。 

  3.3. 網絡故障或異常 

   由于網絡故障等因素造成的請求逾時或出錯也可能導緻輪詢的意外中斷,此時浏覽器将收到錯誤資訊。 

4. 輪詢的重建 

浏覽器收到回複并進行相應處理後,應馬上重新發起請求,開始一個新的輪詢周期。

五、程式設計

1、普通輪詢 Ajax方式

用戶端代碼片段
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>      
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">      
<html>      
<head>      
<meta http-equiv="pragma" content="no-cache">      
<meta http-equiv="cache-control" content="no-cache">      
<meta http-equiv="author" content="hoojo & http://hoojo.cnblogs.com">      
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />      
<%@ include file="/tags/jquery-lib.jsp"%>      
<script type="text/javascript">      
$(function () {      
window.setInterval(function () {      
$.get("${pageContext.request.contextPath}/communication/user/ajax.mvc",       
{"timed": new Date().getTime()},       
function (data) {      
$("#logs").append("[data: " + data + " ]<br/>");      
});      
}, 3000);      
});      
</script>      
</head>      
<body>      
<div id="logs"></div>      
</body>      
</html>      

用戶端實作的就是用一種普通輪詢的結果,比較簡單。利用setInterval不間斷的重新整理來擷取伺服器的資源,這種方式的優點就是簡單、及時。缺點是連結多數是無效重複的;響應的結果沒有順序(因為是異步請求,當發送的請求沒有傳回結果的時候,後面的請求又被發送。而此時如果後面的請求比前面的請求要先傳回結果,那麼目前面的請求傳回結果資料時已經是過時無效的資料了);請求多,難于維護、浪費伺服器和網絡資源。

伺服器端代碼

@RequestMapping("/ajax")      
public void ajax(long timed, HttpServletResponse response) throws Exception {      
PrintWriter writer = response.getWriter();      
Random rand = new Random();      
// 死循環 查詢有無資料變化      
while (true) {      
Thread.sleep(300); // 休眠300毫秒,模拟處理業務等      
int i = rand.nextInt(100); // 産生一個0-100之間的随機數      
if (i > 20 && i < 56) { // 如果随機數在20-56之間就視為有效資料,模拟資料發生變化      
long responseTime = System.currentTimeMillis();      
// 傳回資料資訊,請求時間、傳回資料時間、耗時      
writer.print("result: " + i + ", response time: " + responseTime + ", request time: " + timed + ", use time: " + (responseTime - timed));      
break; // 跳出循環,傳回資料      
} else { // 模拟沒有資料變化,将休眠 hold住連接配接      
Thread.sleep(1300);      
}      
}      
}      

伺服器端實作,這裡就模拟下程式監控資料的變化。上面代碼屬于SpringMVC 中controller中的一個方法,相當于Servlet中的一個doPost/doGet方法。如果沒有程式環境适應servlet即可,将方法體中的代碼copy到servlet的doGet/doPost中即可。

伺服器端在進行長連接配接的程式設計時,要注意以下幾點: 

1. 伺服器程式對輪詢的可控性 

由于輪詢是用死循環的方式實作的,是以在算法上要保證程式對何時退出循環有完全的控制能力,避免進入死循環而耗盡伺服器資源。 

2. 合理選擇“心跳”頻率 

從圖1可以看出,長連接配接必須由用戶端不停地進行請求來維持,是以在用戶端和伺服器間保持正常的“心跳”至為關鍵,參數POLLING_LIFE應小于WEB伺服器的逾時時間,一般建議在10~20秒左右。 

3. 網絡因素的影響 

在實際應用時,從伺服器做出應答,到下一次循環的建立,是有時間延遲的,延遲時間的長短受網絡傳輸等多種因素影響,在這段時間内,長連接配接處于暫時斷開的空檔,如果恰好有資料在這段時間内發生變動,伺服器是無法立即進行推送的,是以,在算法設計上要注意解決由于延遲可能造成的資料丢失問題。 

4. 伺服器的性能 

在長連接配接應用中,伺服器與每個用戶端執行個體都保持一個持久的連接配接,這将消耗大量伺服器資源,特别是在一些大型應用系統中更是如此,大量并發的長連接配接有可能導緻新的請求被阻塞甚至系統崩潰,是以,在進行程式設計時應特别注意算法的優化和改進,必要時還需要考慮伺服器的負載均衡和叢集技術。

Web 通信 之 長連接配接、長輪詢(long polling)
上圖是傳回的結果,可以看到先送出請求,不一定會最先傳回結果。這樣就不能保證順序,造成髒資料或無用的連接配接請求。可見對伺服器或網絡的資源浪費。

2、普通輪詢 iframe方式

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>      
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">      
<html>      
<head>      
<meta http-equiv="pragma" content="no-cache">      
<meta http-equiv="cache-control" content="no-cache">      
<meta http-equiv="expires" content="0">      
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />      
<%@ include file="/tags/jquery-lib.jsp"%>      
<script type="text/javascript">      
$(function () {      
window.setInterval(function () {      
$("#logs").append("[data: " + $($("#frame").get(0).contentDocument).find("body").text() + " ]<br/>");      
$("#frame").attr("src", "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime());      
// 延遲1秒再重新請求      
window.setTimeout(function () {      
window.frames["polling"].location.reload();      
}, 1000);      
}, 5000);      
});      
</script>      
</head>      
<body>      
<iframe id="frame" name="polling" style="display: none;"></iframe>      
<div id="logs"></div>      
</body>      
</html>      
這裡的用戶端程式是利用隐藏的iframe向伺服器端不停的拉取資料,将iframe擷取後的資料填充到頁面中即可。同ajax實作的基本原理一樣,唯一不同的是當一個請求沒有響應傳回資料的情況下,下一個請求也将開始,這時候前面的請求将被停止。如果要使程式和上面的ajax請求一樣也可以辦到,那就是給每個請求配置設定一個獨立的iframe即可。下面是傳回的結果:
Web 通信 之 長連接配接、長輪詢(long polling)
其中紅色是沒有成功傳回請求就被停止(後面請求開始)掉的請求,黑色是成功傳回資料的請求。

3、長連接配接iframe方式

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>      
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">      
<html>      
<head>      
<meta http-equiv="pragma" content="no-cache">      
<meta http-equiv="cache-control" content="no-cache">      
<meta http-equiv="author" content="hoojo & http://hoojo.cnblogs.com">      
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />      
<%@ include file="/tags/jquery-lib.jsp"%>      
<script type="text/javascript">      
$(function () {      
window.setInterval(function () {      
var url = "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime();      
var $iframe = $('<iframe id="frame" name="polling" style="display: none;" src="' + url + '"></iframe>');      
$("body").append($iframe);      
$iframe.load(function () {      
$("#logs").append("[data: " + $($iframe.get(0).contentDocument).find("body").text() + " ]<br/>");      
$iframe.remove();      
});      
}, 5000);      
});      
</script>      
</head>      
<body>      
<div id="logs"></div>      
</body>      
</html>      
這個輪詢方式就是把剛才上面的稍微改下,每個請求都有自己獨立的一個iframe,當這個iframe得到響應的資料後就把資料push到目前頁面上。使用此方法已經類似于ajax的異步互動了,這種方法也是不能保證順序的、比較耗費資源、而且總是有一個加載的條在位址欄或狀态欄附件(當然要解決可以利用htmlfile,Google的攻城師們已經做到了,網上也有封裝好的lib庫),但用戶端實作起來比較簡單。
Web 通信 之 長連接配接、長輪詢(long polling)
如果要保證有序,可以不使用setInterval,将建立iframe的方法放在load事件中即可,即使用遞歸方式。調整後的代碼片段如下:
<script type="text/javascript">      
$(function () {      
(function iframePolling() {      
var url = "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime();      
var $iframe = $('<iframe id="frame" name="polling" style="display: none;" src="' + url + '"></iframe>');      
$("body").append($iframe);      
$iframe.load(function () {      
$("#logs").append("[data: " + $($iframe.get(0).contentDocument).find("body").text() + " ]<br/>");      
$iframe.remove();      
// 遞歸      
iframePolling();      
});      
})();          
});      
</script>      
這種方式雖然保證了請求的順序,但是它不會處理請求延時的錯誤或是說很長時間沒有傳回結果的請求,它會一直等到傳回請求後才能建立下一個iframe請求,總會和伺服器保持一個連接配接。和以上輪詢比較,缺點就是消息不及時,但保證了請求的順序。

4、ajax實作長連接配接

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>      
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">      
<html>      
<head>      
<meta http-equiv="pragma" content="no-cache">      
<meta http-equiv="cache-control" content="no-cache">      
<meta http-equiv="expires" content="0">      
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />      
<%@ include file="/tags/jquery-lib.jsp"%>      
<script type="text/javascript">      
$(function () {      
(function longPolling() {      
$.ajax({      
url: "${pageContext.request.contextPath}/communication/user/ajax.mvc",      
data: {"timed": new Date().getTime()},      
dataType: "text",      
timeout: 5000,      
error: function (XMLHttpRequest, textStatus, errorThrown) {      
$("#state").append("[state: " + textStatus + ", error: " + errorThrown + " ]<br/>");      
if (textStatus == "timeout") { // 請求逾時      
longPolling(); // 遞歸調用      
// 其他錯誤,如網絡錯誤等      
} else {       
longPolling();      
}      
},      
success: function (data, textStatus) {      
$("#state").append("[state: " + textStatus + ", data: { " + data + "} ]<br/>");      
if (textStatus == "success") { // 請求成功      
longPolling();      
}      
}      
});      
})();      
});      
</script>      
</head>      
<body>      
上面這段代碼就是才有Ajax的方式完成長連接配接,主要優點就是和伺服器始終保持一個連接配接。如果目前連接配接請求成功後,将更新資料并且繼續建立一個新的連接配接和伺服器保持聯系。如果連接配接逾時或發生異常,這個時候程式也會建立一個新連接配接繼續請求。這樣就大大節省了伺服器和網絡資源,提高了程式的性能,進而也保證了程式的順序。
Web 通信 之 長連接配接、長輪詢(long polling)

六、總結

現代的浏覽器都支援跨域資源共享(Cross-Origin Resource Share,CORS)規範,該規範允許XHR執行跨域請求,是以基于腳本的和基于iframe的技術已成為了一種過時的需要。

把Comet做為反向Ajax的實作和使用的最好方式是通過XMLHttpRequest對象,該做法提供了一個真正的連接配接句柄和錯誤處理。當然你選擇經由HTTP長輪詢使用XMLHttpRequest對象(在伺服器端挂起的一個簡單的Ajax請求)的Comet模式,所有支援Ajax的浏覽器也都支援該種做法。

基于HTTP的長連接配接技術,是目前在純浏覽器環境下進行即時互動類應用開發的理想選擇,随着浏覽器的快速發展,html5将為其提供更好的支援和更廣泛的應用。在html5中有一個websocket 可以很友好的完成長連接配接這一技術,網上也有相關方面的資料,這裡也就不再做過多介紹。

Web 通信 之 長連接配接、長輪詢(long polling)
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>      
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">      
<html>      
<head>      
<meta http-equiv="pragma" content="no-cache">      
<meta http-equiv="cache-control" content="no-cache">      
<meta http-equiv="author" content="hoojo & http://hoojo.cnblogs.com">      
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />      
<%@ include file="/tags/jquery-lib.jsp"%>      
<script type="text/javascript">      
$(function () {      
window.setInterval(function () {      
$.get("${pageContext.request.contextPath}/communication/user/ajax.mvc",       
{"timed": new Date().getTime()},       
function (data) {      
$("#logs").append("[data: " + data + " ]<br/>");      
});      
}, 3000);      
});      
</script>      
</head>      
<body>      
<div id="logs"></div>      
</body>      
</html>      
@RequestMapping("/ajax")      
public void ajax(long timed, HttpServletResponse response) throws Exception {      
PrintWriter writer = response.getWriter();      
Random rand = new Random();      
// 死循環 查詢有無資料變化      
while (true) {      
Thread.sleep(300); // 休眠300毫秒,模拟處理業務等      
int i = rand.nextInt(100); // 産生一個0-100之間的随機數      
if (i > 20 && i < 56) { // 如果随機數在20-56之間就視為有效資料,模拟資料發生變化      
long responseTime = System.currentTimeMillis();      
// 傳回資料資訊,請求時間、傳回資料時間、耗時      
writer.print("result: " + i + ", response time: " + responseTime + ", request time: " + timed + ", use time: " + (responseTime - timed));      
break; // 跳出循環,傳回資料      
} else { // 模拟沒有資料變化,将休眠 hold住連接配接      
Thread.sleep(1300);      
}      
}      
}      
Web 通信 之 長連接配接、長輪詢(long polling)
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>      
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">      
<html>      
<head>      
<meta http-equiv="pragma" content="no-cache">      
<meta http-equiv="cache-control" content="no-cache">      
<meta http-equiv="expires" content="0">      
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />      
<%@ include file="/tags/jquery-lib.jsp"%>      
<script type="text/javascript">      
$(function () {      
window.setInterval(function () {      
$("#logs").append("[data: " + $($("#frame").get(0).contentDocument).find("body").text() + " ]<br/>");      
$("#frame").attr("src", "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime());      
// 延遲1秒再重新請求      
window.setTimeout(function () {      
window.frames["polling"].location.reload();      
}, 1000);      
}, 5000);      
});      
</script>      
</head>      
<body>      
<iframe id="frame" name="polling" style="display: none;"></iframe>      
<div id="logs"></div>      
</body>      
</html>      
Web 通信 之 長連接配接、長輪詢(long polling)
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>      
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">      
<html>      
<head>      
<meta http-equiv="pragma" content="no-cache">      
<meta http-equiv="cache-control" content="no-cache">      
<meta http-equiv="author" content="hoojo & http://hoojo.cnblogs.com">      
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />      
<%@ include file="/tags/jquery-lib.jsp"%>      
<script type="text/javascript">      
$(function () {      
window.setInterval(function () {      
var url = "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime();      
var $iframe = $('<iframe id="frame" name="polling" style="display: none;" src="' + url + '"></iframe>');      
$("body").append($iframe);      
$iframe.load(function () {      
$("#logs").append("[data: " + $($iframe.get(0).contentDocument).find("body").text() + " ]<br/>");      
$iframe.remove();      
});      
}, 5000);      
});      
</script>      
</head>      
<body>      
<div id="logs"></div>      
</body>      
</html>      
Web 通信 之 長連接配接、長輪詢(long polling)
<script type="text/javascript">      
$(function () {      
(function iframePolling() {      
var url = "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime();      
var $iframe = $('<iframe id="frame" name="polling" style="display: none;" src="' + url + '"></iframe>');      
$("body").append($iframe);      
$iframe.load(function () {      
$("#logs").append("[data: " + $($iframe.get(0).contentDocument).find("body").text() + " ]<br/>");      
$iframe.remove();      
// 遞歸      
iframePolling();      
});      
})();          
});      
</script>      
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>      
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">      
<html>      
<head>      
<meta http-equiv="pragma" content="no-cache">      
<meta http-equiv="cache-control" content="no-cache">      
<meta http-equiv="expires" content="0">      
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />      
<%@ include file="/tags/jquery-lib.jsp"%>      
<script type="text/javascript">      
$(function () {      
(function longPolling() {      
$.ajax({      
url: "${pageContext.request.contextPath}/communication/user/ajax.mvc",      
data: {"timed": new Date().getTime()},      
dataType: "text",      
timeout: 5000,      
error: function (XMLHttpRequest, textStatus, errorThrown) {      
$("#state").append("[state: " + textStatus + ", error: " + errorThrown + " ]<br/>");      
if (textStatus == "timeout") { // 請求逾時      
longPolling(); // 遞歸調用      
// 其他錯誤,如網絡錯誤等      
} else {       
longPolling();      
}      
},      
success: function (data, textStatus) {      
$("#state").append("[state: " + textStatus + ", data: { " + data + "} ]<br/>");      
if (textStatus == "success") { // 請求成功      
longPolling();      
}      
}      
});      
})();      
});      
</script>      
</head>      
<body>      
Web 通信 之 長連接配接、長輪詢(long polling)