03-创建一个静态网站服务器都需要什么
[toc]
5.2 创建一个静态网站服务器都需要什么
为什么不用_ _dirname 在本书的部分例子中,我对Web文件的路径进行了硬编码,比如典型的/home/examples/ publichtml,你可能好奇我为什么不使用 _
dirname
。 在Node中,你可以使用预定义的dirname
来获取当前的工作目录。但是,在本章的例子中,我访问的文件是独立于我的Node程序工作目录的,所以我没有使用dirname
。除了这种情况,你都应该使用dirname
。它提供了一种不需要变更根目录变量值,就可以让你的程序在测试环境和产品环境都正常运行的方法。dirname
要通过下面这种方式来使用: 请留意前面的下划线。
我们已经知道在Node中创建一个简单的路由或静态文件服务器所需的所有功能。但是能做跟能简单地做是两件事。
当考虑创建一个简单且功能完善的静态文件服务器时,你可能会想到下面的一系列步骤。
(1)创建一个用来监听请求的HTTP服务器。
(2)请求到来时,解析请求中的URL并确定文件的位置。
var pathname = _ _dirname + req.url;
(3)检查文件是否存在。
(4)如果文件不存在,给出合适的返回。
(5)如果文件存在,打开并读取文件。
(6)准备响应头。
(7)把文件写入响应。
(8)等待下一个请求。
我们只需要核心模块来执行所有这些功能(只有一个例外,本节后面会看到)。创建HTTP服务器和阅读文件需要HTTP和文件系统模块。另外,我们需要为基础目录定义一个全局变量,或者使用预定义的 __dirname
(在5.2节的“为什么不用 dirname
”专栏中会讲到更多)。
应用程序最前面有这样一段代码:
var http = require('http'),
fs = require('fs'),
base = '/home/examples/public_html';
我们在第1章中的“Hello World”例子中创建了一个Web服务器,本章我们会在这个基础上编写代码。 http.createServer()
函数创建了一个服务器,它的回调函数里有两个值:Web请求和我们将要创建的响应。应用程序可以通过HTTP请求的 url
属性来获取所请求的文档。为了确保响应正确,我们会在 console.log
中打印请求的文件路径名。这是对服务器第一次启动时 console.log中纪录的
信息的补充:
var http = require('http'),
fs = require('fs'),
base = '/home/examples/public_html';
http.createServer(function (req, res) {
pathname = base + req.url;
console.log(pathname);
}).listen(8124);
console.log('Server web running at 8124');
运行这个程序会启动一个Web服务器,它会在8124端口监听Web请求。
关于端口 当你在浏览器上访问网页时,一般不需要提供端口号,因为有两个广为人知的端口号——HTTP请求的80端口和HTTPS请求的443端口。浏览器在发请求时会自动把它们包含在响应中。维基百科维护了一个常用端口的列表。 我们监听8214端口,因为默认的80端口通常由开发环境中的主要Web服务器管理,无论是Ngnix、Apache、lighttpd,还是较新的Caddy或产品环境上的Node应用程序。 我们可以用著名的80端口,但是使用1024以下的端口都需要root(管理员)权限,甚至连Apache都不提供以root身份运行的服务器实例。Apache本身是用root运行的,但它产生的子进程却有着更严格的权限控制。你不会想要用root权限运行Web服务器的。 你可以调整iptables等服务器内部端口,将端口1024重定向到端口80,但我不会采用这种方式。更好的方式是设置你的Web服务(Apache、Ngnix、lighttpd或Caddy),让它将请求代理发送到该Web服务器上。更多的详细信息请参见第5.3节。
使用index.html测试程序,会产生如下输出(输出内容取决于你的开发环境):
/home/examples/public_html/index.html
当然,浏览器会卡住,因为我们并没有准备响应,不过依然会得到上面的结果。
我们可以在打开文件和读取内容之前测试一下请求的文件是否可用。如果文件不存在,则 fs.stat()
函数会返回错误。
**检查后文件被删除** 在状态检查和实际打开文件之间的这段时间,有可能会发生一些事情从而导致文件被删除,所以,使用 `fs.stats()` 是有风险的。另一种做法就是直接打开文件,如果文件不存在的话会返回一个错误信息。但是,直接使用像 `fs.open()` 这样的函数、然后将文件描述符传递给 `fs.createReadStream()` 的问题是,它们不会告诉你该文件是目录还是文件(也不会告诉你文件是丢失还是锁定)。所以我会使用 `fs.stat(),` 在打开可读流的时候它会检查错误,这让我可以更好地进行错误处理。
说到读取文件,我们可以用 fs.readFile()
读取文件内容。不过 fs.readFile()
的问题在于,它会在使用文件前把文件全部读取到内存中。通过服务器读取的文档可能会很大。而且,同一时间内可能会有许多文档请求。像 fs.readFile()
这样的函数无法适用于这样的场景。
我们的程序并没有使用 fs.readFile()
,而是通过 fs.createStream()
方法的默认设置创建了一个可读流。那么将文件内容直接传递给 HTTP response
对象就是变得很简单了。可续流在读取完成后会发送结束信号,因此不需要在可读流上调用结束方法:
res.setHeader('Content-Type', 'test/html');
// create and pipe readable stream
var file = fs.createReadStream(pathname);
file.on("open", function() {
// 200 status - found, no errors
res.statusCode = 200;
file.pipe(res);
});
file.on("error", function(err) {
res.writeHead(403);
res.write('file missing, or permission problem');
console.log(err);
});
可读流中有两个值得关注的事件: open
和 error
。流准备就绪时会发生 open
事件,如果出问题则发生 error事件
。 error
事件代表哪种错误呢?它表示文件可能在状态检查的间隙消失,访问权限可能发生变化,或者该文件可能是一个子目录。既然我们不知道在这一刻发生了什么,那么只能给出一个通用的403错误,它会覆盖大多数可能出现的问题。我们发送了一个错误消息,它可能会显示也可能不显示,这取决于浏览器如何处理这个错误。
应用程序在 open
事件的回调函数中调用了 pipe
方法。就这一点而言,上文中的静态文件服务器和例5-3中的应用程序非常相似。
例5-3 一个简单的静态文件服务器
var http = require('http'),
fs = require('fs'),
base = '/home/examples/public_html';
http.createServer(function (req, res) {
pathname = base + req.url;
console.log(pathname);
fs.stat(pathname, function(err,stats) {
if (err) {
console.log(err);
res.writeHead(404);
res.write('Resource missing 404\n');
res.end();
} else {
res.setHeader('Content-Type', 'text/html');
// create and pipe readable stream
var file = fs.createReadStream(pathname);
file.on("open", function() {
res.statusCode = 200;
file.pipe(res);
});
file.on("error", function(err) {
console.log(err);
res.writeHead(403);
res.write('file missing or permission problem');
res.end();
});
}
});
}).listen(8124);
console.log('Server running at 8124');
我用一个简单的HTML文件来测试。该文件中只有一个 img
元素,它能够正常加载和显示:
<!DOCTYPE html>
<head>
<title>Test</title>
<meta charset="utf-8" />
</head>
<body>
<img src="./phoenix5a.png" />
</body>
我也测试了文件不存在的情况。在我调用 fs.stat()
的时候出错了,返回给浏览器一个404,同时在控制台中打印了文件或目录不存在的错误。
接着我用一个没有读权限的文件进行测试。这次可读流报错了,向浏览器发送了一个文件无法读取的错误,并且在控制台中打印权限相关的错误。我也收到一个更适合这个错误的HTTP状态403,它表示没有权限。
最后,我用另外一个示例文件——一个包含 HTML5 video
元素的文件对服务器进行了测试:
<!DOCTYPE html>
<head>
<title>Video</title>
<meta charset="utf-8" />
</head>
<body>
<video id="meadow" controls>
<source src="videofile.mp4" />
<source src="videofile.ogv" />
<source src="videofile.webm" />
</video>
</body>
我在谷歌浏览器中打开页面时文件可以打开,视频也可以显示,但是当我在IE10上测试的时候 video
元素却不生效。控制台中的消息解释了原因:
Server running at 8124/
/home/examples/public_html/html5media/chapter1/example2.html
/home/examples/public_html/html5media/chapter1/videofile.mp4
/home/examples/public_html/html5media/chapter1/videofile.ogv
/home/examples/public_html/html5media/chapter1/videofile.webm
虽然IE10能够播放MP4视频,但它会测试3个视频,因为每个视频的响应头中的内容类型都是 text/html
。虽然其他浏览器会忽略不正确的内容类型并正确显示媒体,但在我看来,IE10显然不会这样,否则我也不会这么快发现程序的错误。
微软在最新的Edge浏览器中处理了内容类型的问题,让其能播放相应的视频。不过,我们希望应用程序能做正确的事情。所以我们必须对其进行修改以测试每个文件的文件扩展名,然后在响应头中返回适合的MIME类型。我们可以自己编写这个功能,不过,我更愿意用一个现有的模块: Mime
。
你可以通过npm安装 `Mime` : `npm install mime` 。详见该模块的GitHub官方网站。
在给定文件名(带或者不带路径都可以)时, Mime
模块可以返回恰当的MIME类型,在给定内容类型时则可以返回文件扩展名。引入 Mime
模块:
var mime = require('mime');
返回的内容类型会被用于响应头,也会被打印到控制台,这样我们就可以在测试时检查这个值了:
// content type
var type = mime.lookup(pathname);
console.log(type);
res.setHeader('Content-Type', type);
这时,如果我们在IE10中用 video
元素访问这个文件,视频文件就能正常播放了。而且在所有的浏览器中,都返回了正确的内容类型。
但是当我们访问的是一个目录而不是一个文件时,就会出问题——一个错误信息会被打印到控制台上。
{ [Error: EISDIR, illegal operation on a directory] errno: 28, code: 'EISDIR' }
对于浏览器,会发生什么样的事情就很难说了。Edge会显示一个普通的403错误,如图5-1所示。Firefox和Chrome都会显示自己的“something is wrong”信息。但是无法访问一个子目录和读取文件发生错误时的错误信息是不一样的。
我们不仅需要检查资源的存在性,还需要检查资源是文件还是目录。如果要访问的是个目录,我们可以显示目录的内容,也可以显示一个错误信息——取决于开发者喜欢哪种方式。
还有一个变化。我所使用的路径在我的Linux服务器上没什么问题,但是在Windows机器上就会出问题。比如说,在我的Windows机器上,并没有/home/examples/ public_html这个目录。又比如,Windows系统都支持反斜线(\),而不是斜线。
要确保程序在两种环境下都能正常运行,需要先在Windows中创建相应的目录,然后使用核心模块 Path
来标准化路径字符串,以确保它在两个环境下都能用。 path.normalize()
函数接收一个字符串作为参数,如果对于当前环境它已经是标准路径,则原样返回,如果不是的话,就会返回一个转换之后的字符串。
var pathname = path.normalize(base + req.url);
现在这个程序在我的两台计算机上都能用了。
第6章会介绍更多关于Path模块的内容。
我们的简化静态文件服务器的最终版本,如例5-4所示,使用 fs.stats
函数检查所请求对象是否存在,以及它是否是文件。如果资源不存在,则返回一个404的HTTP状态。如果资源存在但是是一个目录,则返回一个403的HTTP状态——意思是禁止访问。如果文件被锁定了,也会返回同样的状态码,只是消息不同。这样一来,每种情况都有其对应的请求响应了。
例5-4 简化静态文件服务器的最终版本
var http = require('http'),
url = require('url'),
fs = require('fs'),
mime = require('mime'),
path = require('path');
var base = '/home/examples/public_html';
http.createServer(function (req, res) {
pathname = path.normalize(base + req.url);
console.log(pathname);
fs.stat(pathname, function(err, stats) {
if (err) {
res.writeHead(404);
res.write('Resource missing 404\n');
res.end();
} else if (stats.isFile()) {
// content type
var type = mime.lookup(pathname);
console.log(type);
res.setHeader('Content-Type', type);
// create and pipe readable stream
var file = fs.createReadStream(pathname);
file.on("open", function() {
// 200 status - found, no errors
res.statusCode = 200;
file.pipe(res);
});
file.on("error", function(err) {
console.log(err);
res.statusCode = 403;
res.write('file permission');
res.end();
});
} else {
res.writeHead(403);
res.write('Directory access is forbidden');
res.end();
}
});
}).listen(8124);
console.log('Server running at 8124');
下面是使用Firefox来访问一个包含图片和视频文件链接的页面时,控制台所打印出来的内容:
/home/examples/public_html/video.html
text/html
/home/examples/public_html/phoenix5a.png
image/png
/home/examples/public_html/videofile.mp4
video/mp4
/home/examples/public_html/favicon.ico
image/x-icon
/home/examples/public_html/favicon.ico
image/x-icon
请注意所有的内容类型都被正确地处理了——除了Firefox和Chrome在打开页面时自动发出的favicon请求。
当加载一个包含video元素的页面、并且播放它的时候,你会对读取文件流的工作原理有一个更清晰的认识。浏览器会以一个可控的速度来抓取读取流中的内容,将其装入自己内部的缓存中,然后暂停读取。如果你在播放过程中关闭服务器,那么视频会继续播放到缓存所加载的位置。然后video元素会变成一片空白,因为读取流已经不存在了。我们只需要很少量的操作,这一切就都有条不紊地运行起来了,是不是很神奇?
虽然这个程序已经用多种不同的文档测试过了,但是它依然不完美。它无法处理很多其他类型的Web请求,也不能处理安全性问题和缓存问题,而且也不能正确地处理视频请求。我曾经测试过一个使用HTML来播放视频的Web应用程序,程序使用了HTML5中 video
元素的API来显示视频加载进度。而目前这个程序还做不到这一点。
在创建一个静态文件服务器的过程中,我们遇到了很多的陷阱。所以人们倾向于使用更复杂一些的系统,比如Express。我会在第9章进行详细的讲解。