Web 技术研究所

我一直坚信着,Web 将会成为未来应用程序的主流

NodeJS简易聊天室(服务器端)

  之前的文章一直都在说理论知识,如果不拿出点实例的话有些人就会提不起兴趣吧?所以这回就用这些东西实现一个简易的聊天室。这个聊天室程序的Web通信我使用了最受欢迎的Comet方式长轮询,另外还使用了短连接。来做客户端发送数据的方式。
  现在从代码开始说吧,虽然我都已经注释好了。
//载入一些必要的对象
var querystring,url,fs;
querystring=require('querystring');
url=require('url');
fs=require('fs');
//全局变量
var pool=[];//连接池
var queue=[];//消息队列
var ETag;//最新消息标识符
  这是载入一些程序中需要使用到的对象和定义一些全局变量。这里的全局变量的概念和PHP是不同的,PHP的所谓“全局变量”的作用域只是一个请求或者说一个页面,而这里定义的全局变量是针对整个服务器上所有连接的。现在定义了三个全局变量,我们一个个解释。
  pool是连接池,客户端的长轮询请求如果没能在服务器上找到相应的消息发送回去它就必须在服务器上等待。而服务器是一对多的,是可以接收很多个客户端的长轮询请求。如果他们都只是普普通通的等待服务器就没办法同时管理这么多个等待的请求,于是我们使用一个连接池来统一管理这些连接。
  queue是消息队列,相当于聊天记录。客户端每次发送消息到服务器都会被放入消息队列中。因为长轮询的连接在接收到数据后是需要暂时断开的,如果服务器在这个断开的期间接收到消息,客户端就会收不到。所以必须使用一个消息队列来保存。当客户端访问服务器时候附带一个标识符,来检测它是不是访问过最新的消息,如果不是最新则从消息队列中取得自己漏过的消息,如果已经是最新的就进入连接池中等待。
  ETag就是刚才说的客户端用来判断最新消息的标识符。ETag本身是作为缓存标识符来使用的,可以看之前的文章“使用ETag缓存”,这里我只是借用了它。因为这个概念和ETag非常相识。只是把缓存的返回304改成了进入连接池等待而已。也许有人会问,这个判断消息是否为最新的标识符是一个时间,为什么不用LastModified呢?这是因为LastModified只能精确到秒,而我们聊天的时候一秒发送多条消息是很常见的。不过这个程序中确实可以用LastModified,只是我偏爱ETag而已。这个等明天说道前端代码时候我会再做解释。
  说完这个三个全局变量,也基本把整个程序的核心思路理了一遍,看下面的代码就简单了。 //在8000端口开启HTTP监听
require('http').createServer(function(req,res){
  //解析URL
  var u=url.parse(req.url);
  switch(u.pathname){
    case '/'://服务器主页被访问时加载页面
      fs.readFile('index.html',function(e,s){
        //输出一个包含客户端自己IP的Cookie
        var ip=req.connection.remoteAddress;
        res.setHeader("Set-Cookie","ip="+ip);
        //输出页面数据
        res.setHeader("Content-Type","text/html");
        res.end(s);
      });
      break;
    //定义两个接口让前端调用
    case '/receive':return onPolling(req,res);
    case '/send':return onReceive(req,res);
    //其它请求返回404
    default:
      res.statusCode=404;
      res.end('Not Found');
  };
}).listen(8000);
  这个代码是NodeJS开启一个HTTP监听端口。createServer的参数是客户端请求事件的回调函数。这是NodeJS基础的东西,我就不说了。可以去看看NodeJS官方文档的HTTP部分。这段代码虽然挺长的,但是没啥好说的。先是解析客户端访问服务器时的URL,如果是“/”也就是访问首页,服务器就去读文件“index.html”发送给客户端。这里还在发送给客户端的Cookie中加入一个客户端自己的IP地址,这个在前端的代码中会用到,现在就不做解释了。如果客户端访问的不是主页,而是“/receive”或“/send”这两个页面,就调用后面定义的函数做相应的处理。这个两个页面是前端通过Ajax调用的,我们现在关心的是这两个页面或者说两个接口中调用的函数,这会在后面给出。这个switch语句还有个default,也就是客户端访问除了之前提到的三个页面之外的页面时的动作,我这里定义为返回404。这样这个代码就搞定了。接着是这两个接口处理函数,我们先来看onPolling。 //当收到客户端长轮询请求时调用
function onPolling(req,res){
  var e,i,o,s;
  //获取客户端传过来的ETag
  e=req.headers['if-none-match']*1;
  //判断ETag是否是最新的
  if(queue.length==0||e==ETag){
    //把当前的连接放入连接池中等待
    pool.push(function(e){
      res.setHeader("ETag",ETag);
      res.end("["+e+"]");
    });
  }else{
    //把消息队列中最新的数据输出到客户端
    s=[],i=queue.length;
    while(o=queue[--i])o[0]==e?(i=0):s.push(o[1]);
    //更新ETag
    res.setHeader("ETag",ETag);
    res.end("["+s.join(",")+"]");
  };
};
  这个函数是客户端的长轮询请求发起时候被调用的。其实也没啥好说的,我已经注释的很清楚了,需要说的应该只有放入连接池的那一块代码。我并没有把一整个对象都放入连接池,因为那样麻烦,也没必要。我只是定义了个回调函数放入连接池。这就意味着,所谓的“连接池”我存放的并不是连接,而是处理连接的函数。JavaScript有个特性,函数内部可以使用函数在被定义时的局部变量。这样存放回调函数就很方便。因为这个回调函数是临时定义的,定义时的局部变量就是真连接本身的对象。所以在这个回调函数被调用时候就可以直接访问到连接自身的对象。这个做法也就JavaScript上可行了,要是PHP就只能自己老老实实的把连接对象放入池中,要是C++要在这里new一个对象,还要考虑在哪里释放内存。NodeJS果然是最适合做这种事情的。下面是onReceive的定义。
//当收到客户端发送的消息时调用
function onReceive(req,res){
  //接收POST过来的数据
  var data=[],f;
  req.on('data',function(e){data.push(e.toString())});
  req.on('end',function(){
    //当POST传输完毕时立即断开客户端(短连接)
    res.end();
    //解析POST过来的数据
    data=querystring.parse(data.join(''));
    //添加IP和时间字段
    data.ip=req.connection.remoteAddress;
    data.time=new Date*1;
    //转换成JSON格式
    data=JSON.stringify(data);
    //更新ETag
    ETag=new Date*1;
    //放入消息队列
    queue.unshift([ETag,data]);
    //让消息队列最多只保存20条消息
    queue=queue.slice(0,20);
    //把消息分发给连接池中的连接
    while(f=pool.shift())f(data);
  });
};
  这个注释的就更清楚了,需要注意的是NodeJS处理POST的方式,它是异步的。data事件中接收数据,end事件表示所有数据接收完毕。所以我们把代码全丢在了end事件里面,因为我们需要接收全部数据。在end中的第一行代码就是res.end(),如果你有仔细看之前的代码就能猜到这个代码是返回一个数据并断开客户端用的。在这里直接调用就意味着不返回任何数据直接断开客户端,这就是使用了短连接技术。而NodeJS和PHP的运行方式是不同的,PHP的连接是由宿主服务器管理的,比如Apache。所以PHP本身没办法断开连接只能连哄带骗让客户端自己断开。而NodeJS本身就是服务器程序,可以直接访问到连接的对象。甚至可以透过应用层协议直接访问传输层协议的对象。比如这里获取客户端IP的代码中的req.connection这个对象就是一个TCP连接对象,所以直接断开客户端只是简单的一个语句而已。这个代码的最后部分是把收到的消息发送到池中的连接上。我们在定义池的连接时保存的是一个回调函数。所以我们可以很简单的使用一个循环语句调用这个池中的所有函数。而每处理完一个连接就应该把它从池中删除掉,因为我们使用的是长轮询,收到数据之后需要断开,所以不能再把它留在池中了。
  以上这么多就是这个程序的服务器端代码了,在明天的文章中来解释客户端的代码。
网名:
54.144.24.*
电子邮箱:
仅用于接收通知
提交 悄悄的告诉你,Ctrl+Enter 可以提交哦
神奇海螺
[查看全部主题]
各类Web技术问题讨论区
发起新主题
本模块采用即时聊天邮件通知的模式
让钛合金F5成为历史吧!
次碳酸钴的技术博客,文章原创,转载请保留原文链接 ^_^