像 Facebook 和 Plurk 這種大型社群網站,每天都有數以萬計 (甚至是百萬計) 的網路使用者在線上,而社群網站為了要讓使用者快速的得到來自其他使用者或是相關服務的動態,通常都會做一些通知機制,當資料出現時,就會立即告知使用者,常玩 Facebook 或 Plurk 的網友應該對這種通知機制十分熟悉...
像 Facebook 和 Plurk 這種大型社群網站,每天都有數以萬計 (甚至是百萬計) 的網路使用者在線上,而社群網站為了要讓使用者快速的得到來自其他使用者或是相關服務的動態,通常都會做一些通知機制,當資料出現時,就會立即告知使用者,常玩 Facebook 或 Plurk 的網友應該對這種通知機制十分熟悉,所以我就不多說了。
但是,一般我們常見的輪詢 (Polling) 機制,不是 server-side only 就是 client-side only,雙方似乎很難有交集,當然也是受限於 Server Application 和 Client Application 的設計與應用程式的環境,只是這兩種作法一來固定時間,二來當流量一大時,勢必會有更長的 delay 或是 queue 的狀況出現 (想像一下如果用戶端每五秒傳 50000 個 request,而五秒後伺服器可能只處理了 44990 個,但 50000 個新的 request 又來了,伺服器等於要處理 94990 個 request...),等於說每次的 request 都可能要因為前一批的 request 沒被處理完就要排隊,而伺服器上的應用程式會因為 database query, network latency 或 application issue 而無法順暢處理大量的 request,雖然有 Load Balancing 方式可以分散,但是問題仍然存在。這個問題的另一面,就是使用者之間的狀況聯繫會受到這個 Polling 延遲而變慢,甚至會完全得不到通知,對社群網站來說,這個問題就相當嚴重了。
為了要解決這個問題,便開始有軟體開發人員在研究怎麼樣來解決這種 Client 和 Server 間的 Push/Poll 模型的問題,2006 年 3 月,有位名叫 Alex Russell 的軟體工程師在他的部落格中提出了 "Comet" 這個名詞,以及其程式設計的方法,然後慢慢的被大型的軟體與網路服務供應商採用,這個方法被稱為 "Comet Programming",它的意思是像彗星 (Comet) 一樣,尾巴會拉的很長,代表當用戶端和伺服器間建立連線時,這個連線會保持一段不短的時間 (可能大於 60 秒),直到得到資料為止。簡單的說,當用戶端和伺服器間一旦建立連線,就會一直保持到得到資料後才會斷線。但為了要保持應用程式持續運作,所以馬上會再建立一條新的連線,讓用戶端和伺服器間的連線一直保持下去,這樣用戶端和伺服器間的資料傳輸的時間差就會降到最低。
Comet Programming 依 push 或 poll 的模型也分為由伺服器發動或是由用戶端發動,如果由伺服器發動 (push) 的話,用戶端勢必一定要接取連線才會成立,這樣一定會遇到防火牆的問題,所以最常看到的實作是由用戶端發動 (poll),而發動的作法只是建立網路連線和接取資料,但是與日常的網路連線最大的差別是,伺服器如果沒有回傳資料,那麼連線就不能中斷,socket 的 Timeout 會設定較長的時間,這也是為什麼會叫做 Long Polling 的原因。而通常服務端會以單純的 HTTP 端點,保持最大的輕量化,而用戶端只會向那個 HTTP 端點連線,Web Server 必須要能接收大量的 HTTP request 並長時間保持,以確保用戶端的連線不會因為 timeout 而自動斷線。
Comet Programming 的最大優點,就是除非 connection timeout,否則只要用戶端發出 HTTP request,就一定可以由伺服器得到結果,但整個 comet programming pattern 卻沒有特別的困難,用戶端只要關注 polling 階段的連線保持,以及接收到結果後再重新建立連線的方式,伺服器端則是保持原樣,而 Web Server 可能要因為 Long Polling 的機制而做一些設定 (ex: default timeout),設定上也不會太困難。
為了要實作這個架構,我建立了一個小的 ASP.NET 專案,並且在伺服器端加上一個 HTTP Handler,並模擬不定時間的伺服器回傳:
public class Time : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
AutoResetEvent signal = new AutoResetEvent(false);
Random rnd = new Random();
int waitSecond = rnd.Next(5, 30);
Timer timer = new Timer(new TimerCallback((e) =>
{
context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
context.Response.Write("Current Time: " + DateTime.Now.ToString());
signal.Set();
}), null, waitSecond * 1000, 0);
signal.WaitOne();
}
public bool IsReusable
{
get
{
return false;
}
}
}
而用戶端則是 Comet Programming 的 Polling 發動者,我們使用 jQuery 來做這件事:
$(function () {
var waitSeconds = 0;
var polling = false;
window.setInterval(function () {
waitSeconds += 1;
$("#timer").text(waitSeconds);
if (!polling) {
// long polling.
polling = true;
$.get("Time.ashx", null, function (r) {
// set value.
$("#container").html(
$("#container").html() + "<br />" + r +
", period: " + waitSeconds + " sec(s).");
// reset timer.
$("#timer").text("");
waitSeconds = 0;
polling = false;
});
}
}, 1000);
});
而執行結果是:
由這簡短的實驗可以看的出來 Comet Programming 的潛在實力,配合這個架構,再加上 Load Balancing 與輕量化服務的開發,那麼在 Web 應用程式上加上像 Facebook 或 Plurk 這樣的即時通知能力就再也不是夢想。
PS: 未來 Websocket 的架構可能會比 Comet Programming 更出色,但 Websocket 還有瀏覽器支援的問題,因此短期內,至少在 Websocket 成熟前,Comet Programming 還會在主流地位上一段時間。
Reference:
http://en.wikipedia.org/wiki/Comet_(programming)