简洁与性能:服务器端的 JavaScript
多年来,JavaScript (JS) 的高级祭司 Douglas Crockford 声称,它是一种强大而灵活的语言,适用于多种任务,特别是当您可以将其与丑陋的浏览器端部分(即文档对象模型或 DOM)分离时。由于浏览器的存在,JavaScript 成为了用户数量最多的流行编程语言。招聘网站 dice.com 和 monster.com 上发布的 JavaScript 职位比除 Java 之外的任何其他语言都多。当然,如果 JavaScript 在浏览器或任何地方运行,它必须有一个引擎。这些引擎自最早支持 JS 的浏览器以来就已存在,并且作为独立的实体已经存在多年。因此,在自身上运行 JS 的潜力一直存在。然而,JavaScript 总是缺少两个关键要素,使其值得在服务器端运行。
第一个缺失的部分是一组通用的库。很简单,由于 JS 如此专注于浏览器,它缺少基本的 I/O 库,例如文件读取和写入、网络端口创建和监听以及可以在任何像样的独立语言中找到的其他元素。Ruby 原生包含它们;Java 在其 java.io 和 java.net 包中包含它们。对于 JavaScript 而言,当您唯一能做的就是处理文本和数据结构,但无法与外部世界通信时,单独运行相当无用。多年来,人们已经尝试制作某种形式的 JS I/O 和网络包,主要围绕原生 C 调用进行封装(如果 JS 引擎是用 C 编写的,例如 SpiderMonkey),或者 java.io 和 java.net 调用(如果 JS 引擎是用 Java 编写的,例如 Rhino)。
这种情况在 2009 年初开始改变,当时创建了 CommonJS 项目(出于某种神秘的原因,它代表 Common JavaScript),该项目在通用命名空间下统一了这些努力,具有 JS 特定的语义,并包含了一个包包含系统。
以 Rhino 为例,您可以使用以下代码从文件中读取:
defineClass("File"); var f = new File("myfile.txt"), line; while ((line = f.readLine()) !== null) { // do some processing } // this example slightly modified and simplified // from the Mozilla Rhino site
如您所见,这不是 JavaScript 中的文件处理;而是 Java 中的文件处理!我所做的只是向 JavaScript 打开了 Java API。如果您真的打算用 Java 编程,那很好,但如果您尝试使用纯 JS,尤其是当您的引擎不是基于 Java 时,它的帮助有限。
借助 CommonJS,出现了一个标准的 JavaScript 原生接口,用于包含包,例如 I/O 包或 http 包,并定义许多标准功能。在底层,实现可以是 C、Java、Erlang 或 Gobbledygook。重要的是,面向开发人员的接口是平台无关的,并且可以从一个解释器移植到另一个解释器。
第二个缺失的部分是一个服务器,类似于 Java 的 Tomcat/Jetty 或 Ruby 的 Mongrel/Thin,它提供了一个真实的环境,包括必要的模块并且易于使用。最重要的是,它需要利用 JavaScript 的优势,而不是尝试复制适用于 Java 或 Ruby 的系统。真正的突破是 Ryan Dahl 的 Node.JS。Ryan 结合了 Google 的高性能 V8 引擎、JavaScript 的自然异步语义、模块系统和基本模块,创建了一个非常适合 JavaScript 的服务器。
大多数 Web 服务器都有一个主进程来接收每个新请求。然后,它要么派生一个新进程来处理特定请求,而父进程监听更多请求,要么创建一个新线程来执行相同的操作,本质上是相同的方法,如果稍微更有效率的话。进程或线程的问题是三重奏。首先,它们需要大量的资源使用(内存和 CPU)来处理少量不同的代码。其次,这些线程经常会阻塞各种活动,例如文件系统或网络访问,从而占用宝贵的资源。最后,线程和进程需要在 CPU 中进行上下文切换。尽管现代操作系统非常出色,但上下文切换仍然很昂贵。
另一种越来越流行的替代方案是事件驱动的异步回调。在事件模型中,一切都在一个线程中运行。但是,每个请求都没有自己的线程。相反,每个请求都有一个回调,当事件(例如新的连接请求)发生时调用该回调。一些产品已经利用了事件驱动模型。Nginx 是一款 Web 服务器,其 CPU 利用率特性与占主导地位的 Apache 相似,但内存使用量恒定,无论它服务多少个并发请求。使用 EventMachine 已将相同的模型应用于 Ruby。
任何使用 JavaScript 编程过的人,尤其是异步 AJAX,都知道 JS 非常适合事件驱动编程。Node.JS 巧妙地将打包和异步事件驱动模型与一流的 JS 引擎相结合,创建了一个令人难以置信的轻量级、易于使用但功能强大的服务器端引擎。Node 存在不到两年,并且在 2009 年 5 月底才首次向全世界发布,但它已得到广泛采用,并已成为许多其他框架和项目的催化剂。简而言之,Node 改变了我们编写高性能服务器端节点(双关语)的方式,并开辟了一个全新的视野。
本文的其余部分探讨了安装 Node 和创建两个示例应用程序。一个是经典的“hello world”,这是每个编程示例的起点,另一个是简单的静态文件 Web 服务器。更复杂的应用程序、基于 Node 的开发框架、Node 的包管理器、可用的托管环境以及如何托管您自己的 Node 环境将是未来文章的主题。
Node 将安装在几乎任何平台上,但它非常适合 UNIX 类环境,例如 Linux、UNIX 和 Mac OS X。它可以安装在 Windows 上,使用 Cygwin,但它不如其他平台容易,并且有很多陷阱。像大多数服务器端软件包一样,如果您想做任何严肃的事情,请在 UNIX/Linux/BSD 上进行。
在 Linux 或 UNIX 上,安装遵循典型的 UNIX 程序安装:下载、配置、make、make install。
首先,下载最新的软件包。在撰写本文时,最新的不稳定版本是 0.3.2,最新的稳定版本是 0.2.5。我建议尽快转向 0.3+。不要被低版本号所迷惑;许多生产站点现在正在使用 Node 来至少处理其环境的一部分,包括 github.com。
您可以直接从 nodejs.org 下载 tarball,或克隆 github 存储库,这是我首选的方法。如果您尚未安装 git,请通过您首选的软件包管理器或直接安装。在开始之前,请确保您已具备先决条件。尽管我可以在此处包含它们,但构建 git 的详细信息超出了本文的范围。
在 Mac OS X 上
# install XCode from the Apple developer Web site $ brew install git
在 Linux 或类似的 apt 包管理系统上
$ sudo apt-get install g++ curl libssl-dev apache2-utils $ sudo apt-get install git-core
现在,您已准备好下载、编译和安装 Node。首先,您需要cd到适当的目录。此时,克隆 git 存储库
$ git clone git://github.com/ry/node.git # if you have problems with git protocol, http works fine $ git clone http://github.com/ry/node.git
接下来,确保您处于正确的版本。由于 git 克隆了整个存储库,请确保您切换到正确的版本
$ cd node $ git checkout <version> # version can be whichever you want, # but I recommend v0.3.2 as of this writing
然后,运行configure。与往常一样,configure 将检查您是否安装了所有先决条件。Configure 还会确定在准备就绪时将 Node 安装在哪里。除非您正在生产机器上工作,否则我强烈建议将 Node 安装在您主目录下的本地可写存储库中,例如 ~/local/。将 git 安装在默认的 /usr/local/bin/ 中会导致在安装软件包和在安装过程中以 sudo 身份运行所有内容时出现各种有趣的权限问题。除非它将要在所有人之间共享并在生产中使用,否则安装在您自己的目录中更有意义。它也很小。我在笔记本电脑上的整个安装,包括二进制文件、手册页和几个附加软件包,不到 50MB。Node 二进制文件本身不到 5MB
# installing in the default $ ./configure # installing in your own local directory, # my preferred method $ ./configure --prefix=~/local
然后,编译并安装
$ make $ make install
此时,Node 已安装并准备运行。如果您将 Node 安装在 ~/local/ 中,则需要将 ~/local/bin 添加到您的路径中,这取决于您的 shell。
关于 Node 开发需要记住的关键是,所有重要的东西都是异步的。当然,您可以同步地执行许多操作,但为什么要这样做呢?
例如,传统的 Web 编程模型可能如下所示
// pseudo-code conn = connection.waitForRequest(); if (conn != null) { request = conn.getRequest(); response = conn.getResponse(); data = database.getData(query); response.write(someData); }
在异步 Node 中,您会执行更像这样的操作
server.handleRequest(function(request,response) { // we need some data from the database database.submitQuery(query,function(data) { response.write(data); }); });
请注意,一切都在回调中,这是一个事件驱动的异步模型。
一切都始于 hello world。此示例演示了模块和异步处理请求的基础知识。
首先,包含必要的 http 模块
var http = require('http');
http 是 Node 附带的标准模块。如果您想要一个不在标准路径中的模块,您应该在前面加上./,它相对于应用程序运行的路径执行。例如,require("./mymodule");.
接下来,创建服务器,这就像createServer()一样简单,以及处理每个请求的回调函数
http.createServer( function(request, response) { // handling code here });
接下来,放入处理代码。您知道您希望响应为 hello world,并且 http 状态代码为 200,这是基本成功
http.createServer( function(request, response) { // set your status code to 200 and content to plain text, // since "hello, world!" is as plain as it gets response.writeHead(200,{"Content-Type": "text/plain"}); // write out our content response.write("Hello, world!\n"); // indicate that we are done response.end(); });
以上是一个回调函数。每次有新的连接请求进入时都会调用它。
最后,您需要告诉服务器监听哪个端口。现在,让我们将其放在 8080 端口上(只是为了惹恼 Tomcat)
http.createServer( callbackFunction ).listen(8080);
将所有内容组合在一起,您将得到一个非常简单的程序
var http = require('http'); http.createServer( function(request, response) { // set your status code to 200 and content to plain text, // since "hello, world!" is as plain as it gets response.writeHead(200,{"Content-Type": "text/plain"}); // write out our content response.write("Hello, world!\n"); // indicate that we are done response.end(); }).listen(8080);
六行代码,以及一个功能正常的 Web 服务器,它说“Hello, world!”。将文件另存为 app.js,然后运行它
# cd to your development directory $ cd workingdir $ node ./app.js
将您的浏览器连接到 https://:8080,或使用 curl 或 wget,您将看到“Hello, world!”
对于下一个示例,让我们从本地文件系统提供文件。如果该文件在文档根目录中可用,则让我们使用 200 响应返回它;如果不可用,则让我们返回 404 状态代码和错误消息。
与上次一样,您需要 http 模块。与上次不同,您还需要从文件系统读取的模块以及处理 URL 的能力
var http = require('http'), fs = require('fs'), ↪path = require('path'), url = require('url');
以与上次相同的方式创建服务器及其处理程序,并在端口 8080 上监听(只是为了惹恼 Tomcat)
http.createServer( function(request, response) { // handling code }).listen(8080);
区别在于处理代码。现在,当您收到请求时,您想查看它是否在文件系统中存在,如果存在,则返回它
http.createServer( function(request, response) { // __dirname is a special variable set by node var file = __dirname+path; // check if the requested path exists path.exists(file, function(exists) { if (exists) { } else { }); }); }).listen(8080);
您使用 path 模块来检查文件是否可用,但您是异步执行此操作的。通常,文件访问非常慢,并且线程或进程中的所有内容都会阻塞。借助 Node 的事件驱动模型,没有任何东西会阻塞;相反,系统会继续移动并在它回答文件是否存在时调用 function(exists) 回调。
如果文件确实存在,您需要使用“file”模块读取它并将其发送回去。如果不存在,则发送回 404 错误。首先,让我们看一下简单的未找到文件的情况
if (exists) { // do some handling } else { response.writeHead(404, {"Content-Type": "text/plain"}); response.write("404 Not Found\n"); response.end(); }
现在,让我们看一下读取文件并在文件存在时将其发送回去。再次,异步读取文件
if (exists) { // read the file asynchronously fs.readFile(file,"binary",function(err,file) { if (err) { // we got some kind of error, report it response.writeHead(500,{"Content-Type":"text/plain"}); response.write(err+"\n"); response.end(); } else { response.writeHead(200,{"Content-Type":"text/html"}); response.write(file,"binary"); response.end(); } }); }
将所有内容组合在一起并进行一些清理,您将获得一个简洁、整洁、异步、事件驱动的 Web 文件服务器
var http = require('http'), fs = require('fs'), ↪path = require('path'), url = require('url'); http.createServer( function(request, response) { var file = __dirname+url.parse('url').pathname; // check if the requested path exists path.exists(file, function(exists) { if (exists) { fs.readFile(file,"binary",function(err,file) { if (err) { response.writeHead(500,{"Content-Type":"text/plain"}); response.write(err+"\n"); response.end(); } else { response.writeHead(200,{"Content-Type":"text/html"}); response.write(file,"binary"); response.end(); } }); } else { response.writeHead(404, {"Content-Type": "text/plain"}); response.write("404 Not Found\n"); response.end(); } }); }).listen(8080);
一个静态 Web 文件服务器,它将胜过市场上大多数此类服务器,仅需 23 行代码——这是一件艺术品。
资源
Node.JS: nodejs.org
Node.JS Git 仓库: github.com/ry/node
CommonJS: www.commonjs.org
Cygwin: www.cygwin.com
Nginx: nginx.org
Douglas Crockford: www.crockford.com
Avi Deitcher 是一位常驻纽约和以色列的运营和技术顾问,自 Z80 和 Apple II 时代以来一直从事技术工作。他拥有哥伦比亚大学电气工程学士学位和杜克大学 MBA 学位。可以通过 avi@atomicinc.com 与他联系。