第1章 Node基础
本章内容
- Node.js是什么
- 服务端JavaScript
- Node的异步和事件触发本质
- Node为谁而生
- Node程序示例
我们在本章中会看到下面这些概念:
- 为什么JavaScript对服务端开发很重要;
- 浏览器如何用JavaScript处理I/O;
- Node在服务端如何处理I/O;
- DIRT程序是什么意思,为什么适于用Node开发;
- 几个基础的Node程序示例。
1.1 构建于JavaScript之上
得益于浏览器厂商的白热化竞争,JavaScript的性能以不可思议的速度得到了大幅提升。现代化JavaScript虚拟机的性能正改变着可以构建在Web上的应用类型。在服务器端编程,Node使用的是为Google Chrome提供动力的V8虚拟机。V8让Node在性能上得到了巨大的提升,因为它去掉了中间环节,执行的不是字节码,用的也不是解释器,而是直接编译成了本地机器码。Node已经赚足了眼球。但JavaScript只是整幅拼图中的一块;Node使用JavaScript的方式 则更为有趣。为了理解Node环境,我们先看看你最熟悉的JavaScript环境:浏览器。
1.2 异步和事件触发:浏览器
Node为服务端JavaScript提供了一个事件驱动的、异步的平台。它把JavaScript带到服务端中的方式跟浏览器把JavaScript带到客户端的方式几乎一模一样。了解浏览器的工作原理对我们了解Node的工作原理会有很大帮助。它们都是事件驱动(用事件轮询)和非阻塞的I/O处理(用异步I/O)。
事件轮询和异步I/O
<!--我们来看一小段jQuery用XMLHttpRequest(XHR)做Ajax请求的代码:-->
$.post('/resource.json', function (data) { //I/O不会阻塞执行
console.log(data);
});
// 脚本继续执行
注意,代码没有写成下面这样:
var data = $.post('/resource.json'); //在I/O完成之前程序会被阻塞
console.log(data);
1.3 异步和事件触发:服务器
可能大多数人都了解传统的服务端编程的I/O模型,就像1.2节那个“阻塞”的jQuery例子一样。下面是一个PHP的例子:
$result = mysql_query('SELECT * FROM myTable'); //在数据库查询完成之前程序不会继续执行
print_r($result);
这段代码做了些I/O操作,并且在所有数据回来之前,这个进程会被阻塞。对于很多程序而言,这个模型没什么问题,并且很容易理解。但有一点可能会被忽略:这个进程也有状态,或者说内存空间,并且在I/O完成之前基本上什么也不会做。根据I/O操作的延迟情况,那可能会有10ms到几分钟的时间。延迟也可能是由下列意外情况引发的:
- 硬盘正在执行维护操作,读/写都暂停了;
- 因为负载增加,数据库查询变得更慢了;
- 由于某种原因,今天从sitexyz.com拉取资源非常迟缓。
如果程序在I/O上阻塞了,当有更多请求过来时,服务器会怎么处理呢?在这种情景中通常会用多线程的方式。一种常见的实现是给每个连接分配一个线程,并为那些连接设置一个线程池。你可以把线程想象成一个计算工作区,处理器在这个工作区中完成指定的任务。线程通常都是处于进程之内的,并且会维护它自己的工作内存。每个线程会处理一到多个服务器连接。尽管这听起来是个很自然的委派服务器劳动力的方式(最起码对那些曾经长期采用这种方式的开发人员来说是这样的)但程序内的线程管理会非常复杂。此外,当需要大量的线程处理很多并发的服务器连接时,线程会消耗额外的操作系统资源。线程需要CPU和额外的RAM来做上下文切换。
NGINX,它跟Apache一样,是个HTTP服务器,但它用的不是带有阻塞I/O的多线程方式,而是带有异步I/O的事件轮询(就像浏览器和Node一样)。因为这些设计上的选择,NGINX通常能处理更多的请求和客户端连接,它因此变成了响应能力更强的解决方案。

在Node中,I/O几乎总是在主事件轮询之外进行,使得服务器可以一直处于高效并且随时能够做出响应的状态,就像NGINX一样。这样进程就更加不会受I/O限制,因为I/O延迟不会拖垮服务器,或者像在阻塞方式下那样占用很多资源。因此一些在服务器上曾经是重量级的操作,在Node服务器上仍然可以是轻量级的。
1.4 DIRT程序
实际上,Node所针对的应用程序有一个专门的简称:DIRT。它表示数据密集型实时程序。因为Node自身在I/O上非常轻量,它善于将数据从一个管道混排或代理到另一个管道上,这能在处理大量请求时持有很多开放的连接,并且只占用一小部分内存。它的设计目标是保证响应能力,跟浏览器一样。
1.4.1 默认DIRT
Node从构建开始就有一个事件驱动和异步的模型。JavaScript从来没有过标准的I/O库,那是服务端语言的常见配置。对于JavaScript而言,这总是由“宿主”环境决定的。JavaScript最常见的宿主环境,也是大多数开发人员所用的,就是浏览器,它是事件驱动和异步的。
1.4.2 简单的异步程序
使用jQuery的Ajax例子:
$.post('/resource.json', function (data) {
console.log(data);
});
我们要在Node里做一个跟这个差不多的例子,不过这次是用文件系统(fs )模块从硬盘中加载resource.json
var fs = require('fs');
fs.readFile('./resource.json', function (er, data) {
console.log(data);
})
1.4.3 Hello World HTTP服务器
Node常被用来构建服务器。如果你过去习惯于把程序部署到服务器中运行(比如把PHP程序放到Apache HTTP服务器上),可能会觉得这种方式很怪异。在Node中,服务器和程序是一样 的。下面是个简单的HTTP服务器实现,它会用“Hello World”响应所有请求:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(3000);
console.log('Server running at http://localhost:3000/');
下面是同一服务器的另一种写法,这样看起来request 事件更明显:
var http = require('http');
var server = http.createServer();
server.on('request', function (req, res) {  //为request设置一个事件监听器
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
})
server.listen(3000);
console.log('Server running at http://localhost:3000/');
1.4.4 流数据
Node在数据流和数据流动上也很强大。你可以把数据流看成特殊的数组,只不过数组中的数据分散在空间上,而数据流中的数据是分散在时间上 的。通过将数据一块一块地传送,开发人员可以每收到一块数据就开始处理,而不用等所有数据都到全了再做处理。下面我们用数据流的方式来处理resource.json:
var stream = fs.createReadStream('./resource.json')
stream.on('data', function (chunk) { //当有新的数据块准备好时会激发data事件
console.log(chunk)
})
stream.on('end', function () {
console.log('finished')
})
只要有新的数据块准备好,就会激发data 事件,当所有数据块都加载完之后,会激发一个end 事件。由于数据类型不同,数据块的大小可能会发生变化。有了对读取流的底层访问,程序就可以边读取边处理,这要比等着所有数据都缓存到内存中再处理效率高得多。Node中也有可写数据流,可以往里写数据块。当HTTP服务器上有请求过来时,对其进行响应的res 对象就是可写数据流的一种。
我们借用一下前面那个HTTP服务器,看看如何把一张图片流到客户端:
var http = require('http');
var fs = require('fs');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'image/png'});
fs.createReadStream('./image.png').pipe(res); //设置一个从读取流到写出流的管道
}).listen(3000);
console.log('Server running at http://localhost:3000/');
小结
Node跟所有技术一样,并不是万能灵药。它只能解决特定的问题,并为我们开创新的可能性。不管你是做什么的,我们都希望你能了解Node到底适合帮你完成什么样的任务。回顾一下,Node是:
- 构建在JavaScript之上的;
- 事件触发和异步的;
- 专为数据密集型实时程序设计的。
第2章 构建有多个房间的聊天室程序
第4章 构建Node Web程序
本章内容
- 用Node的API处理HTTP请求
- 构建一个RESTful Web服务
- 提供静态文件服务
- 接受用户在表单中输入的数据
- 用HTTPS加强程序的安全性
本章将向你介绍Node为创建HTTP服务器所提供的工具,将学会如何处理其他常见的Web程序需求,比如创建底层的RESTful Web服务,接受用户通过HTML表单输入的数据,监测文件上传进度,以及用Node的安全套接字层(SSL)增强Web程序的安全性等。Node的核心是一个强大的流式HTTP解析器,大概由1500行经过优化的C代码组成,是Node的作者Ryan Dahl写的。这个解析器跟Node开放给JavaScript的底层TCP API相结合,为你提供了一个非常底层,但也非常灵活的HTTP服务器。

跟Node的大多数核心模块一样,http 模块也很简单。高层的“含糖”API被留给了Connect或Express这样的第三方框架,这样极大地简化了Web程序的构建过程。
4.1 HTTP服务器的基础知识
就像我们在本书中一再提及的那样,Node的API相对来说比较底层。跟PHP之类的语言或其他框架相比,Node的HTTP接口一样比较底层,不过这是为了保证它的速度和灵活性。为了让你能创建出既健壮又高效的Web程序,本节将重点讨论下面这些内容:
- Node如何向开发者呈现HTTP请求;
- 如何编写一个简单的HTTP服务器,用“Hello World”做响应;
- 如何读取请求头,以及如何设置响应头;
- 如何设置HTTP响应的状态码。
4.1.1 Node如何向开发者呈现HTTP请求
Node中的http 模块提供了HTTP服务器和客户端接口:
var http = require('http');
创建HTTP服务器要调用http.createServer() 函数。它只有一个参数,是个回调函数,服务器每次收到HTTP请求后都会调用这个回调函数。这个请求回调会收到两个参数,请求和响应对象,通常简写为req 和res :
var http = require('http');
var server = http.createServer(function(req, res){
// 处理请求
});
Node不会自动往客户端写任何响应。在调用完请求回调函数之后,就要由你负责用res.end() 方法结束响应了(见图4-2)。这样在结束响应之前,你可以在请求的生命期内运行任何你想运行的异步逻辑。如果你没能结束响应,请求会挂起,直到客户端超时,或者它会一直处于打开状态。Node服务器是长期运行的进程,在它的整个生命期里,它会处理很多请求。
