使用 Node.js 开发基于 JavaScript 的 RESTful 应用
Node.js 是服务器端的 JavaScript 运行环境,它的设计初衷是以一种简单的方式创建可伸缩性的网络程序。Node.js 具有异步 I/O 和事件驱动等特性,充分利用了 JavaScript 的闭包特性和事件处理机制,实现了类似 Apache 的 HTTP Server,使之具备了构建基于 JavaScript 的高并发的 Web Application 的能力。REST 风格几乎是为 HTTP 协议量身定做的,在 HTTP 协议中用 URI 来标识唯一的资源,用 GET、PUT、POST、DELETE 等动词来操作资源,HTTP 协议是无状态协议,可以通过 Cache 来提高性能。本文将使用 Node.js 开发一个基于 JavaScript 的 RESTful 应用。
1 评论:
左 超, 软件工程师, IBM
王 芳, 软件工程师, IBM
2012 年 11 月 28 日
在 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();
});
};
图 11.RESTful 请求删除 group 资源
图 12.RESTful 请求删除 group 资源结果
结束语
本文介绍了 Node 和 RESTful 服务的基本概念,使用 Node 创建了一个服务器端的 JavaScript 框架来提供 RESTful 服务,并结合实例展示了如何采用回调函数来编写符合 Node 异步 I/O 特性的代码,在 Node 中如何处理异步的 form 提交以及在 Node 中如何操作 MySQL。
下载
描述 | 名字 | 大小 |
示例代码 | 138KB |