[anti-both]
筆 者 老 是 覺 得 世 界 上 是 沒 有 push 這 種 東 西 的 ,頂 多 就 是 你 pull 得 很 利 害 很 有 技 巧 幾 乎 沒 有 人 察 覺 而 已 。
所 謂 的 push 和 pull 是 什 麼 呢 ?在 一 個 client / server 的 環 境 ,特 別 是 互 聯 網 的 環 境 ,一 般 都 是 client 向 server 發 出 一 個 request,然 後 server 就 傳 回 client 要 求 的 資 料 ,這 就 是 所 謂 的 pull。
而 push 呢 ?理 論 上 就 是 server 可 以 向 client 直 接 發 送 資 料 ,而 不 需 要 client 先 發 request。
最 初 ,所 謂 的 push 就 是 在 client 方 面 不 停 的 向 server 發 出 request。例 如 用 javascript 的 setInterval()去 發 出 一 個 ajax 的 查 詢 ,再 把 傳 回 的 資 料 顯 示 給 使 用 者 。這 就 是 所 謂 的 polling 了 。
較 舊 的 polling 的 interval 是 在 client 那 裡 設 定 的 ,如 果 設 定 得 太 頻 密 ,那 server 端 就 會 很 忙 碌 。試 想 像 用 一 秒 一 次 這 個 頻 率 吧 ,如 果 網 站 上 有 一 百 位 用 家 ,那 server 每 秒 就 會 收 到 一 百 個 request。但 這 個 頻 率 如 果 設 定 得 太 低 的 話 ,應 用 程 式 又 會 不 夠 實 時 (real time)。比 如 說 每 三 秒 更 新 一 次 ,如 果 應 用 程 式 是 下 棋 的 話 ,玩 家 每 完 成 一 步 ,對 手 都 要 等 三 秒 才 開 始 ,就 實 在 是 太 lag (慢 )了 。
在 上 面 的 polling,每 一 次 查 詢 都 是 完 整 的 ,完 成 之 後 都 會 斷 開 連 線 。然 後 又 有 人 想 到 ,如 果 不 斷 開 連 線 ,由 server 端 不 停 的 傳 回 最 新 的 資 料 ,那 就 可 以 減 少 request 的 次 數 了 。這 個 方 法 就 像 是 在 server 端 做 polling。這 樣 做 令 到 request 減 少 了 ,但 同 時 的 應 用 程 式 又 會 比 較 實 時 。
但 上 面 的 兩 個 polling 方 法 ,都 無 可 避 免 地 重 覆 傳 送 相 同 的 數 據 ,特 別 數 據 變 更 得 不 太 頻 密 的 時 候 。這 實 在 是 白 白 浪 費 了 頻 寬 ,費 時 失 事 。
針 對 這 個 問 題 ,又 有 聰 明 的 人 士 想 到 了 long polling 的 解 決 辦 法 。long polling 的 做 法 是 先 由 client 向 server 發 出 一 個 查 詢 ,然 後 server 只 在 有 須 要 時 傳 回 資 料 ,如 果 沒 有 須 要 就 等 待 ,等 到 有 須 要 時 再 傳 回 資 料 。
在 client 端 來 看 ,我 發 出 一 個 查 詢 ,可 能 馬 上 有 資 料 傳 回 來 ,也 可 能 三 分 鐘 後 才 有 資 料 傳 回 來 ,但 傳 回 來 的 資 料 都 是 實 時 ,又 有 用 的 ,而 不 是 重 覆 又 重 覆 的 無 用 資 料 。在 server 端 來 看 ,這 個 作 法 既 減 少 了 request,又 實 保 了 實 時 ,也 減 省 了 頻 寬 。
讓 我 舉 一 個 超 簡 化 了 的 回 合 制 遊 戲 例 子 ,來 說 明 一 下 long polling 是 如 何 達 成 的 。long polling 並 不 是 火 箭 科 學 ,基 本 上 任 一 個 web server 加 上 任 何 一 個 web browser 都 可 以 實 作 成 功 。
<html> <head> </head> <body> <div id="active"></div> <input type="button" value="Next Player" id="nextplayer" > </body> </html>
上 例 中 ,我 們 用 一 個 div 來 顯 示 自 己 是 不 是 active player。然 後 有 一 個 button 用 來 完 成 自 的 回 合 ,把 主 動 權 交 到 下 一 個 玩 家 。
$(document).ready(function(){ var userid = 0; var active = 0; checkuser(); $('#nextplayer').click(function(){ if (active==1) { $.ajax({ url:"nextplayer.php?userid=" + userid, async: true, dataType: "json", timeout: 10000, complete: function(){ active=0; } }); } }); poll(); function poll(){ $.ajax({ url:"data_json.php?userid=" + userid + "&active=" + active, async: true, dataType: "json", timeout: 1000000, success: function(data){ // update data if (data.active=='1') { $('#active').text("Active"); active = 1; } else { $('#active').text("Inactive"); active = 0; } }, complete: function() { setTimeout( poll, /* Request next message */ 1000 /* ..after 1 seconds */ ); } }); } checkuser(){ $.ajax({ url:"checkuser.php" async: true, dataType: "json", timeout: 10000, success: function(data){ $userid = data.newuserid; } }); } });
然 後 是 簡 化 了 的 javascript 的 部 分 ,我 用 了 jquery 的 ajax 來 達 成 。在 上 例 中 ,首 先 我 們 一 開 始 就 checkuser() 一 次 ,是 用 來 決 定 自 己 的 userid 的 ,這 部 分 簡 化 了 之 後 如 果 是 ,如 果 你 是 第 一 個 打 開 網 頁 的 ,你 的 userid 就 是 1,第 二 個 ,就 是 2,如 此 類 推 。
然 後 如 果 你 是 active player(就 是 active==1),那 你 就 可 以 按 一 下 Next Player,把 主 動 權 交 給 下 一 個 玩 家 。
因 為 要 在 server 端 才 能 確 定 你 的 userid 和 次 序 ,所 以 兩 者 都 必 須 要 在 server 端 j做 才 成 。完 成 工 作 的 就 是 checkuser.php 和 nextuser.php。這 部 分 應 該 沒 有 難 度 吧 ?我 們 就 直 接 跳 過 了 ,把 精 神 集 中 在 long polling 的 實 作 。
在 上 面 的 jquery 中 ,poll() 函 數 就 是 實 現 long polling 的 client 端 部 分 。我 們 向 data_json.php 提 出 查 詢 ,然 後 按 傳 回 的 結 果 設 定 自 己 究 竟 是 active 還 是 inactive。如 果 是 active 的 ,我 們 就 把 active 寫 在 div 裡 面 。如 果 不 是 ,就 寫 上 inactive。完 成 的 話 就 等 一 秒 ,再 poll 另 一 次 (setTimeout(poll,1000))。
比 較 要 注 意 的 ,就 是 我 在 poll 函 數 裡 面 ,除 了 傳 遞 了 自 己 的 userid,還 傳 遞 了 自 己 的 active status。這 兩 者 都 是 必 須 要 的 。
然 而 我 們 看 看 server 端 的 部 分 ,我 這 裡 是 用 php 實 作 的 。
<?php if (isset($_GET['userid']) and isset($_GET['active'])){ $userid = $_GET['userid']; $client_active = $_GET['active']; openConnection(); for ($i = 0, $timeout = 180; $i < $timeout; $i++ ) { $SQL = "SELECT active FROM table WHERE user='$userid';"; $result = mysql_query($SQL) or die("Couldn t execute query.".mysql_error()); $row = mysql_fetch_array($result); $active = $row[active]; if ($active<>$client_active) { $responce->active = $active; echo json_encode($responce); flush(); exit(0); } usleep(1000000); } $responce->active = $active; echo json_encode($responce); flush(); } ?>
在 這 個 超 簡 化 了 的 php 中 ,我 們 先 檢 查 有 沒 有 傳 來 userid 和 active,如 果 有 的 話 ,我 們 就 在 資 料 庫 檢 查 user 現 在 的 active 值 。有 了 現 在 的 active 值 ,再 對 比 client 傳 來 的 值 ,如 果 是 一 樣 的 話 ,就 是 沒 有 變 化 ,於 是 就 不 用 傳 回 資 料 ,先 等 一 秒 ,再 作 下 一 次 的 檢 查 。
直 到 active 的 值 不 一 樣 ($active<>$client_active),我 們 就 馬 上 傳 回 最 新 的 資 料 。在 實 際 的 應 用 上 ,整 個 網 路 的 各 個 部 分 都 會 有 自 己 的 timeout 的 ,所 以 一 個 request 不 可 能 無 限 地 延 續 下 去 。在 上 例 中 ,我 設 定 為 最 多 查 詢 180 次 ,按 每 一 秒 鐘 查 詢 一 次 算 ,一 共 就 是 3 分 鐘 。就 是 說 ,即 使 三 分 鐘 都 還 沒 有 變 化 ,我 還 是 會 傳 回 一 次 結 果 的 。然 後 再 由 client 端 發 送 另 一 次 request。
筆 者 舉 的 這 個 例 子 ,在 最 一 般 的 browser 和 最 一 般 的 web server 就 可 以 成 功 執 行 ,完 全 是 使 用 現 有 最 根 本 的 技 術 。你 完 全 不 需 要 在 client 和 server 端 安 裝 任 何 額 外 的 東 西 。據 了 解 ,html 5 已 經 內 建 了 push 的 技 術 ,但 因 為 安 全 問 題 ,大 部 分 的 browser 還 沒 有 支 援 html 5 的 push。看 來 ,真 真 正 正 的 push 還 是 得 多 等 一 會 ,哈 哈 。