在 Web2.0 盛行的今天,作为一种可以运行在浏览器客户端的轻量级脚本语言,JavaScript 被越来越多的开发人员所熟悉和掌握。大家印象中的 JavaScript 是简单高效的,
运行在客户端的程序,甚至有人认为 JavaScript 只是一个“小玩意”。Node.js 的出现彻底颠覆了以往对 JavaScript 的看法。Node.js 是服务器端的 JavaScript 运行环境,它的设计初衷是以一种简单的方式创建可伸缩性的网络程序。Node.js 具有异步 I/O 和事件驱动等特性,充分利用了 JavaScript 的闭包特性和事件处理机制,实现了类似 Apache 的 HTTP Server,使之具备了构建基于 JavaScript 的高并发的 Web Application 的能力。在 Node.js 的官方网站中使用 Node 来表示 Node.js,本文也将沿用这种用法,后文中出现的 Node 等同于 Node.js。
Node.js 的异步 I/O 和事件驱动机制
首先解释一下所谓的同步 I/O 和异步 I/O 的概念。它们都是针对于应用程序而言。
- 同步 I/O 指的是应用程序在遇到 I/O 操作时进入等待,不能利用 CPU,直到 I/O 操作结束后应用程序才继续执行后续任务。
- 异步 I/O 指的是应用程序在遇到 I/O 操作时不等待,继续执行后续任务,等 I/O 操作结束后操作系统发出事件提醒应用程序。
可以看出异步 I/O 能够更好地利用 CPU,提高应用程序的执行效率,同时提高 I/O 的并发性。举例来说,应用程序要先后执行两个 I/O 任务,网络 I/O 和文件 I/O,在同步 I/O 模型中应用程序必须等网络 I/O 执行完成后,才能执行文件 I/O。在异步 I/O 模型中,应用程序执行了网络 I/O 任务之后,不需要等待网络 I/O 的结束,可以立即执行后续的文件 I/O,从而使得 I/O 并发。
下例展示了用 Node 来打开一个文件的异步 I/O 的执行过程。主线程在执行了文件 I/O 后不等待,直接执行后续程序,I/O 执行结束后自动执行回调函数。
清单 1. 异步文件 I/O
// 加载 fs module fs = require('fs'); // 打开 example.log 文件 fs.open('c:/example.log', 'r', function(){ // 在回调函数中对文件进行操作 console.log('Open file completed!'); }); console.log('Execute main thread!');
图 1. 异步文件 I/O 执行结果
Node 的异步 I/O 特性很好地提高了对请求的并发处理能力,这点在 Web 服务器领域非常重要。传统的 Web 服务器比如 Apache HTTP Server,采用的是同步 I/O 的模型,通过多线程的方式来实现对请求的并发处理。基于线程的模型相对来说没有那么高效,并且要处理公共资源同步问题,对连接数也有一定的限制,服务器要给每个连接分配线程栈空间存储局部变量,对服务器的内存开销很大。Node 采用的是单线程 + 异步 I/O+ 事件轮询的机制来处理请求并发的。Node 用一个主线程来监听服务器的请求,有请求到达就用异步 I/O 处理请求,同时主线程能够快速的执行完毕来处理下一个请求。Node 后台通过事件轮询去监听 I/O 的处理情况,当 I/O 完成后,Node 会根据事件驱动机制去调用相应的回调函数。值得注意的是 JavaScript 的闭包特性保证了回调函数执行时依然处在之前的执行上下文中,可以取到正确的变量的值。
Node 的一个最佳实践是内存操作始终快于 I/O 操作。Node 的异步 I/O 机制使它在内存中并发处理多个 I/O 请求,而不是等待 I/O 请求的串行操作。有实验证明在相同的服务器硬件条件下,Node 的并发处理能力高于 PHP/Apache 的并发处理能力。
- 当 PHP/Apache 3187 请求 / 秒
- Node.js 5569 请求 / 秒
RESTful 应用的特点
REST 并不是一种具体的实现技术,而是一种软件架构风格,主要有以下特点:
- 从资源的角度来考察整个网络,每个资源有唯一标识
- 使用通用的连接器接口操作资源
- 对资源的操作不会改变资源标识
- 连接协议具有无状态性
- 能够使用 Cache 机制来增进性能
REST 风格几乎是为 HTTP 协议量身定做的,在 HTTP 协议中用 URI 来标识唯一的资源,用 GET、PUT、POST、DELETE 等动词来操作资源,HTTP 协议是无状态协议,可以通过 Cache 来提高性能。
基于 REST 的架构风格,人们把它使用到了 Web 服务中。在目前主流的三种 Web 服务实现方案中,RESTful 的 Web 服务比基于 SOAP 和 XML-RPC 方式的 Web 服务更加简洁高效。它直接使用 HTTP 协议就可以实现 Web 服务,不需要额外的封装协议和远程进程的调用。资源的表现形式可以是 HTML,也可以是 XML,JSON 等其他数据形式,这取决于 Web 服务的消费者是人还是机器。
表 1. HTTP 请求在 RESTful Web 服务中的典型应用
资源 | GET | PUT | POST | DELETE |
---|---|---|---|---|
一组资源的 URI,比如 http://www.example.com/resources/ | 列出 URI 及该资源组中每个资源的详细信息 | 使用一组给定的资源替换当前整组资源 | 在本组资源中创建 / 追加一个新资源 | 删除整组资源 |
单个资源的 URI,比如 http://www.example.com/resources/1 | 获取给定资源的详细信息 | 替换 / 创建指定的资源,并将其追加到相应的资源组 | 把指定的资源作为资源组,并在其下创建 / 追加一个新元素,使其隶属于当前资源 | 删除指定元素 |
使用 Node 创建 RESTful 应用
从上文的分析中可以看出,Node 提供了 HTTP 操作能力,并且可以使用在服务器和客户端。Node 的异步 I/O 特性保证了它具备了很强的伸缩性,所以 Node 原生就适合创建 RESTful 应用。下面我们会实现一个基于 Node 的,支持 RESTful 服务的框架。并使用这个框架来实现一个简单 RESTful 应用。
创建一个基于 Node、支持 RESTful 服务的应用框架
图 2 展示了我们要实现的框架的基本结构:
- restserver 类:集成了 Node HTTPServer 来提供 HTTP 访问的能力,使用 restparser 类解析 RESTful 的 URL,然后将请求转发给 router 类。
- restparser 类:将 RESTful 的 URL 解析成 JavaScript 对象。来这里我们约定 RESTful 的 URL 以 resources/ 开头,比如 http://www.example.com/resources/group/1 将会被解析成 {resource: group, id: 1}。
- router 类:将 HTTP 请求的动作转化为框架能够支持的事件,并将请求定位到具体的 DAO 类。
- resource_dao 类:负责处理具体的资源请求,比如在数据库或者文件中处理资源请求。
- resource 类:具体的资源类。
- JSONRender:将处理结果以 JSON 的格式返回到客户端。
图 2. 基于 Node,支持 RESTful 服务的应用框架
Node 提供了一个基于异步 I/O 和事件响应机制的 HTTPServer 实现。当有 HTTP 请求到达服务器时,Node 会在 event loop 响应后为它调用回调函数,然后再等待下一个 HTTP 请求,不同于同步 I/O,必须等到前一个请求处理结束后才能处理下一个请求。清单 1 展示了 restserver 的具体实现。它封装了 Node HTTPServer,在回调函数中处理请求的流程。
清单 2 .restserver.js
var http = require('http'), restrouter = require('./router'), \ restparser = require('./restparser'), parse = require('url').parse,\ util = require('util'), formidable = require('formidable'); http.createServer(function (req, res) { var url = parse(req.url), pathname = url.pathname; console.log('Request URL: http://127.0.0.1:8090' + url.href); // 解析 URL 参数到 resource 对象 req.resource = restparser.parse(pathname); //resource.id 存在,表示是 RESTful 的请求 if(req.resource.id){ res.writeHead(200, {'Content-Type': 'text/plain'}); restrouter.router(req, res, function(stringfyResult){ res.end(stringfyResult); }); }else{ res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Request URL is not in RESTful style!'); } }).listen(8090, '127.0.0.1'); console.log('Server running at http://127.0.0.1:8090/');
restserver 类首先调用了 restparser 类来解析 RESTful 请求,如果请求不符合 RESTful 的格式,将不会被处理。
图 3. 不符合 RESTful 格式的 HTTP 请求
restparser 类负责将 RESTful URL 请求解析成资源 JavaScript 对象,我们这里约定资源 id 为 0 时表示操作资源列表。清单 3 给出了 restparser 的具体实现。
清单 3.restparser.js
exports.parse = function(input){ if(null == input || '' == input) return {}; // 去除 URL 末端的斜杠 var str = removeSlashAtEnd(input), resIndex = str.indexOf('resources'); if(resIndex == -1 || resIndex == str.length -9) return {}; queryStrs = str.substr(resIndex + 10).split('/'); // id = 0 表示列出所有资源 if(queryStrs.length % 2 != 0){ queryStrs.push('0'); } return { resource : queryStrs[0], id : queryStrs[1] }; };
router 类是整个框架的核心处理器。它负责将 HTTP 方法映射到自定义的事件。如清单 4 所示,我们自定义了 8 种事件,分别将 4 种 HTTP 方法对应到集合资源和单个资源。
清单 4. 映射 HTTP 方法到自定义事件
function emitEvent(method, resource){ var localEvent; // 将 HTTP 方法映射到自定义事件 switch(method){ case 'GET' : localEvent = resource.id == 0 ? 'list' : 'retrieve'; break; case 'PUT' : localEvent = resource.id == 0 ? 'putCollection' : 'update'; break; case 'POST' : localEvent = resource.id == 0 ? 'create' : 'postMember'; break; case 'DELETE' : localEvent = resource.id == 0 ? 'deleteCollection' : 'deleteMember'; break; } return localEvent; }
这里值得一提的是 Node 在处理 GET / DELETE 方法是采用的是同步的方式,处理 POST / PUT 方法是采用了异步的方式,原因是 POST / PUT 请求时,请求的参数封装在了 HTTP Body 内,Node 基于异步 I/O,所以在读取 HTTP Body 流采用了异步的方式。因此框架提供了一个带有回调函数的 execute 方法来处理异步访问的方式。execute 方法针对 Node 处理 HTTP 方法的不同,提供了两种解析 HTTP 参数的方式。
清单 5. 带有回调函数的 execute 方法
function execute(req, event, callback){ req.params = req.params || {}; if(req.method === 'POST' || req.method === 'PUT'){ // 处理 POST / PUT 请求中的数据流 var form = new formidable.IncomingForm(); form.on('field', function(field, value) { req.params[field] = value; }).on('end', function() { // 当数据流加载结束后调用相应的 Module 处理请求 return invoke(req, event, callback); }); form.parse(req); }else{ // 对于 GET / DELETE 请求,直接调用相应的 Module 处理请求 var urlParams = urlParser(req.url, true).query; clone(req.params, urlParams); return invoke(req, event, callback); } }
router 类的另外一个核心的功能是将对资源的请求分发到具体的 resource_dao 类。resource_dao 类响应框架自定义的 8 种事件,来实现对资源的请求。在清单 4 中,可以看到 resource_dao 类的接口也必须传入一个回调函数。这是因为 DAO 的实现会涉及到 I/O 的操作,比如访问数据库。因此 DAO 的方法也是异步的,必须通过回调函数才能拿到资源数据。
清单 6 .invoke 方法将请求分发到具体的 resource_dao 类
function invoke(req, event, callback){ // 加载对应的资源处理 DAO var module = require( './model/' + req.resource['resource'] + '_dao'), model = new module.dao(), fn = model[event]; fn(req.resource.id, req.params, function(result){ console.log('Execute result'); console.log(result); // 以 JSON 格式展示执行结果 var stringfyResult = JSON.stringify(result); callback(stringfyResult); }); }
resource_dao 类和 resource 类针对的是具体的资源。接下来我们通过实现一个具体的例子来展示如何实现 RESTful 的服务。
创建一个 RESTful 服务的实例
我们定义一个 group 资源,并使用 MySQL 存储 group 资源。
清单 7.group.js
function group(id, name, location, size){ this.id = id; this.name = name; this.location = location; this. size = size; } exports.group=group;
图 4.MySQL 中存储的 group 数据
group_dao 类将会对框架定义的 8 种事件提供响应函数。这里我们采用了 Node 提供的 MySQL 组件来处理数据库操作。
清单 8.Node 中配置 MySQL
var mysql = require('mysql'); function group_dao(){ // 创建 MySQL 数据库连接 var dbConnection = mysql.createConnection({ host : 'localhost', user : 'root', password : 'admin', database : 'nodejs' }); }
首先我们实现对 group 资源的集合查询。集合的 GET 请求映射到自定义的 list 方法,在 list 方法签名中包含了一个回调函数,这是为了在异步操作中将查询结果返回到上层调用者中。
清单 9 .list 方法从数据库中查询 group 资源集合
this.list = function(id, params, callback){ var groups = []; // 执行数据库查询,在回调函数中处理查询结果 dbConnection.query('SELECT * FROM groups', function(err, rows, fields) { if (err) throw err; for(var i=0; i<rows.length; i++){ var group = new Group(rows[i].id, rows[i].name, rows[i].location, rows[i].size); groups.push(group); } // 执行回调函数 callback(groups); dbConnection.end(); }); };
我们使用 HttpRequester 插件来对 group 集合资源进行访问,从图 5 的 Response 中我们可以看到 Node 从数据库中查询到集合资源,并以 JSON 方式返回到客户端。
URL: http://127.0.0.1:8090/resources/group HTTP Method: GET
图 5.RESTful 请求 group 集合资源
下面我们实现对单个资源的访问,同样从数据库中查询资源并以 JSON 方式返回到客户端。
URL: http://127.0.0.1:8090/resources/group/1 HTTP Method: GET
图 6.RESTful 请求 group 单个资源
接下来我们展示如何采用 RESTful 请求来创建资源,并保存到数据库中。清单 10 中我们可以看到 Node 提供了简介的数据库操作接口,可以直接将 JavaScript 对象作为参数传入到 SQL 查询中。
URL: http://127.0.0.1:8090/resources/group/3 HTTP Method: POST HTTP Parameter: name=GroupC&location=WX&size=10
清单 10.postMember 方法创建资源
this.postMember = function(id, params, callback){ if(arguments.length >= 2){ var newId = arguments[0]; var params = arguments[1]; // 执行数据库插入操作,通过 JavaScript 对象传递参数 dbConnection.query('INSERT INTO groups SET ?', {id: newId, name: params.name, \ location: params.location, size: params.size} , function(err, result) { if (err) throw err; callback({id: newId}); dbConnection.end(); }); } };
图 7.RESTful 请求创建 group 资源
图 8.RESTful 请求创建 group 资源的结果
下面我们展示如何用 RESTful 的方式来更新资源。HTTP 的 PUT 操作在语义上可以用来更新资源,它和 POST 一样,在 HTTP Body 中封装了参数信息。
URL: http://127.0.0.1:8090/resources/group/3
HTTP Method: PUT
HTTP Parameter: name=GroupC&location=WX&size=30
清单 11.update 方法更新资源
图 9.RESTful 请求更新 group 资源
图 10.RESTful 请求更新 group 资源结果
最后我们展示用 HTTP DELETE 操作来删除资源。
URL: http://127.0.0.1:8090/resources/group/3
HTTP Method: DELETE
清单 12.deleteMember 方法删除资源
this.deleteMember = function(id, params, callback){ // 执行数据库删除操作 dbConnection.query('DELETE FROM groups where id = ?', [id] , function(err, result){ if (err) throw err; callback({id: id}); dbConnection.end(); }); };