Browser 與 Server 持續同步的作法介紹 (Polling, Comet, Long Polling, WebSocket)

标签: browser server 同步 | 发表时间:2011-03-17 10:19 | 作者:(author unknown) Cary
出处:http://josephj.com/

對 Comet 的懵懂

記得兩年多前,第一次看到 Gmail 中的 GTalk 覺得很好奇:「咦?線上聊天且是 Google 的熱門系統,只用傳統的 AJAX 應該會操爆伺服器吧?」很幸運的,當時前公司內部的 Tech Talk 就有位同事分享這個叫 Comet 的技術、是種「為了讓瀏覽器與伺服器頻繁溝通所使用的技術、主要的瓶頸在於 WWW 伺服器上。」但因為工作沒有用到這類的需求、加上找不太到好的入門文章、實作的人不多,因此我對 Comet 的認識一直停留在懵懂的階段。

這一年多,會自動更新的網站越來越多,例如 Twitter、Plurk、Facebook 都會隨時有新資料出現在頁面上。也越來越常聽到 nodeJS 這個框架、似乎成為了此類需求的最佳的解決方案。心中的疑問是:「nodeJS 是專門為了實作 Comet 的 Web 伺服器嗎?」(當然不只是這樣 =b)

一頭霧水的實作階段

最近 miiiCasa 需實作一個即時通知的功能:「當有人做了跟我有關聯的動作時(例如:設為聯絡人、上傳照片到我可以存取的設備),立刻會有一則訊息在左下角。」同事分別將 nodeJS 架設起來並做了分享,似乎萬事皆備只欠 Coding。不過真的開始 Coding、尋找文件時就開始混亂了。因先前錯誤的認知,將許多名詞都混在一起: Polling、AJAX Comet、Comet with Iframe、Non-blocking IO、Web Socket、Long Polling、Socket.io 等。而且還發現 nodeJS 的定位跟我想像的差異很大,本來只知道它是一套事件驅動的伺服器端語言、後來才了解它的強大、甚至可寫出不同類型的伺服器(A HTTP Proxy Server in 20 Lines of node.js Code),它的定位對我來說,根本就是另一套不同概念的 Apache + PHP,即時通知只是其中的一種受歡迎的實作罷了。

先把 nodeJS 放一旁吧(畢竟我對它的了解還在幼稚園階段)。這篇文章主要要介紹的是上面提到的混亂名詞,希望用最簡單的實作讓大家了解每個技術的定義、避免混淆在溝通時造成誤解。

1. 老掉牙的輪詢 - Polling

Polling 的範例

輪詢最常見也最簡單:「利用 JavaScript 的 setInterval(),每隔一段時間就對 Server 發送一個 Request 以 JSONP 或 AJAX 的方式取得最新的資料。」例如:每隔 3 分鐘向伺服器問一次,檢查目前登入 Cookie 是否過期。

JavaScript 的部份

每秒鐘從 Server 取得資料:

YUI().use("node-base", "io", function (Y) {
    Y.on("io:complete", function (id, o, args) {
        Y.one("#show").append(o.responseText);
    }); // 將 Server 成功 Response 的資料寫到頁面上。
    Y.later(1000, null, function () {
        Y.io("polling.php");
    }, null, true); // 每 1 秒用 XMLHttpRequest 向  polling.php 發送 Request。
});
polling

PHP 的部份

範例只是輸出亂數,你可以想像這是從 memcache 或資料庫中取出了幾筆資料:

$num = rand(10, 100); 
echo "Server said $num.rn";
exit(); 

優點是非常容易實作、沒有跨瀏覽器的問題、也不需要特殊伺服器做配合。而缺點是沒效率,因多數時間的 Request/Response 的 Header/Content 一致但又不能做 Cache,會因此浪費不必要的頻寬。

2. 舊時代的 Comet - 永不停止的連線

Comet 如同我前面所說,已經有兩三年的歷史了,前端的技術完全無新意可言、後端雖然老舊但會有點 Tricky。Comet 的中文是「彗星」的意思,顧名思義發出的 Request 會像彗星的尾巴一般,拉得很長(一般的 Request 立刻就會結束了)。而這樣做的好處就是可以不結束連線,讓 Server 持續地 Response 資料回 Browser,如此一來就可以解決 Polling 造成頻寬浪費的問題。常見作法有以下兩種:

2-1. 用 AJAX 實作 Comet

AJAX Comet 的範例

首先,必須在 PHP 動些手腳:在 Server 查詢完畢後、利用 flush() 顯示結果再使用 sleep() 暫停執行,依這樣的方式做無窮迴圈。這樣的作法可將 Browser 的 Request 減到最低、但 Server 端仍得用無窮迴圈一直做查詢(可以說是 Server 端的 Polling)。

echo str_repeat(" ", 1024); // 本來我沒辦法產生片段輸出的效果,但先輸出 1024 就可以了,真神奇。
while (TRUE) // 無窮迴圈
{
    $wait = rand(1, 3);
    flush(); // 輸出結果,有人會另外加上 ob_flush()
    $num = rand(10, 100); // 一樣是亂數、可以想成是 ,memcache 或資料庫的查詢。
    echo "Server said $num.rn";
    sleep($wait); // 等待一陣子
}

再看看 JavaScript 的部份,其實就是 XMLHttpRequest(也可以是 Script Tag Hack / JSONP)。因為連線不會結束的關係,我們必須使用 readyState = 3 來對回傳的資料做處理。另外由於 PHP 的 flush() 是一直將 response 增加、不是刷新 response,所以我們必須用 substring 才能取得最新的資料。Hmmm... 有點鳥,但這個問題還算可以接受 :p

var node  = Y.one("#show");
try {
    var request = new XMLHttpRequest(); 
} catch (e) {
    alert("Browser doesn't support window.XMLHttpRequest");
}
var pos = 0; // 記下目前的輸出總長度
request.onreadystatechange = function () {
    if (request.readyState === 3) { // 在 Interactive 模式就得處理
        var text = request.responseText; 
        node.append("

" + text.substring(pos) + "

"); // 用前一次的輸出長度擷取最新的字串 pos = text.length; // 更新總長度 } }; request.open("GET", "comet-ajax.php", true); // 傳統的作法,但因 PHP 的特殊處理讓它不會中斷 request.send(null);
comet-ajax

這個作法有個致命的缺點,就是 IE 沒辦法像 Firefox 或 Chrome 針對 readyState = 3 的資料來做處理。所以... 這個作法的可用性並不高。

2-2 用 Iframe 實作 Comet

範例程式

Iframe 是過去 Comet 中最常見的作法,Server 端的程式幾乎一模一樣,只有輸出的格式改為 HTML、用來輸出一行行的 Inline JavaScript 。由於它一輸出就會執行,就沒有剛剛 XMLHttpRequest 得用 substring 取得最新資料的鳥問題了。重點是每個瀏覽器都可以用,實作起來也相當方便。而此作法的缺點為缺少像是XMLHttpRequest 可利用 readyState 判斷進度、以及 status 判斷連線狀態

PHP 的部份

echo str_repeat(" ", 1024);
while (TRUE)
{
    flush();
    $num = rand(10, 100);
    echo "[script]top.onmessage('Server said $num. ');[/script]";
    sleep(3);
}

JavaScript 的部份

YUI().use("node-base", function (Y) {
    var node  = Y.one("#show"),
        frame = Y.one("iframe");   
    window.callback = function (str) {  // 設定 Iframe 的 Callback 方法
        node.append(str);
    };
    Y.later(10, null, function () { // 只是為了早一點讓 iframe 載入,直接寫 src 太久了
        document.getElementsByTagName("iframe")[0].src = "comet-iframe.php";
    });
});
comet-iframe

Iframe 解決了跨瀏覽器的問題,但所有問題解決了嗎?其實並沒有... 因為 Comet 的這種作法會把將傳統的 Web 伺服器(例如 Apache)的連線給佔住,一個人可能會有多個連線(多個 Tab)、而連線一達到上限卻又沒辦法釋放時,你的網站也就沒辦給更多人使用(IO 被佔滿)。所以 Comet 的技術得配合 Non-Blocking IO 的 Web 伺服器才能運作。另外它也只能由 Server 單方向的供給資料,比起 Polling 每次都可以互動,似乎也是一個麻煩的缺點。像是持續地檢查 Cookie 就沒辦用 Comet 來做、 Polling 才有可能。

3. 改良式 Comet - 長時間的輪詢

範例程式

長時間的輪詢(Long Polling)是 Comet 演化過後的方式、也是目前 Facebook、Plurk 實現動態更新的方法。前面的 Iframe 與 XMLHttpRequest 都屬於「永遠不會斷線」的作法。Long Polling 的作法是發一個長時間等待的 Request、當伺服器有資料 Response 時立刻斷掉、接著再發一個新的 Request

JavaScript 的部份

其實若 Server 沒有支援,它就是一個 Polling 的程式碼(一個結束後再做一個):

YUI().use("jsonp", "node-base", function (Y) {
    var handler = function (response) {
        Y.one("#show").append("[p]" + response.result + "[/p]");
        Y.jsonp("http://comet.josephj.com/?callback={callback}", arguments.callee);
    };
    Y.jsonp("http://comet.josephj.com/?callback={callback}", handler);
});

nodeJS 的部份

因為 Long Polling 是目前的主流,我也用主流的 nodeJS 來寫吧。下面的 setTimeout 只是為了等待的效果,其實在實作時是可以不用對 Server 做 Polling 的,採用其他方式驅動事件才是 nodeJS 的精神。

var http = require("http"),
    url  = require("url"),
    qs   = require("querystring");
httpServer = http.createServer(function (request, response) {
    var callback = qs.parse(url.parse(request.url).query).callback; // 取得 callback GET 參數
    setTimeout(function () { //  3 秒後(只是為了達成等待的效果)就輸出 JSONP 格式,可以想成每一段時間就去 DB 或 memcache 查詢。
        var text = callback + "({'result': 'Server said " + parseInt(new Date().getTime(), 10) + "'});";
        response.write(text);
        response.end(); // 結束連線。
    }, 3000);
    response.writeHead(200, {"Content-Type": "text/javascript"});
}).listen(1387);
long-polling

與 Polling 的不同之處就在於它是比較有效率的、可以等到 timeout 或拿到資料時再重新發、因此減少不必要的流量浪費。另外,跟舊型態的 Comet 比起來,Browser 比較有機會傳遞資料(每次發新的 Request 的時候)。加上沒有瀏覽器相容性的問題,難怪它會成為當今最常見的解法了。

4. 明日之星 - WebSocket

上面所講的幾種方法,除了 Polling 外,全部都有僅單向溝通的問題。HTML5 的 WebSocket 解決了此問題。他利用新的協定建立了雙向的通道:當通道建立起來之後,Browser 可以隨時丟訊息給 Server、Server 可以隨時丟訊息給 Browser。非常地方便好用。唯一的缺點就是當今瀏覽器的支援度不普及(IE9 不支援、Chrome 支援、FF4 未知)

JavaScript 的部份

範例程式(因為 Server 有 Proxy,所以沒辦法順利成功,但在直接連線的環境並且使用 Chrome 是沒問題的)
YUI().use("node-base", function (Y) {
	var node = Y.one("#show");
	var conn = new WebSocket("ws://node.josephj.com/test");
	conn.onopen = function (e) { // 當通道建立完畢時
		Y.later(3000, null, function () { // 每三秒往 Server 塞資料
			conn.send("Browser said " + parseInt(new Date().getTime()) + ".");
		}, null, true);
	};
	conn.onmessage = function (e) { // 當收到 Server 的資料時
		node.append("[p]" + e.data + "[/p]"); // 顯示在頁面上
	};
});

nodeJS 的部份

範例程式

因為 WebSocket 是另外一個協定,我套用了現成的 node-websocket-server 來達成。

var ws   = require(__dirname + "/node-websocket-server/lib/ws/server"),
    server;
server = ws.createServer(); // 建立 WebSocket 伺服器
server.addListener("connection", function (conn) { // 當與 Client 連線順利建立
    conn.addListener("message", function (message) { // 當收到 Client 的連線
        var text = "<" + conn.id + "> " + message + ".";
        conn.send(text); // 將資料送回 Client(製造雙通道的效果)
    });
    setInterval(function () {
        conn.send("Server said " + parseInt(new Date().getTime(), 10) + "."); // 持續的將資料送回 Client
    }, 5000);
});
server.listen(1388);
WebSocket

寫起程式真的直覺多了,不是嗎?另外聽同事說 Socket.io 是完整解決方案,包含前後端函式庫,另外當 Browser 不支援時還有 fallback (應該是恢復使用 Long Polling)。有機會來玩 :D(註:WebSocket disabled in Firefox 4:2010/12 目前 Opera 跟 Firefox 都宣告 WebSocket 是個不安全的 Protocol、暫時無法讓開發者使用、必須修正之後再開放。我在 Chrome 9 是可以順利執行的)

結語

全部想清楚、並且都實作出來,花了我一整天的時間(特別是 WebSocket,因有架 Proxy 導致一直失敗、建議大家在試上面的所有範例時都不要有 Proxy)。唯一的好處就是搞懂 Comet 這個名詞至少代表了三種實作方法、Long Polling 則是其中的一種、也是目前最熱門的。實作的方向仍然不變囉。希望對有興趣使用的朋友有幫助。所有範例都放在 GitHub

推薦連結

相关 [browser server 同步] 推荐:

Browser和Server持续同步的几种方式(jQuery+tornado演示)

- mrluanma - 残阳似血的博客
在B/S模型的Web应用中,客户端常常需要保持和服务器的持续更新. 这种对及时性要求比较高的应用比如:股票价格的查询,实时的商品价格,自动更新的twitter timeline以及基于浏览器的聊天系统(如GTalk)等等. 由于近些年AJAX技术的兴起,也出现了多种实现方式. 本文将对这几种方式进行说明,并用jQuery+tornado进行演示,需要说明的是,如果对tornado不了解也没有任何问题,由于tornado的代码非常清晰且易懂,选择tornado是因为其是一个非阻塞的(Non-blocking IO)异步框架(本文使用2.0版本).

Browser 與 Server 持續同步的作法介紹 (Polling, Comet, Long Polling, WebSocket)

- Cary - 這樣做就對了!
記得兩年多前,第一次看到 Gmail 中的 GTalk 覺得很好奇:「咦. 線上聊天且是 Google 的熱門系統,只用傳統的 AJAX 應該會操爆伺服器吧. 」很幸運的,當時前公司內部的 Tech Talk 就有位同事分享這個叫 Comet 的技術、是種「為了讓瀏覽器與伺服器頻繁溝通所使用的技術、主要的瓶頸在於 WWW 伺服器上.

[小技巧] JavaScript Cross Browser Best Practices

- - 小惡魔 - 電腦技術 - 工作筆記 - AppleBOY
我們來看看 Javascript 的小技巧. 不要再使用 navigator.userAgent. 簡單來說 Canvas 在 IE9 才有支援,所以針對 IE 部份,我們使用 navigator.userAgent 來判斷. 但是如果遇到 Safari, Chrome, Android, IPad, IPhone 版本呢,也很好解決,就是一直些判斷式,那為什麼不換個角度去想,直接判斷有無 Cnavas 功能即可,透過 Modernizr 套件可以簡單做到.

HTML5 Boilerplate 不再支援 legacy browser

- - 小惡魔 - 電腦技術 - 工作筆記 - AppleBOY
HTML5 Boilerplate 在 V5.0 版本將不支援舊有瀏覽器,包含 IE6/7. Firefox 3.6 (Mozilla 已經不再維護) 及 Safari 4,詳細資料可以參考 V5.0 的 Milestone ( Drop legacy browser support) 下一版本會有哪些改變呢.

SQL Server--索引

- - CSDN博客推荐文章
         1,概念:  数据库索引是对数据表中一个或多个列的值进行排序的结构,就像一本书的目录一样,索引提供了在行中快速查询特定行的能力..             2.1优点:  1,大大加快搜索数据的速度,这是引入索引的主要原因..                             2,创建唯一性索引,保证数据库表中每一行数据的唯一性..

SQL Server 面试

- - SQL - 编程语言 - ITeye博客
在SQL语言中,一个SELECT…FROM…WHERE语句称为一个查询块,将一个查询块嵌套在另一个查询块的WHERE子句中的查询称为子查询. 子查询分为嵌套子查询和相关子查询两种. 嵌套子查询的求解方法是由里向外处理,即每个子查询在其上一级查询处理之前求解,子查询的结果作为其父查询的查询条件. 子查询只执行一次,且可以单独执行;.

什么是Server SAN? Server SAN精解

- - 云存储技术网--光头老蒋
Server SAN是现在一个全新的概念,现在给出的概念都太笼统. 按照老蒋的想法:Server San应该是一个利用软件将基于DAS存储(包含闪存卡,直连存储)的集合,做成一个能共享的SAN存储网络.      SAN存储区域网,大家都很熟悉. 普遍使用的就两种: FC SAN;ISCSI SAN.

weblogic server启动慢

- - Linux - 操作系统 - ITeye博客
(1)较好的解决办法: 在Weblogic启动参数里添加 “-.   Djava.security.egd=file:/dev/./urandom” (/dev/urandom 无法启动). 修改Linux上Weblogic使用的jdk $JAVA_HOME/jre/lib/security/java.security 文件.

Server-Sent Events 教程

- - 阮一峰的网络日志
服务器向浏览器推送信息,除了 WebSocket,还有一种方法:Server-Sent Events(以下简称 SSE). 严格地说, HTTP 协议无法做到服务器主动推送信息. 但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming). 也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来.

译|High-Performance Server Architecture

- - 掘金 架构
本文的目的是分享我多年来关于如何开发某种应用程序的一些想法,对于这种应用程序,术语“服务”只是一个无力的近似称呼. 更准确地说,将写的与一大类程序有关,这些程序旨每秒处理大量离散的消息或请求. 网络服务通常最适合此定义,但从某种意义上讲,实际上并非所有的程序都是服务. 但是,由于“高性能请求处理程序”是很糟糕的标题,为简单起见,倒不如叫“服务”万事大吉.