使用Node.js搭建最簡單的comet原型
什么是Comet
Comet, 據(jù)IBM這篇文章介紹,是基于HTTP長連接的“服務(wù)器推”技術(shù). 和AJAX類似, 這是一種改善WEB用戶體驗(yàn)的通訊技術(shù). 其實(shí)早在CGI盛行的時(shí)代, 有種叫做"Server-Push"的技術(shù), 和Comet本質(zhì)是一回事, 都是基于長連接來實(shí)現(xiàn). Server-Push更具體, 強(qiáng)調(diào)使用multipart/x-mixed-replace的Conent-Type技巧, 使得服務(wù)器能替換瀏覽器的內(nèi)容. Comet包含面更廣泛, 只要是有長連接和HTTP chunked的實(shí)現(xiàn), 都算作其中. 這篇文章詳細(xì)介紹了Comet的各種形態(tài),值得一讀.
51CTO推薦專題:Node.js專區(qū)
Comet雖然能讓瀏覽器達(dá)到及時(shí)的響應(yīng), 但是由于基于長連接實(shí)現(xiàn), 服務(wù)器成本很高. 最近這種技術(shù)之所以火起來, 主要還是牛人們探索到了各種降低服務(wù)器成本的方法. 這個(gè)叫amix的家伙對此有較多的研究.
nodejs號稱Evented I/O for V8 JavaScript, 是基于V8的一款神器, 讓我們可以使用javascript輕松進(jìn)行服務(wù)器端編程.
最簡單的Comet原型
我用一下午的時(shí)間, 使用nodejs搞了一個(gè)簡單的不能再簡單的Comet原型. 在這個(gè)demo里面, 我假定使用iframe實(shí)現(xiàn)Comet, 但是忽略了iframe的父窗口和客戶端js庫, 只考慮服務(wù)器如何將HTTP chunked push到客戶端.
我定義了一種Comet資源: http://{host}/{pathname}?[{query_string}] . 其中{pathname}直接當(dāng)作客戶端id來使用(在程序里面它被叫做resid). {query_string}用來做消息內(nèi)容. 這樣, 原型就簡化成了兩種操作:
HTTP GET : http://{host}/{pathname} 用來模擬iframe長連接, 不斷接收到新數(shù)據(jù).
HTTP PUT : http://{host}/{pathname}?{query_string} 用來模擬業(yè)務(wù)操作, 直接將{query_string}當(dāng)作數(shù)據(jù)投遞到上面的長連接里面.
具體實(shí)現(xiàn)
好, 主角登場, 用nodejs實(shí)現(xiàn)最簡單的Comet:
- global.messages = {
 - //'resid':[]
 - };
 - var char500 = (function(){ var i=0; var arr = []; for(i=0; i<500; i++) { arr.push( ' ' ); } return arr.join(''); })();
 - var http_method_funs = {
 - 'GET': function(resid, data, request, response) {
 - if(global.messages[resid] == undefined) {
 - global.messages[resid] = [];
 - }
 - response.writeHead(200, {'content-type': 'text/plain'});
 - var interval = setInterval(myoutput, 500 );
 - response.connection.on('end', function(){
 - console.log("GET\t" + resid + "\tclosed");
 - clearInterval(interval);
 - });
 - myoutput();
 - function myoutput(){
 - var msgs = global.messages[resid];
 - if(msgs.length){
 - var str = msgs.join("\n\n\n") + "\n\n\n";
 - str = (str.length < 500 ) ? ( str + char500 ) : str; //for MTU
 - response.write(str);
 - global.messages[resid] = [];
 - }
 - }
 - },
 - 'PUT': function(resid, data , request, response) {
 - if(global.messages[resid] == undefined) {
 - global.messages[resid] = [];
 - }
 - global.messages[resid].push(data);
 - console.log(global.messages);
 - response.writeHead(200, {'content-type': 'text/plain'});
 - response.end( 'ok\n');
 - },
 - };
 - //method function
 - require('http').createServer(function (request, response) {
 - var urlinfo = require('url').parse(request.url);
 - var resid = urlinfo['pathname'];
 - var data = (urlinfo['query']) ? urlinfo['query'] : 0 ;
 - var method = request.method;
 - console.log(method + "\t" + resid );
 - if(typeof http_method_funs[method] == 'function') {
 - http_method_funs[method].call(null, resid, data, request, response);
 - }
 - else {
 - response.writeHead(400);
 - response.end("unsupport method\n");
 - }
 - }).listen(18124);
 - console.log('server running at http://127.0.0.1:18124/');
 
測試方法
上面的代碼保存到文件, 我們在第一個(gè)終端啟動這個(gè)服務(wù):
- shell> node hello.js
 
我們在第二個(gè)終端模擬iframe的數(shù)據(jù)流.輸入命令, 觀察收到的數(shù)據(jù):
- telnet 127.0.0.1 18124
 - GET /mymessages HTTP/1.1
 - HTTP/1.1 200 OK
 - content-type: text/plain
 - Connection: keep-alive
 - Transfer-Encoding: chunked
 
我們在第三個(gè)終端輸入curl -X PUT命令, 模擬發(fā)送兩條消息:
- shell> curl -X PUT "http://127.0.0.1:18124/mymessages?a=1&b=2&c=3"
 - ok
 - shell> curl -X PUT "http://127.0.0.1:18124/mymessages?a=4&b=5&c=6"
 - ok
 
觀察第二個(gè)終端, 會發(fā)現(xiàn)已經(jīng)收到兩條HTTP chunked. (為了避免測試數(shù)據(jù)小于MTU, 我實(shí)際上多輸出了一些空格,但這里省去了.)
- 202
 - a=1&b=2&c=3
 - 202
 - a=4&b=5&c=6
 
總結(jié)
在這個(gè)原型中, 我省掉了Comet iframe方案內(nèi)無關(guān)緊要的東西, 只用HTTP PUT/GET來演示一個(gè)最簡單的原型. 用NodeJs輕松搭建了它.
可以看到, 用javascript event的風(fēng)格寫服務(wù)器, 簡直是明白如話, 散文那樣自然.
我用global.messages對象來存儲消息, key是resid(上面說的客戶端id), value是個(gè)array, 里面存儲客戶端收到的messages.
我為GET/PUT兩種操作分別實(shí)現(xiàn)了兩個(gè)函數(shù).
PUT函數(shù), 收到請求就將query_string當(dāng)作message存到對應(yīng)resid的array中, 然后斷開HTTP連接.
GET函數(shù), 收到請求就啟動一個(gè)定時(shí)器, 輪詢global.messages里面自己的消息隊(duì)列(array). 如果遇到數(shù)據(jù)則在HTTP response輸出http chunked. HTTP連接不主動關(guān)閉, 但如果被異常關(guān)閉則清除定時(shí)器對象.
就這么一個(gè)簡單功能, 如果用C和select來開發(fā), 那么一個(gè)全局的客戶端句柄隊(duì)列是免不了要實(shí)現(xiàn)的, 當(dāng)io事件到來時(shí), 如何恢復(fù)之前中斷的上下文,進(jìn)行正確的io操作, 也是一件頭疼的事情.
而我們看這個(gè)實(shí)現(xiàn)里面的myoutput定時(shí)器函數(shù). 由于局部變量resid,response在函數(shù)的定義時(shí)環(huán)境內(nèi), 所以函數(shù)被執(zhí)行時(shí), 很自然就使用這些上下文信息. 相比來說, C的實(shí)現(xiàn)里面專門為此設(shè)計(jì)一個(gè)客戶端句柄隊(duì)列就太突兀了.
javascript通過函數(shù)式和閉包, 輕而易舉的完成了一個(gè)非阻塞服務(wù)器. 如果說libevent是通過庫來實(shí)現(xiàn)了事件的封裝, 那么nodejs所宣稱的"Evented I/O for V8 JavaScript", 則是借語言本身的優(yōu)雅特性獲得自然的收獲.
原文:http://club.cnodejs.org/topic/4f16442ccae1f4aa27001115
【編輯推薦】















 
 
 






 
 
 
 