如果英文ok的話,可以直接看這篇文章
實現即時通訊一般有兩種方式:
socket或comet。socket是比較好的解決方案,問題在於不是所有的瀏覽器都相容,伺服器端實現起來也稍微有點麻煩。相比之下,comet(基於HTTP長串連的"伺服器推")實現起來更加方便,而且相容所有的瀏覽器。所以這次就來說說comet的php實現。
comet也有好幾種實現方式,如iframe, http long request,二者的區別可以參考這篇文章。本文主要探討http long request實現即時通訊。
先說說http長連結是怎麼回事,通俗點講就是伺服器不是一收到請求就直接吐資料,而是在那憋啊憋,一直憋到憋不住了,才告訴你執行結果。
<?php$count = 10;for($i=0; $i<$count; $i++){// do something ...sleep(2);}echo '憋死我了';
至於憋多長時間,就看具體應用了,如果憋太久的話,伺服器資源的佔用也會是個問題。
現在我們就要通過這種方法來實現即時通訊(其實是准即時),先說一下原理:
1. 用戶端發起一個ajax長連結查詢,然後服務端就開始執行代碼,主要是檢查某個檔案是否被更新,如果沒有,睡一會(sleep),醒來接著檢查
2. 如果用戶端又發起了一個查詢連結(正常請求),服務端收到後,處理請求,處理完畢後更新某個特定檔案的modify time
3. 這時第一次ajax查詢的後台代碼還在執行,發現某個檔案被更新,說明來了新請求,輸出對應的結果
4. 第一次ajax查詢的callback被觸發,更新頁面,然後再發起一個新的ajax長連結
實戰用戶端
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Comet Test</title> </head> <body> <p><a class='customAlert' href="#">publish customAlert</a></p> <p><a class='customAlert2' href="#">publish customAlert2</a></p> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js" type="text/javascript"></script> <script src="NovComet.js" type="text/javascript"></script> <script type="text/javascript">NovComet.subscribe('customAlert', function(data){console.log('customAlert');//console.log(data);}).subscribe('customAlert2', function(data){console.log('customAlert2');//console.log(data);});$(document).ready(function() {$("a.customAlert").click(function(event) {NovComet.publish('customAlert');});$("a.customAlert2").click(function(event) {NovComet.publish('customAlert2');});NovComet.run();}); </script> </body></html>
這段代碼說的是,有個NovComet的Object,註冊了customAlert和customAlert2事件,當頁面載入完成時,對兩個按鈕又加了監聽事件,當點擊時NovComet會發布customAlert或customAlert2事件,然後NovComet執行了run方法。
NovComet
//NovComet.jsNovComet = { sleepTime: 1000, _subscribed: {}, _timeout: undefined, _baseurl: "comet.php", _args: '', _urlParam: 'subscribed', subscribe: function(id, callback) { NovComet._subscribed[id] = { cbk: callback, timestamp: NovComet._getCurrentTimestamp() }; return NovComet; }, _refresh: function() { NovComet._timeout = setTimeout(function() { NovComet.run() }, NovComet.sleepTime); }, init: function(baseurl) { if (baseurl!=undefined) { NovComet._baseurl = baseurl; } }, _getCurrentTimestamp: function() { return Math.round(new Date().getTime() / 1000); }, run: function() { var cometCheckUrl = NovComet._baseurl + '?' + NovComet._args; for (var id in NovComet._subscribed) { var currentTimestamp = NovComet._subscribed[id]['timestamp']; cometCheckUrl += '&' + NovComet._urlParam+ '[' + id + ']=' + currentTimestamp; } cometCheckUrl += '&' + NovComet._getCurrentTimestamp(); $.getJSON(cometCheckUrl, function(data){ switch(data.s) { case 0: // sin cambios NovComet._refresh(); break; case 1: // trigger for (var id in data['k']) { NovComet._subscribed[id]['timestamp'] = data['k'][id]; NovComet._subscribed[id].cbk(data.k); } NovComet._refresh(); break; } }); }, publish: function(id) { var cometPublishUrl = NovComet._baseurl + '?' + NovComet._args; cometPublishUrl += '&publish=' + id; $.getJSON(cometPublishUrl); }};
NovComet的run方法首先把之前註冊的幾個事件串成一個url,並且很狡猾地使用了"[]",類似:?subscribed[customAlert]=1300016814&subscribed[customAlert2]=1300016814&1300016825,這樣php收到後,就會得到$_GET[subscribed]數組,最後那個時間戳記是為了避免請求被緩衝。如果收到後台傳過來的資料data的s值為0,說明什麼也沒發生,隔1秒後繼續執行;如果data.s的值為1,說明NovComet的publish事件被觸發,則調用對應的callback。publish方法執行後,會構造一個類似:
?publish=customAlert 這樣一個url發送到後台。後台檢測到pubish參數,則擷取該參數的值,並更新對應檔案的mtime。
服務端
<?php// comet.phpinclude('NovComet.php');$comet = new NovComet();$publish = filter_input(INPUT_GET, 'publish', FILTER_SANITIZE_STRING);if ($publish != '') { echo $comet->publish($publish);} else { foreach (filter_var_array($_GET['subscribed'], FILTER_SANITIZE_NUMBER_INT) as $key => $value) { $comet->setVar($key, $value); } echo $comet->run();}
如果收到publish參數,直接輸出,否則執行run方法,至於run是怎麼回事,且看下碼。
<?php// NovComet.phpclass NovComet { const COMET_OK = 0; const COMET_CHANGED = 1; private $_tries; private $_var; private $_sleep; private $_ids = array(); private $_callback = null; public function __construct($tries = 20, $sleep = 2) { $this->_tries = $tries; $this->_sleep = $sleep; } public function setVar($key, $value) { $this->_vars[$key] = $value; } public function setTries($tries) { $this->_tries = $tries; } public function setSleepTime($sleep) { $this->_sleep = $sleep; } public function setCallbackCheck($callback) { $this->_callback = $callback; } const DEFAULT_COMET_PATH = "/dev/shm/%s.comet"; public function run() { if (is_null($this->_callback)) { $defaultCometPAth = self::DEFAULT_COMET_PATH; $callback = function($id) use ($defaultCometPAth) { $cometFile = sprintf($defaultCometPAth, $id); return (is_file($cometFile)) ? filemtime($cometFile) : 0; }; } else { $callback = $this->_callback; } for ($i = 0; $i < $this->_tries; $i++) { foreach ($this->_vars as $id => $timestamp) { if ((integer) $timestamp == 0) { $timestamp = time(); } $fileTimestamp = $callback($id); if ($fileTimestamp > $timestamp) { $out[$id] = $fileTimestamp; } clearstatcache(); } if (count($out) > 0) { return json_encode(array('s' => self::COMET_CHANGED, 'k' => $out)); } sleep($this->_sleep); } return json_encode(array('s' => self::COMET_OK)); } public function publish($id) { return json_encode(touch(sprintf(self::DEFAULT_COMET_PATH, $id))); }}
可以看到publish時,建立了一個以$id命名的檔案。run時,如果發現該$id檔案存在,且時間戳記大於之前儲存的該$id對應的時間戳記(通過setVar設定的),說明$id事件被觸發,處理完後把$id放到$out數組中,然後判斷一下$out數組是否為空白,如果不為空白,則輸出一段json。
如果一段時間內都沒有觸發事件(for迴圈執行完畢),也輸出一段json,告訴前端執行完了,但是沒有任何新情況。
說明
- 可以在用戶端監聽/發布多個事件
- 監聽事件時,可以傳一個callback,這樣收到資料時就會出發該callback
- 當監聽事件時,會傳一個時間戳記
- 當事件被publish時,會向後台發一個請求,並傳遞一個新的時間戳記
- 服務端不會一直執行,如果指定時間內,沒有任何請求被觸發,則結束運行
- 用戶端會重複上述過程(setTimeout & NovComet.run())
最後來一張圖說明一下
1. 運行一段時間後,沒有收到任何publish事件,服務端結束執行
2. 服務端返回一段json
3. 用戶端觸發了一個事件,服務端收到事件,返回一段新的json
4. callback被觸發
5. 用戶端進入下一次的ajax長連結查詢
轉自:http://blog.leezhong.com/tech/2011/03/21/php-comet.html