突袭HTML5之WebSocket入门2 - 高效服务器Node.js

    JavaScript脱离浏览器的限制以后,隐隐有再次走向光明的迹象,看来还得重新审视一下它的前景。由于条件有限,这里所有的例子是运行在Windows平台下的。而且由于目前并没有太好的组织JS程序的IDE工具,所以使用VS2010的WebSite类型建立了一个空的Web,方便组织文件和文件夹。
node.js
    node.js(简称为Node)不是JavaScript应用、而是JavaScript运行平台
    Node从2009年诞生至今,已经发展了两年有余,其成长的速度有目共睹。从在github的访问量超过Rails,到去年底Node创始人Ryan Dalh加盟Joyent获得企业资助,再到发布Windows移植版本,Node的前景获得了技术社区的肯定。

    Node采用C++语言编写而成;为什么采用C++语言呢?据Node创始人Ryan Dahl回忆,他最初希望采用Ruby来写Node,但是后来发现Ruby虚拟机的性能不能满足他的要求,后来他尝试采用Google的V8引擎,所以选择了C++语言。Node是一个服务器端的JavaScript运行环境(支持的系统包括*nux、Windows),这意味着你可以编写系统级或者服务器端的JavaScript脚本,交给Node来解释执行。

程序安装

    到主站http://nodejs.org/下载系统对应的最新版的装上即可,非常简单。安装程序会自动安装npm,这个就是Node的模块管理工具(就像Ruby的Gem一样),所有扩展的模块或框架都是通过这个安装的。

Node模块 

    模块是Node可用的类库;Node使用CommonJS模块系统作为核心模块,比如http, sys, process, fs, streams...来完成各种功能。用户可以自己编写模块,在程序中使用;此外,目前有大量稳定的外部模块可用,基本都可以在https://github.com/joyent/node/wiki/modules看到介绍;使用npm install可以安装模块相应的模块到当前目录中,比如经典的socket.io, express,是通过下面的命令行执行安装的:

npm install socket.io express

    在Node中,文件和模块是一一对应的。

卓越特性

    Node的特性可以概括如下:Google V8,单线程,非阻塞IO,事件驱动。 

    先说V8,Google Chrome浏览器的JavaScript引擎Google V8是一个开源的独立引擎,可独立运行或内嵌于任何C++工程之中,性能很好,同时还提供了很多系统级的API如文件操作、网络编程等。速度是V8追求的主要设计目标之一,它把JavaScript脚本直接编译成机器码运行,比起传统的“中间代码+解释器”的引擎,优势不言而喻,这就是Node高效的一个重要原因。浏览器端的JavaScript脚本在运行时会受到各种安全性的限制,对客户系统的操作有限。相比之下,Node则是一个全面的后台运行时,为JavaScript提供了其他语言能够实现的许多功能。Node的目的是提供一种简单的途径来编写高性能的网络程序。

    Node是单线程执行的,那么如何解决并行的问题呢?这个就是通过非阻塞IO与事件驱动实现的。 这种做法的的根据是IO的运行速度远远慢于CPU的运行速度,所以当请求来了以后,CPU处理一下,直接甩给相应的IO去处理,然后继续等待别的请求。最经典的一句解释就是“除了你的代码,其他全部是并行执行的”。下面这幅图清晰的说明了Node的并行处理机制:

    Node核心模块中的方法基本都有异步版本与同步版本(Sync结尾的方法),尽量不要使用同步的版本来阻塞主线程。

编程实践

    Node大大简化了Web开发人员的工作量,只需要少少几步,就可以搭建起一个服务器,进行开发和调试。

  • 服务端编程

    服务器编程就是启动服务器,处理客户端请求。下面就是一个附带简单路由功能的服务器程序: 

//导入需要的模块
var http = require("http"), url = require("url"), path = require("path"),  fs = require("fs"); 
//创建Server并处理请求
http.createServer(function (req, res) 
{     
    var pathname=__dirname+url.parse(req.url).pathname;     
    if (path.extname(pathname)=="") 
    {        
        pathname+="/";     
    }     
 
    if (pathname.charAt(pathname.length-1)=="/")
    {         
        pathname+="index.html";     
    }      

    path.exists(pathname,function(exists)
    {         
        if(exists)
        {            
            switch(path.extname(pathname))
            {                 
                case ".html":                     
                    res.writeHead(200, {"Content-Type": "text/html"});                     
                    break;                 
                case ".js":                     
                    res.writeHead(200, {"Content-Type": "text/javascript"});                     
                    break;                                
                case ".png":                     
                    res.writeHead(200, {"Content-Type": "image/png"});                  
                    break;               
                default:                   
                    res.writeHead(200, {"Content-Type": "application/octet-stream"});      
            }         
                    
            fs.readFile(pathname,function (err,data)
            {               
                res.end(data);            
            });       
        } 
        else 
        {     
            res.writeHead(404, {"Content-Type": "text/html"});          
            res.end("<h1>404 Not Found</h1>");     
        }    
        });   
}).listen(8080, "127.0.0.1");   
                    
console.log("Server running at http://127.0.0.1:8080/"); 

    运行这个服务程序很简单,把这个代码保存为server.js(名字随意),然后直接在cmd中执行"node server.js",这样一个服务端程序就就绪了,是不是很简单!

注意

1. node后面的参数一定要带扩展名,不能省略那个".js",否则会报错。

2. 这是一个简单的服务器程序,所以没有考虑程序的结构;在典型的开发过程中,通常需要把启动代码,服务器代码,路由代码,请求处理代码分别放到不同的模块中并使用MVC来减弱每个功能之间的耦合性,易于扩展。例如下面的代码结构:

|-Solution文件夹

    |--index.js:程序的入口,放置启动服务器等初始化方法(Node的惯用法,也可使用run.js等名字)。

    |--server.js:程序服务器程序,通常放置启动服务器,调用路由程序(这个有时也放到index.js中)等。

    |--route.js:程序路由程序,放置路由方法。

    |--config.js:配置路由程序。

    |--controllers:放置各种需求处理的业务逻辑。

        |--hanlerRequest.js:请求处理程序模块,放置每种请求的处理方法。

    |--models:放置各种需要的数据

        |--model...业务数据

    |--views:放置各种页面模板

        |--index.html:页面模板

    这里的组织方式只是我采用的一个组织方式,实际开发中还是以个人的实际需要来设计自己的组织方式。

3. 模块的成员使用exports或者this导出。例如服务器程序server.js:

var http = require("http");
   
exports.runServer = function(){
    var server = http.createServer(function(req, res){
       //...        
    }).listen(8080);
    
    console.log('Server running at http://127.0.0.1:'+ port +'/');
};

this.server = '127.0.0.1';

    server.js模块导出了runServer方法,server属性。

4. 导入模块用require方法,导入以后可以直接使用导入的成员。例如index.js模块的代码为:

var server = require('./server');
server.runServer();

导入的规则如下:

(1)核心模块通过标示符直接导入,而且总是被优先加载。例如,require('http')将总是返回内建的HTTP模块,即便又一个同名文件存在。

(2)文件模块通过文件名导入(可以省略扩展名),如果没有找到确切的文件名,Node将尝试以追加扩展名.js后的文件名读取文件,如果还是没有找到则尝试追加扩展名.node。.js文件被解释为JavaScript格式的纯文本文件,.node文件被解释为编译后的addon(插件)模块,并使用dlopen来加载。

(3)以'/'为前缀的模块是一个指向文件的绝对路径,例如require('/home/publish/foo.js')将加载文件/home/publish/foo.js。
(4)以'./'为前缀的模块是指向文件的相对路径,相对于调用require()的文件。也就是说为了使require('./circle') 能找到正确的文件,circle.js必须位于与foo.js 相同的路径之下。
(5)如果标明一个文件时没有 '/' 或 './'前缀,该模块或是"核心模块",或者是位于安装目录的node_modules目录中。如果在其他的目录下运行npm安装模块,则这里指的就是其他目录的node_modules目录。

(6)在Node中,require.paths是一个保存模块搜索路径的字符串数组。当模块不以'/','./'或'../'为前缀时,将从此数组中的路径里进行搜索。可以在运行时改变require.paths数组的内容,以改变路径搜索行为。但在实践中发现,修改require.paths列表往往是造成混乱和麻烦的源头。

  • 客户端编程

    Node运行服务端程序,不影响客户端,客户端编程还是与以前一样,就是考验兄弟JavaScript/JQuery的功力了。

 

实用参考:

Node核心模块介绍:http://cnodejs.org/cman/

Node入门经典教程:http://www.nodebeginner.org/index-zh-cn.html#responding-request-handlers-with-non-blocking-operations 

 

posted @ 2012-01-21 11:14  沙场秋点兵  阅读(2344)  评论(0编辑  收藏  举报