使用nodejs搭建最简单的comet原型
发布于 14 年前 作者 duzhigang 15606 次浏览 最后一次编辑是 8 年前

<h3>什么是Comet</h3> <br/>Comet, 据<a href=“http://www.ibm.com/developerworks/cn/web/wa-lo-comet/”>IBM这篇文章</a>介绍,是<em>基于HTTP长连接的“服务器推”技术</em>. 和AJAX类似, 这是一种改善WEB用户体验的通讯技术. 其实早在CGI盛行的时代, 有种叫做"Server-Push"的技术, 和Comet本质是一回事, 都是基于长连接来实现. Server-Push更具体, 强调使用multipart/x-mixed-replace的Conent-Type技巧, 使得服务器能替换浏览器的内容. Comet包含面更广泛, 只要是有长连接和HTTP chunked的实现, 都算作其中. <a href=“http://www.josephj.com/entry.php?id=358”>这篇文章</a>详细介绍了Comet的各种形态,值得一读. <br/> <br/>Comet虽然能让浏览器达到及时的响应, 但是由于基于长连接实现, 服务器成本很高. 最近这种技术之所以火起来, 主要还是牛人们探索到了各种降低服务器成本的方法. <a href=“http://amix.dk/blog/label/25#AJAX-and-comet”>这个叫amix的家伙</a>对此有较多的研究. <br/><h3>什么是NodeJs</h3> <br/><a href=“http://nodejs.org/”>nodejs</a>号称Evented I/O for V8 JavaScript, 是基于<a href=“http://code.google.com/p/v8/”>V8</a>的一款神器, 让我们可以使用javascript轻松进行服务器端编程. <br/><h3>最简单的Comet原型</h3> <br/>我用一下午的时间, 使用nodejs搞了一个简单的不能再简单的Comet原型. 在这个demo里面, 我假定使用iframe实现Comet, 但是忽略了iframe的父窗口和客户端js库, 只考虑服务器如何将HTTP chunked push到客户端. <br/> <br/>我定义了一种Comet资源: http://{host}/{pathname}?[{query_string}] . 其中{pathname}直接当作客户端id来使用(在程序里面它被叫做resid). {query_string}用来做消息内容. 这样, 原型就简化成了两种操作: <br/><ul> <br/> <li>HTTP GET : <em>http://{host}/{pathname}</em> 用来模拟iframe长连接, 不断接收到新数据.</li> <br/> <li>HTTP PUT : <em>http://{host}/{pathname}?{query_string} </em> 用来模拟业务操作, 直接将{query_string}当作数据投递到上面的长连接里面.</li> <br/></ul> <br/><h3>具体实现</h3> <br/>好, 主角登场, 用nodejs实现最简单的Comet: <br/><pre escaped=“true” lang=“javascript” line=“1”>global.messages = { <br/> //‘resid’:[] <br/>}; <br/> <br/>var char500 = (function(){ var i=0; var arr = []; for(i=0; i<500; i++) { arr.push( ’ ’ ); } return arr.join(’’); })(); <br/> <br/>var http_method_funs = { <br/> ‘GET’: function(resid, data, request, response) { <br/> if(global.messages[resid] == undefined) { <br/> global.messages[resid] = []; <br/> } <br/> <br/> response.writeHead(200, {‘content-type’: ‘text/plain’}); <br/> <br/> var interval = setInterval(myoutput, 500 ); <br/> response.connection.on(‘end’, function(){ <br/> console.log(“GET\t” + resid + “\tclosed”); <br/> clearInterval(interval); <br/> }); <br/> <br/> myoutput(); <br/> <br/> function myoutput(){ <br/> <br/> var msgs = global.messages[resid]; <br/> if(msgs.length){ <br/> var str = msgs.join("\n\n\n") + “\n\n\n”; <br/> str = (str.length < 500 ) ? ( str + char500 ) : str; //for MTU <br/> response.write(str); <br/> global.messages[resid] = []; <br/> } <br/> <br/> } <br/> }, <br/> <br/> ‘PUT’: function(resid, data , request, response) { <br/> if(global.messages[resid] == undefined) { <br/> global.messages[resid] = []; <br/> } <br/> <br/> global.messages[resid].push(data); <br/> console.log(global.messages); <br/> <br/> response.writeHead(200, {‘content-type’: ‘text/plain’}); <br/> response.end( ‘ok\n’); <br/> }, <br/>}; <br/>//method function <br/> <br/>require(‘http’).createServer(function (request, response) { <br/> <br/> var urlinfo = require(‘url’).parse(request.url); <br/> var resid = urlinfo[‘pathname’]; <br/> var data = (urlinfo[‘query’]) ? urlinfo[‘query’] : 0 ; <br/> var method = request.method; <br/> <br/> console.log(method + “\t” + resid ); <br/> <br/> if(typeof http_method_funs[method] == ‘function’) { <br/> http_method_funs[method].call(null, resid, data, request, response); <br/> } <br/> else { <br/> response.writeHead(400); <br/> response.end(“unsupport method\n”); <br/> } <br/> <br/> }).listen(18124); <br/>console.log(‘server running at http://127.0.0.1:18124/’);</pre> <br/><h3>测试方法</h3> <br/>上面的代码保存到文件, 我们在第一个终端启动这个服务: <br/><pre escaped=“true”>shell> node hello.js</pre> <br/>我们在第二个终端模拟iframe的数据流. 输入命令, 观察收到的数据: <br/><pre escaped=“true”>telnet 127.0.0.1 18124 <br/>GET /mymessages HTTP/1.1 <br/> <br/>HTTP/1.1 200 OK <br/>content-type: text/plain <br/>Connection: keep-alive <br/>Transfer-Encoding: chunked</pre> <br/>我们在第三个终端输入curl -X PUT命令, 模拟发送两条消息: <br/><pre escaped=“true”>shell> curl -X PUT “http://127.0.0.1:18124/mymessages?a=1&b=2&c=3” <br/>ok <br/>shell> curl -X PUT “http://127.0.0.1:18124/mymessages?a=4&b=5&c=6” <br/>ok</pre> <br/>观察第二个终端, 会发现已经收到两条HTTP chunked. (为了避免测试数据小于MTU, 我实际上多输出了一些空格,但这里省去了.) <br/><pre escaped=“true”>202 <br/>a=1&b=2&c=3 <br/> <br/>202 <br/>a=4&b=5&c=6</pre> <br/><h3>总结</h3> <br/>在这个原型中, 我省掉了Comet iframe方案内无关紧要的东西, 只用HTTP PUT/GET来演示一个最简单的原型. 用NodeJs轻松搭建了它. <br/> <br/>可以看到, 用javascript event的风格写服务器, 简直是明白如话, 散文那样自然. <br/>我用global.messages对象来存储消息, key是resid(上面说的客户端id), value是个array, 里面存储客户端收到的messages. <br/>我为GET/PUT两种操作分别实现了两个函数. <br/>PUT函数, 收到请求就将query_string当作message存到对应resid的array中, 然后断开HTTP连接. <br/>GET函数, 收到请求就启动一个定时器, 轮询global.messages里面自己的消息队列(array). 如果遇到数据则在HTTP response输出http chunked. HTTP连接不主动关闭, 但如果被异常关闭则清除定时器对象. <br/> <br/>就这么一个简单功能, 如果用C和select来开发, 那么一个全局的客户端句柄队列是免不了要实现的, 当io事件到来时, 如何恢复之前中断的上下文,进行正确的io操作, 也是一件头疼的事情. <br/>而我们看这个实现里面的<em>myoutput</em>定时器函数. 由于局部变量<em>resid,response</em>在函数的定义时环境内, 所以函数被执行时, 很自然就使用这些上下文信息. 相比来说, C的实现里面专门为此设计一个客户端句柄队列就太突兀了. <br/> <br/>javascript通过函数式和闭包, 轻而易举的完成了一个非阻塞服务器. 如果说libevent是通过库来实现了事件的封装, 那么nodejs所宣称的"Evented I/O for V8 JavaScript", 则是借语言本身的优雅特性获得自然的收获. <br/> <br/>----我是分割线---- <br/>大家好, 今天我刚参加了NodeParty, 有幸认识了许多大侠, 收获良多(不幸的是迟到了). <br/>早就听说过nodejs, 但这次是第一次拿它做个原型, 分享给大家, 期待共同进步. <br/>我的blog: <a href=“http://www.dulao5.com/”>http://www.dulao5.com/</a> , gmail 和 twitter id: dulao5 .

5 回复

很易懂,可以说nodejs是原生地支持,也可以看看amix的PPT,很不错http://www.slideshare.net/amix3k/comet-with-nodejs-and-v8

这篇文章怎么没有标题呢?

抱歉, 不熟悉这个界面, title漏掉了(居然需要填两次)

函数式编程(函数可以作为参数传递),使用闭包做状态保持,是快速实现异步无阻塞开发的无上利器.

不错的实验。 <br/>不过我在实验时telnet 没有显示,我采用 <br/>curl http://127.0.0.1/mymessages <br/>获得了类似效果。

回到顶部