nodejs初探 搭建一个类似 apache 的服务器

#针对于项目而言
我们需要明白的是

* 项目大多数的文件都是属于静态文件,只有数据部分存在动态请求。
* 数据部分的请求都呈现为RESTful的特性。
* 所以项目主要包含两个部分就是静态服务器和RESTful服务器。
###section one :创建一个静态服务器


1.创建一个以 app.js 为入口的文件

app.js

<pre>
var PORT = 8000;
var http = require('http');
var server = http.createServer(function (request, response) {
// TODO
});
server.listen(PORT);
console.log("Server runing at port: " + PORT + ".");
</pre>
###静态文件服务器的功能:

* 浏览器发送URL,服务端解析URL,对应到硬盘上的文件。
* 如果文件存在,返回200状态码,并发送文件到浏览器端;如果文件不存在,返回404状态码,发送一个404的文件到浏览器端。

### section two :实现路由的功能


* 添加url模块是必要的,然后解析pathname。

以下是实现代码:

<pre>
var server = http.createServer(function (request, response) {
var pathname = url.parse(request.url).pathname;
response.write(pathname);
response.end();
});

</pre>

 

###读取静态文件

为了不让用户在浏览器端通过请求/app.js查看到我们的代码,我们设定用户只能请求assets目录下的文件。
服务器会将路径信息映射到assets目录。

文件读取就跟fs(file system)这个模块有关系。
同样,涉及到了路径处理,path模块也是需要的。

我们通过path模块的path.exists方法来判断静态文件是否存在磁盘上。
加入不存在我们直接响应给客户端404错误。

如果文件存在则调用fs.readFile方法读取文件。
如果发生错误,我们响应给客户端500错误,表明存在内部错误。
正常状态下则发送读取到的文件给客户端,表明200状态。

<pre>
var server = http.createServer(function (request, response) {
var pathname = url.parse(request.url).pathname;
var realPath = "assets" + pathname;
path.exists(realPath, function (exists) {
if (!exists) {
response.writeHead(404, {
'Content-Type': 'text/plain'
});

response.write("This request URL " + pathname + " was not found on this server.");
response.end();
} else {
fs.readFile(realPath, "binary", function (err, file) {
if (err) {
response.writeHead(500, {
'Content-Type': 'text/plain'
});

response.end(err);
} else {
response.writeHead(200, {
'Content-Type': 'text/html'
});

response.write(file, "binary");

response.end();
}
});
}
});
});
</pre>

 

以上这段简单的代码加上一个assets目录,就构成了我们最基本的静态文件服务器。

那么眼尖的你且看看,这个最基本的静态文件服务器存在哪些问题呢?
答案是MIME类型支持。因为我们的服务器同时要存放html, css, js, png, gif, jpg等等文件。
并非每一种文件的MIME类型都是text/html的。

###MIME类型支持

像其他服务器一样,支持MIME的话,就得一张映射表。
<pre>

exports.types = {
"css": "text/css",
"gif": "image/gif",
"html": "text/html",
"ico": "image/x-icon",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"js": "text/javascript",
"json": "application/json",
"pdf": "application/pdf",
"png": "image/png",
"svg": "image/svg+xml",
"swf": "application/x-shockwave-flash",
"tiff": "image/tiff",
"txt": "text/plain",
"wav": "audio/x-wav",
"wma": "audio/x-ms-wma",
"wmv": "video/x-ms-wmv",
"xml": "text/xml"
};I</pre>
以上代码另存在mime.js文件中。

我们要做的是引入这个 MIME.js 文件。
< pre>
var mime = require("./mime").types;
</pre>
我们通过path.extname来获取文件的后缀名。由于extname返回值包含”.”,所以通过slice方法来剔除掉”.”,对于没有后缀名的文件,我们一律认为是unknown。
<pre>
var ext = path.extname(realPath);
ext = ext ? ext.slice(1) : 'unknown';
</pre>
接下来我们很容易得到真正的MIME类型了。
<pre>
var contentType = mime[ext] || "text/plain";
response.writeHead(200, {'Content-Type': contentType});
response.write(file, "binary");
response.end();
</pre>
对于未知的类型,我们一律返回text/plain类型。

###缓存支持/控制

在MIME支持之后,静态文件服务器看起来已经很完美了。
任何静态文件只要丢进assets目录之后就可以万事大吉不管了。
看起来已经达到了Apache作为静态文件服务器的相同效果了。

*但是,我们发现用户在每次请求的时候,服务器每次都要调用fs.readFile方法去读取硬盘上的文件的。当服务器的请求量一上涨,硬盘IO会吃不消。*
在解决这个问题之前,
###我们有必要了解一番前端浏览器缓存的一些机制和提高性能的方案。

* GZip压缩文件可以减少响应的大小,能够达到节省带宽的目的。
* 浏览器缓存中存有文件副本的时候,不能确定有效的时候,会生成一个条件get请求。
* 在请求的头中会包含 If-Modified-Since。
* 如果服务器端文件在这个时间后发生过修改,则发送整个文件给前端。
* 如果没有修改,则返回304状态码。并不发送整个文件给前端。
* 如果副本有效,这个get请求都会省掉。判断有效的最主要的方法是服务端响应的时候带上Expires的头。
* 浏览器会判断Expires头,直到制定的日期过期,才会发起新的请求。
*另一个可以达到相同目的的方法是返回Cache-Control: max-age=xxxx。*


为了简化问题,我们只做如下这几件事情:

为指定几种后缀的文件,在响应时添加Expires头和Cache-Control: max-age头。超时日期设置为1年。
由于这是静态文件服务器,为所有请求,响应时返回Last-Modified头。
为带If-Modified-Since的请求头,做日期检查,如果没有修改,则返回304。若修改,则返回文件。
对于以上的静态文件服务器,Node给的响应头是十分简单的:
<pre>
Connection: keep-alive
Content-Type: text/html
Transfer-Encoding: chunked
<pre>
对于指定后缀文件和过期日期,为了保证可配置。那么建立一个config.js文件是应该的。
<pre>
exports.Expires = {
fileMatch: /^(gif|png|jpg|js|css)$/ig,
maxAge: 60 * 60 * 24 * 365
};
</pre>
引入config.js文件。

<pre>var config = require("./config");</pre>
我们在相应之前判断后缀名是否符合我们要添加过期时间头的条件。
<pre>
var ext = path.extname(realPath);
ext = ext ? ext.slice(1) : 'unknown';
if (ext.match(config.Expires.fileMatch)) {
var expires = new Date();
expires.setTime(expires.getTime() + config.Expires.maxAge * 1000);
response.setHeader("Expires", expires.toUTCString());
response.setHeader("Cache-Control", "max-age=" + config.Expires.maxAge);
}
</pre>
这次的响应头中多了两个header。
<pre>
Cache-Control: max-age=31536000
Connection: keep-alive
Content-Type: image/png
Expires: Fri, 09 Nov 2012 12:55:41 GMT
Transfer-Encoding: chunked
<pre>
浏览器在发送请求之前由于检测到Cache-Control和Expires
*(Cache-Control的优先级高于Expires,但有的浏览器不支持Cache-Control,这时采用Expires)*,如果没有过期,则不会发送请求,而直接从缓存中读取文件。

接下来我们为所有请求的响应都添加Last-Modified头。

读取文件的最后修改时间是通过fs模块的fs.stat()方法来实现的。


<pre>
fs.stat(realPath, function (err, stat) {
var lastModified = stat.mtime.toUTCString();
response.setHeader("Last-Modified", lastModified);
});
</pre>
我们同时也要检测浏览器是否发送了If-Modified-Since请求头。如果发送而且跟文件的修改时间相同的话,我们返回304状态。

if (request.headers[ifModifiedSince] && lastModified == request.headers[ifModifiedSince]) {
response.writeHead(304, "Not Modified");
response.end();
}
如果没有发送或者跟磁盘上的文件修改时间不相符合,则发送回磁盘上的最新文件。

通过Expires和Last-Modified两个方案以及与浏览器之间的通力合作,会节省相当大的一部分网络流量,同时也会降低部分硬盘IO的请求。如果在这之前还存在CDN的话,整个方案就比较完美了。

由于Expires和Max-Age都是由浏览器来进行判断的,如果判断成功,http请求都不会发送到服务端的,这里只能通过fiddler和浏览器配合进行测试。但是Last-Modified却是可以通过curl来进行测试的。
<pre>
curl --header "If-Modified-Since: Fri, 11 Nov 2011 19:14:51 GMT" -i http://localhost:8000

HTTP/1.1 304 Not Modified
Content-Type: text/html
Last-Modified: Fri, 11 Nov 2011 19:14:51 GMT
Connection: keep-alive
</pre>
*注意,我们看到这个304请求的响应是不带body信息的。所以,达到我们节省带宽的需求。只需几行代码,就可以省下许多的带宽费用。*


###GZip启用
*可以减少流量和带宽*
######要用到gzip,就需要zlib模块,该模块在Node的0.5.8版本开始原生支持。

<pre>var zlib = require("zlib");</pre>

对于图片一类的文件,不需要进行gzip压缩,所以我们在config.js中配置一个启用压缩的列表。
<pre>
exports.Compress = {
match: /css|js|html/ig
};
</pre>
这里为了防止大文件,也为了满足zlib模块的调用模式,将读取文件改为流的形式进行读取。
<pre>
var raw = fs.createReadStream(realPath);
var acceptEncoding = request.headers['accept-encoding'] || "";
var matched = ext.match(config.Compress.match);
if (matched && acceptEncoding.match(/\bgzip\b/)) {
response.writeHead(200, "Ok", {
'Content-Encoding': 'gzip'
});
raw.pipe(zlib.createGzip()).pipe(response);
} else if (matched && acceptEncoding.match(/\bdeflate\b/)) {
response.writeHead(200, "Ok", {
'Content-Encoding': 'deflate'
});
raw.pipe(zlib.createDeflate()).pipe(response);
} else {
response.writeHead(200, "Ok");
raw.pipe(response);
}
</pre>

posted @ 2015-11-29 20:09  露西涂  阅读(717)  评论(0编辑  收藏  举报