Node.js 开发基于 JavaScript 的 RESTful 应用

使用 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 应用。

左 超, 软件工程师, 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 执行结果
图 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 服务的应用框架
图 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 请求
图 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 数据
图 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 集合资源
图 5.RESTful 请求 group 集合资源

下面我们实现对单个资源的访问,同样从数据库中查询资源并以 JSON 方式返回到客户端。

URL: http://127.0.0.1:8090/resources/group/1
HTTP Method: GET
图 6.RESTful 请求 group 单个资源
图 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 资源
图 7.RESTful 请求创建 group 资源
图 8.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 资源
图 9.RESTful 请求更新 group 资源
图 10.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 资源
图 11.RESTful 请求删除 group 资源
图 12.RESTful 请求删除 group 资源结果
图 12.RESTful 请求删除 group 资源结果

结束语

本文介绍了 Node 和 RESTful 服务的基本概念,使用 Node 创建了一个服务器端的 JavaScript 框架来提供 RESTful 服务,并结合实例展示了如何采用回调函数来编写符合 Node 异步 I/O 特性的代码,在 Node 中如何处理异步的 form 提交以及在 Node 中如何操作 MySQL。

下载

描述 名字 大小
示例代码 restserver.zip 138KB

参考资料

学习

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值