求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
使用 Node.js 作为完整的云环境开发堆栈
 

作者:Noah Gift, Jeremy Jones ,发布于2012-7-25

 

本文探讨 Node.js,这是一个用于 UNIX? 类平台上 V8 JavaScript? 引擎的事件驱动的 I/O 框架,设计这一框架的目的是为了编写可伸缩的网络程序,如 Web 服务器。本文通过一个完整的例子说明如何在 Node.js 中构建聊天服务器,分析了这个框架以及围绕它的生态系统(包括云计算产品),并对这个框架进行了总结。

随着技术创新表面上继续以指数级速度发展,新思想层出不穷。服务器端的 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 结构

最近,一个激动人心的开源项目 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 是负载平衡器与被平衡服务器之间的连接。

这两个套接字分别处理一个事件:接收到的数据。当它们其中一个收到数据时,就会将数据写到另一个套接字。

让我们完整地了解当一个客户端通过负载平衡器连接到通用网络服务器上,发送数据,然后接收数据时发生的事情。

  1. 当一个客户的连接到负载平衡器时,Node.js 在客户端与自己本身之间创建一个套接字,我们称之为 client_socket。
  2. 当连接建立之后,负载平衡器挑选一个目的地并创建一个指向该目的地的套接字连接,我们称之为 dest_socket。
  3. 当客户端发送数据时,负载平衡器将相同的数据推送到目的地服务器。
  4. 当目的地服务器做出响应并将一些数据写到 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 实例上运行,但需要由开发人员去实现它。

相关文章

用户故事与用例
交互设计师之精益画布篇
数据分析之用户画像方法与实践
如何快速建立用户模型?
 
相关文档

用户界面设计
给企业做大数据精准用户画像
用户体验和交互设计
大数据下的用户画像
相关课程

用户体验&界面设计
用户体验、易用性测试与评估
用户研究与用户建模
用户体验的软件UI设计最佳实践

 
分享到
 
 
     


专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件的思考
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS
更多...   
相关培训课程

云计算
Windows Azure 云计算应用开发