使用 Node.js 作为完整的云环境开发堆栈
使用 Node.js 作为完整的云环境开发堆栈
使用通过回调的异步 I/O 开发并发模型,并构建聊天服务器
Noah Gift, 助理工程主管, AT&T Interactive
Jeremy Jones, 高级系统工程师, Predictix
简介: 本文探讨 Node.js,这是一个用于 UNIX? 类平台上 V8 JavaScript? 引擎的事件驱动的 I/O 框架,设计这一框架的目的是为了编写可伸缩的网络程序,如 Web 服务器。本文通过一个完整的例子说明如何在 Node.js 中构建聊天服务器,分析了这个框架以及围绕它的生态系统(包括云计算产品),并对这个框架进行了总结。
本文的标签: javascript, node, node.js, nodejs, web_development, web_应用, 云计算平台, 应用开发
标记本文!
发布日期: 2011 年 8 月 01 日
级别: 中级
原创语言: 英文
访问情况 33312 次浏览
建议: 0 (添加评论)
1 star2 stars3 stars4 stars5 stars 平均分 (共 3 个评分 )
随着技术创新表面上继续以指数级速度发展,新思想层出不穷。服务器端的 JavaScript 就是这些新思想之一。 Node.js 是一种事件驱动的 I/O 框架,用于 UNIX 类平台上的 V8 JavaScript 引擎,适合于编写可伸缩的网络程序,如 Web 服务器。 Node.js 正是这种新思想的实现。
Node.js 并非与 JavaScript 抗衡,而是使用它作为完整的开发堆栈,从服务器端代码一直延伸到浏览器。Node.js 还充分利用了另一种创新思想:通过回调利用异步 I/O 的并发性模型。
Node.js 云计算平台
在云计算环境中使用 Node.js 框架时,能显示出它的一个巨大优点。对于应用程序开发人员,这往往归结使用平台即服务 (PaaS) 或基础架构即服务 (IaaS) 模型。对于开发人员而言,最抽象和公认最方便的方法是使用 PaaS 提供程序。图 1 十分简单地说明了 PaaS 和 IaaS 模型的结构。
图 1. PaaS 与 IaaS 结构
PaaS 与 IaaS 结构
最近,一个激动人心的开源项目 Cloud Foundry 公布了代码以创建一个能够运行 Node.js 的私有 PaaS。 同样的主机引擎也可用在公共云和商业云中,而且它们接受软件补丁。
基础架构管理是一大痛点,如果能够将这项工作外包(永远!)给规模经营的提供商,且无论是源代码,还是物理硬件资源,对于开发人员确实是一个激动人心的时刻。
回页首
使用 Node.js shell
在我们着手编写一个完整的 Node.js 例子之前,让我们先开始介绍如何使用交互式 shell。如果尚未安装 Node.js,您可以参考资源部分,然后按照说明安装它,或者使用在线的交互式 Node.js 站点之一,它允许您直接在浏览器中输入代码。
要在 Node.js 中以交互方式编写 JavaScript 函数,在命令行提示中输入node,如下所示:
lion% node
> var foo = {bar: 'baz'};
> console.log(foo);
{ bar: 'baz' }
>
在这个例子中,创建了对象foo,然后调用console.log 将它输出到控制台。这十分有效而且有趣,不过当您使用 tab 完成功能来探讨 foo 时,如下面的例子所示,真正的乐趣才刚刚开始。如果输入 foo.bar.,然后按下 tab 键,您将看到对象上的可用方法。
> foo.bar.
[...output suppressed for space...]
foo.bar.toUpperCase foo.bar.trim
foo.bar.trimLeft foo.bar.trimRight
试用 toUpperCase 方法似乎很有趣,下面显示了它的用法:
> foo.bar.toUpperCase();
'BAZ'
您可以看到,该方法将字符串转换为大写字母。这类交互式开发非常适合于使用像 Node.js 这样的事件驱动型框架进行开发。
在完成简单介绍之后,我们开始真正地构建一些东西。
回页首
用 Node.js 构建聊天服务器
Node.js 让编写基于事件的网络服务器变得十分简单。例如,让我们创建一些聊天服务器。第一个服务器十分简单,几乎没有什么功能,也没有任何异常处理。
一个聊天服务器允许多个客户端连接到它。每个客户端都可以编写消息,然后广播给所有其他用户。下面给出了最简单的聊天服务器的代码。
net = require('net');
var sockets = [];
var s = net.Server(function(socket) {
sockets.push(socket);
socket.on('data', function(d) {
for (var i=0; i < sockets.length; i++ ) {
sockets[i].write(d);
}
});
});
s.listen(8001);
在不到 20 行代码中(实际上,真正实现功能的代码只有 8 行),您已经构建了一个能够使用的聊天服务器。下面是这个简单程序的流程:
当一个套接字进行连接时,将该套接字对象附加到一个数组。
当客户端写入它们的连接时,将该数据写到所有的套接字。
现在,让我们检查所有代码,并解释这个例子如何实现聊天服务器预定功能。第一行允许访问 net 模块的内容:
net = require('net');
让我们使用这个模块中的 Server。
您将需要一个位置来保存所有客户端连接,以便在写入数据时可以写到它们中去。 下面是用于保存所有客户端套接字连接的变量:
var sockets = [];
下一行开始一个代码块,规定当每个客户端连接时要做的事情。
var s = net.Server(function(socket) {
传递到 Server 中的惟一参数是将针对每个客户端连接进行调用的一个函数。在这个函数中,将客户端连接添加到所有客户端连接的列表中:
sockets.push(socket);
下一部分代码建立了一个事件处理器,规定了当一个客户端发送数据时要做的事情:
socket.on('data', function(d) {
for (var i=0; i < sockets.length; i++ ) {
sockets[i].write(d);
}
});
socket.on() 方法调用为节点注册一个事件处理器,以便当某些事件发生时它知道如何处理。当接收到来自客户端的数据时,Node.js 会调用这个特殊的事件处理器。其他的事件处理器包括 connect、end、timeout、drain、error 和 close。
socket.on() 方法调用的结构类似于前面提过的 Server() 调用。您传入一个函数给这两者,当有事发生时调用此函数。这种回调方法在异步网络框架中很常见。这是当开始使用像 Node.js 这样的异步框架时,拥有过程编程经验的人会遇到的主要问题。
在这种情况下,当任意客户端发送数据给服务器时,就会调用这个匿名函数并将数据传入函数中。它基于您已经积累的套接字对象列表进行迭代,并给它们全部发送相同的数据。每个客户端连接都将接收到这些数据。
这个聊天服务器十分简单,它缺少一些非常基础的功能,比如识别是谁发送哪条消息,或者处理某个客户端断开的情况。(如果一个客户端从这台聊天服务器断开,任何人发送消息,服务器都会崩溃。)
下面的源代码(在下载示例文件中叫做 chat2.js )是一个经过改进的套接字服务器,其功能有所增强,能够处理“糟糕的情况”(比如客户端断开)。
net = require('net');
var sockets = [];
var name_map = new Array();
var chuck_quotes = [
"There used to be a street named after Chuck Norris, but it was changed because
nobody crosses Chuck Norris and lives.",
"Chuck Norris died 20 years ago, Death just hasn't built up the courage to tell
him yet.",
"Chuck Norris has already been to Mars; that's why there are no signs of life.",
"Some magicians can walk on water, Chuck Norris can swim through land.",
"Chuck Norris and Superman once fought each other on a bet. The loser had to start
wearing his underwear on the outside of his pants."
]
function get_username(socket) {
var name = socket.remoteAddress;
for (var k in name_map) {
if (name_map[k] == socket) {
name = k;
}
}
return name;
}
function delete_user(socket) {
var old_name = get_username(socket);
if (old_name != null) {
delete(name_map[old_name]);
}
}
function send_to_all(message, from_socket, ignore_header) {
username = get_username(from_socket);
for (var i=0; i < sockets.length; i++ ) {
if (from_socket != sockets[i]) {
if (ignore_header) {
send_to_socket(sockets[i], message);
}
else {
send_to_socket(sockets[i], username + ': ' + message);
}
}
}
}
function send_to_socket(socket, message) {
socket.write(message + '\n');
}
function execute_command(socket, command, args) {
if (command == 'identify') {
delete_user(socket);
name = args.split(' ', 1)[0];
name_map[name] = socket;
}
if (command == 'me') {
name = get_username(socket);
send_to_all('**' + name + '** ' + args, socket, true);
}
if (command == 'chuck') {
var i = Math.floor(Math.random() * chuck_quotes.length);
send_to_all(chuck_quotes[i], socket, true);
}
if (command == 'who') {
send_to_socket(socket, 'Identified users:');
for (var name in name_map) {
send_to_socket(socket, '- ' + name);
}
}
}
function send_private_message(socket, recipient_name, message) {
to_socket = name_map[recipient_name];
if (! to_socket) {
send_to_socket(socket, recipient_name + ' is not a valid user');
return;
}
send_to_socket(to_socket, '[ DM ' + get_username(socket) + ' ]: ' + message);
}
var s = net.Server(function(socket) {
sockets.push(socket);
socket.on('data', function(d) {
data = d.toString('utf8').trim();
// check if it is a command
var cmd_re = /^\/([a-z]+)[ ]*(.*)/g;
var dm_re = /^@([a-z]+)[ ]+(.*)/g;
cmd_match = cmd_re.exec(data)
dm_match = dm_re.exec(data)
if (cmd_match) {
var command = cmd_match[1];
var args = cmd_match[2];
execute_command(socket, command, args);
}
// check if it is a direct message
else if (dm_match) {
var recipient = dm_match[1];
var message = dm_match[2];
send_private_message(socket, recipient, message);
}
// if none of the above, send to all
else {
send_to_all(data, socket);
};
});
socket.on('close', function() {
sockets.splice(sockets.indexOf(socket), 1);
delete_user(socket);
});
});
s.listen(8001);
回页首
稍微高级一点的主题:聊天服务器的负载平衡
通常,负载按比例增长也是部署到云环境的理由之一。这种部署需要实现一些负载平衡机制。
大多数轻量级 Web 服务器,比如 nginx 和 lighttpd,都能够针对多台 HTTP 服务器进行负载平衡,但如果您想要在非 HTTP 服务器之间实现平衡,nginx 可能无法满足要求。而且尽管存在通用的 TCP 负载平衡器,您可能不会喜欢它们使用的负载平衡算法。或者它们没有提供您想要使用的一些功能。或者,您只是想享受构造自己的负载平衡器的乐趣。
下面是最简单的负载平衡器。它没有实现任何故障恢复,希望所有的目的地都是可用的,而且没有进行任何错误处理。它十分简约。基本的理念是,它接收一个来自客户端的套接字连接,随机挑选一个目标服务器进行连接,然后将来自客户端的所有数据转发给该服务器,并将来自该服务器的所有数据都发回到客户端。
net = require('net');
var destinations = [
['localhost', 8001],
['localhost', 8002],
['localhost', 8003],
]
var s = net.Server(function(client_socket) {
var i = Math.floor(Math.random() * destinations.length);
console.log("connecting to " + destinations[i].toString() + "\n");
var dest_socket = net.Socket();
dest_socket.connect(destinations[i][1], destinations[i][0]);
dest_socket.on('data', function(d) {
client_socket.write(d);
});
client_socket.on('data', function(d) {
dest_socket.write(d);
});
});
s.listen(9001);
destinations 的定义是我们要进行平衡的后端服务器的配置。这是一个简单的多维数组,主机名是第一个元素,端口号是第二个元素。
Server() 的定义类似于聊天服务器的例子。您创建一个套接字服务器,并让它监听一个端口。这次它将监听 9001 端口。
针对 Server() 定义的回调首先随机选择一个要连接到的目的地:
var i = Math.floor(Math.random() * destinations.length);
您可能已经使用过轮询算法或使用“最少连接数”算法完成一些额外的工作然后离去,但我们想尽可能地保持简单。
这个例子中有两个指定的套接字对象: client_socket 和 dest_socket。
client_socket 是负载平衡器与客户端之间的连接。
dest_socket 是负载平衡器与被平衡服务器之间的连接。
这两个套接字分别处理一个事件:接收到的数据。当它们其中一个收到数据时,就会将数据写到另一个套接字。
让我们完整地了解当一个客户端通过负载平衡器连接到通用网络服务器上,发送数据,然后接收数据时发生的事情。
当一个客户的连接到负载平衡器时,Node.js 在客户端与自己本身之间创建一个套接字,我们称之为 client_socket。
当连接建立之后,负载平衡器挑选一个目的地并创建一个指向该目的地的套接字连接,我们称之为 dest_socket。
当客户端发送数据时,负载平衡器将相同的数据推送到目的地服务器。
当目的地服务器做出响应并将一些数据写到 dest_socket 时,负载平衡器通过 client_socket 将这些数据推送回客户端。
可以对这个负载平衡器进行一些改进,包括错误处理,在同一个进程中嵌入另一个进程以动态增加和移除目的地,增加不同的平衡算法,以及增加一些容错处理。
回页首
超越原生解决方案:Express Web 框架
Node.js 配备有 HTTP 服务器功能,但较为低级。如果要在 Node.js 中构建一个 Web 应用程序,您可能会考虑 Express――一个为 Node.js 打造的 Web 应用程序开发框架。它弥补了 Node.js 的一些不足。
在下一个例子中,让我们重点关注使用 Express 胜过简单的 Node.js 的一些明显优势。请求路由就是其中之一,还有一个是为 HTTP "verb" 类型注册一个事件,比如“get”或“post”。
下面给出了一个十分简单的 Web 应用程序,它只是演示了 Express 的一些基本功能。
var app = require('express').createServer();
app.get('/', function(req, res){
res.send('This is the root.');
});
app.get('/root/:id', function(req, res){
res.send('You sent ' + req.params.id + ' as an id');
});
app.listen(7000);
这两行以 app.get() 开始的代码是事件处理器,当 GET 请求进入时就会触发。这两次方法调用的第一个参数是一个正则表达式,用于指定用户可能传入的 URL。第二个参数是真正处理请求的一个函数。
正则表达式参数是路由机制。如果请求类型(GET、POST等)与资源(/, /root/123)匹配,就会调用处理器函数。在第一次 app.get() 调用中,/ 被简单地指定为资源。而在第二次调用中,在指定/root 时后面还加了一个 ID。映射 regex 的 URL 中资源前面的冒号(:) 字符表明,这部分稍后可作为一个参数使用。
当请求类型与正规表达式匹配时,就会调用处理器函数。此函数带有两个参数,一个请求(req) 和一个响应(res)。前面提到的参数被附加给请求对象。而 Web 服务器传回给用户的消息被传入到响应对象。
这是一个非常简单的例子,但已经清楚地说明“真正的应用程序”如何利用这个框架来构建更加丰富和完整的功能。如果插入一个模板系统和一些数据引擎(传统的或 NoSQL 均可),您可以轻松构建出一组功能来满足真正应用程序的需求。
Express 的特点之一是高性能。这与其他快速 Web 应用程序框架的常见特性一起,让 Express 在注重高性能和海量可伸缩性的云部署领域中占据了重要的位置。
回页首
应了解的知识
有两个概念/趋势需要了解:
键/值数据库的突然流行。
其他异步的 Web 范型。
键/值数据库... 为什么突然流行?
因为 JavaScript 是 Web 的通用语言,对于 JavaScript Object Notation (JSON) 的讨论通常远远落后于 JavaScript 相关的研究。 JSON 是在 JavaScript 与一些其他语言之间交换数据的最常用途径。JSON 本质上是一种键/值存储,因此天生适用于对键/值数据库感兴趣的 JavaScript 和 Node.js 开发人员。毕竟,如果能够以 JSON 格式存储数据,JavaScript 开发人员的工作就将变得轻松很多。
有一个不太相关的趋势,在 NoSQL 数据库环境中也会涉及键/值数据库。CAP 定理(也叫做 Brewer 定理)指出,一个分布式系统有 3 个核心属性:一致性、可用性和分区容忍性(formal proof of CAP)。这条定理是 NoSQL 发展背后的推动力量,它为牺牲传统关系数据库的某些特性以换取(通常是高可用性)提供了理论基础。一些流行的键/值数据库包括 Riak、Cassandra、CouchDB 和 MongoDB。
异步 Web 范型
事件驱动的异步 Web 框架已经存在了相当长一段时间。其中最流行和最新的异步 Web 框架是 Tornado,它使用 Python 语言编写,在 Facebook 内部使用。下面这个例子说明了 hello_world 在 Tornado 中(在下载示例文件中叫做 hello_tornado.py )是什么样子。
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
application = tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
Twisted.web 也是用 Python 语言写的,工作方式也十分类似。
最后谈到真正的 Web 服务器本身,与 Apache 不同,nginx 不使用线程,而是使用一种事件驱动的(异步)架构来处理请求。异步 Web 框架使用 nginx 作为其 Web 服务器是十分常见的情况。
回页首
结束语
Node.js 在 Web 开发人员中非常引人关注。它允许开发团队同时在客户端和服务器端上编写 JavaScript。它们还可以结合与 JavaScript 相关的强大技术:JQuery、V8、JSON 和事件驱动的编程。另外还有基于 Node.js 开发的生态系统,比如 Express Web 框架。
Node.js 的优点引人关注,它也存在一些缺点。如果是 CPU 密集型编程,就无法体现 Node.js 提供的非阻塞 I/O 方面的优点。有些架构可以解决这类问题,比如将一个池中的进程分流到每个 Node.js 实例上运行,但需要由开发人员去实现它。
参考资料
学习
本文中提及的 Node.js 概念与技术:
Node.js:其目标是提供一种简单方式来构建可伸缩的网络程序。
Cloud Foundry:开源社区站点,开发人员可以在上面协作并为这个开放的 PaaS 环境项目做出贡献。
Node.js 指南:Felix Geisendorfer 的“教条式与非官方的”指南帮助人们开始使用 Node.js。(这份 StackExchange Q&A 也可以帮助新手解答关于 Node.js 的问题。)
High performance in Haskell:Google Site Reliability Engineer Gregory Collins 使用 Node.js 演示事件驱动的模型。
Tornado:一个开源的可伸缩、相对快速的非阻塞 Web 服务器。
Cloud9 IDE:一个运行在浏览器中但位于云环境中的最先进 IDE,允许您在任何地方运行、调试和部署 Node.js 应用程序。
Bulletproof Node.js coding:Stella Laurenzo 这样解释 Node.js 编码:“功能异常强大”而且“学起来毫不费力”。
Node Nerd:来自 Kevin Gorski 的 Node.js 链接和教程。
npm 1.0:试用 Node Package Manager。
Riak:一个 Dynamo-inspired 数据库,可按预期轻松扩展,并通过赋予用户快速建立原型、测试与部署应用程序的能力来简化开发。
nginx:一种开源的高性能 HTTP 服务器和反向代理服务器,宿主在全球接近 7.65% 的域上,以其高性能、稳定性、功能丰富、配置简单和较低的资源占用率而著称。nginx 解决了 C10K(同时支持 10,000 个客户端)问题。
Express: 一个为 Node.js 构建的 Web 应用程序开发框架,弥补了 Node.js 的一些不足。
阅读 developerWorks 文章“Node.js 究竟是什么?”。
从 developerWorks 文章“JavaScript for Java developers”中了解 JavaScript 为什么是当代 Java 开发人员的一种重要工具。
作者 Noah Gift 在 developerWorks 上发布的其他文章:
Noah Gift on business analytics and cloud computing.
Cloud business analytics: Write your own dashboard.
用 MapReduce 解决与云计算相关的 Big Data 问题.
使用开源工具测试 Web 应用程序.
Ryan McGeary 讨论如何通过 CoffeeScript 构建 atop node.js,让功能性编程更加清晰[收听 | 阅读].
在 developerWorks 云计算开发人员资源 中,发现并共享应用程序与服务开发人员构建针对云部署构建项目的知识与经验。
了解如何 访问 IBM Smart Business Development 和 Test on the IBM Cloud。
加入云计算讨论组,了解和讨论云计算的最新技术、解决方案、趋势等内容。
获得产品和技术
参见 IBM Smart Business Development 和 Test on the IBM Cloud 上可用的 产品镜像 。
讨论
阅读 developerWorks 上所有优秀的云计算博客。
加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。