一个最简单的 Web Server 之功能包含下列三个步骤:步骤一 : 接收浏览器所传来的网址;步骤二 : 取出相对应的文件;步骤三 : 将文件内容传回给浏览器。然而、在这个接收与传回的过程中,所有的资讯都必须遵照固定的格式,规范这个接收/传送格式的协议,称为超文字传送协议 (Hyper Text Transfer Protocol),简称为 HTTP 协议。HTTP 协议格式的基础,乃是建构在网址 URL 上的传输方式,早期只能用来传送简单的 HTML 档桉,后来经扩充后也可以传送 其他类型的档桉,包含 影像、动画、简报、Word 文件等。
在本文中,我们将先简介 HTTP 协议的讯息内容,然后在介绍如何以 Node 实现 HTTP 协议,以建立一个简单的 Web Server。
HTTP 协议
当你在浏览器上打上网址(URL)后,浏览器会传出一个 HTTP 头信息给对应的 Web Server,Web Server 再接收到这个内容后, 根据网址取出对应的文件,并将该文件以 HTTP 格式的内容传回给浏览器,以下是这个过程的一个范例。
某仁兄上网,在浏览器中打上 http://163.com,于是,浏览器传送下列内容给 163.com 这台电脑。
GET /index.htm HTTP/1.0
Accept: image/gif, image/jpeg, application/msword, */*
Accept-Language: zh-ch
User-Agent: Mozilla/4.0
Content-Length:
Host: 163.com
Cache-Control: max-age=259200
Connection: keep-alive
当 163.com 电脑上的 Web Server 程序收到上述内容后,会取出指定的路径 /index.htm ,然后根据预设的网页根目录 (假设为 c:\web\),合成一个 c:\web\index.htm 的绝对路径,接着从硬盘中取出该文件,并传回下列内容给那位仁兄的浏览器。
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 438
<html>
....
</html>
其中第一行 HTTP/1.0 200 OK 代表该网页被成功传回,第二行 Content-Type: text/html 代表传回文件为 HTML 文件, Content-Length: 438 代表该 HTML 文件的大小为 438 位字节。
延时阅读
P.S:由于诸多原因的关系,小弟已经很久没怎么接触 NodeJS 了。其实我对 NodeJS 不但非常感兴趣,而且还十分看好。于是今天趁有时间,并挟持着对 IIS / IIS Express、又或者 Apache 它们“累积已久的情绪”,决心打造一个基于 NodeJS 的静态服务器!
哈哈,要说 NodeJS 的静态服务器,前辈们已有诸多实践,并都付之笔墨与大家共享,尝试列举如下:
- 《Node.js静态文件服务器实战》朴灵大大的 Node 文章,非常详细,不可不说
- 《完成静态服务器——Node.js摸石头系列之四》,还有其他关于 node 的文章
- 《node.js入门—-静态文件服务器》,实现了 GZIP,这点要学习,,
在 Node 上面实现一个静态服务器应该不是一件很难的事情。以上三个链接只是打算给尚不熟悉 Web Server 或者对 Node 不太了解的朋友去了解一下那些原理的知识,当然还可以深入地 Google/Baidu 之。如果你和我一样,喜欢通过阅读源码来了解 Node 静态服务器是怎么一回事的话,那请您和我走一趟源码之旅(附注释)。本文在 WinXP + Node 0.6.21下通过,服务器源码选用 Andy Green 的开源项目,主要的资源链接如下:
服务器文件 Server.js,加上注释:
-
-
-
-
-
-
-
-
-
- 'use strict';
-
-
- var CONFIG = {
-
- 'host': '127.0.0.1',
- 'port': 80,
-
- 'site_base': './site',
-
- 'file_expiry_time': 480,
-
- 'directory_listing': true
-
- };
-
-
- var MIME_TYPES = {
-
- '.txt': 'text/plain',
- '.md': 'text/plain',
- '': 'text/plain',
- '.html': 'text/html',
- '.css': 'text/css',
- '.js': 'application/javascript',
- '.json': 'application/json',
- '.jpg': 'image/jpeg',
- '.png': 'image/png',
- '.gif': 'image/gif'
-
- };
-
-
- var EXPIRY_TIME = (CONFIG.file_expiry_time * 60).toString();
-
-
- var HTTP = require('http');
- var PATH = require('path');
- var FS = require('fs');
- var CRYPTO = require('crypto');
- var CUSTARD = require('./custard');
-
- var template_directory = FS.readFileSync('./templates/blocks/listing.js');
-
-
-
-
- function ResponseObject( metadata ){
-
- this.status = metadata.status || 200;
- this.data = metadata.data || false;
- this.type = metadata.type || false;
-
- }
-
-
- ResponseObject.prototype.getEtag = function (){
- var hash = CRYPTO.createHash( 'md5' );
- hash.update( this.data );
- return hash.digest( 'hex' );
- };
-
-
-
-
- function handleRequest( url, callback ){
-
- if ( PATH.extname( url ) === '' ){
- getDirectoryResponse( url, function ( response_object ){
- callback( response_object );
- } );
- }
- else {
-
- getFileResponse( url, function ( response_object ){
- callback( response_object );
- } );
- }
-
- }
-
-
-
-
- function getFileResponse( path, callback ){
-
- var path = CONFIG.site_base + path;
-
- PATH.exists( path, function ( path_exists ){
- if ( path_exists ){
- FS.readFile( path, function ( error, data ){
- if ( error ){
-
- callback( new ResponseObject( { 'data': error.stack, 'status': 500} ) );
- }
- else {
-
- callback( new ResponseObject({
- 'data': new Buffer( data )
- ,'type': MIME_TYPES[PATH.extname(path)]
- })
- );
- }
- } );
- }
- else {
-
- callback( new ResponseObject( { 'status': 404} ) );
- }
- } );
-
- }
-
-
-
-
- function getDirectoryResponse( path, callback ){
-
- var full_path = CONFIG.site_base + path;
- var template;
- var i;
-
- if ( CONFIG.directory_listing ){
- PATH.exists( full_path, function ( path_exists ){
- if ( path_exists ){
- FS.readdir( full_path, function ( error, files ){
- if ( error ){
-
- callback( new ResponseObject( { 'data': error.stack, 'status': 500} ) );
- }
- else {
-
-
- template = new CUSTARD;
-
- template.addTagSet( 'h', require('./templates/tags/html') );
- template.addTagSet( 'c', {
- 'title': 'Index of ' + path,
- 'file_list': function ( h ){
- var items = [];
- var stats;
- for ( i = 0; i < files.length; i += 1 ){
- stats = FS.statSync( full_path + files[i] );
- if ( stats.isDirectory() ){
- files[i] += '/';
- }
- items.push( h.el( 'li', [
- h.el( 'a', { 'href': path + files[i]}, files[i] )
- ] ) );
- }
- return items;
- }
- } );
-
- template.render( template_directory, function ( error, html ){
- if ( error ){
-
- callback( new ResponseObject( { 'data': error.stack, 'status': 500} ) );
- }
- else {
- callback( new ResponseObject( { 'data': new Buffer( html ), 'type': 'text/html'} ) );
- }
- } );
- }
- } );
- }
- else {
-
-
- callback( new ResponseObject( { 'status': 404} ) );
- }
- } );
- } else {
-
-
- callback( new ResponseObject( { 'status': 403} ) );
- }
-
- }
-
-
-
-
- HTTP.createServer( function ( request, response ){
-
- var headers;
- var etag;
-
- if ( request.method === 'GET' ){
-
- handleRequest( request.url, function ( response_object ){
- if ( response_object.data && response_object.data.length > 0 ){
- etag = response_object.getEtag();
-
- if ( request.headers.hasOwnProperty('if-none-match') && request.headers['if-none-match'] === etag ){
-
- response.writeHead( 304 );
- response.end();
- }
-
- else {
- headers = {
- 'Content-Type': response_object.type,
- 'Content-Length' : response_object.data.length,
- 'Cache-Control' : 'max-age=' + EXPIRY_TIME,
- 'ETag' : etag
- };
- response.writeHead( response_object.status, headers );
- response.end( response_object.data );
- }
- }
- else {
- response.writeHead( response_object.status );
- response.end();
- }
- } );
- }
- else {
-
- response.writeHead( 403 );
- response.end();
- }
-
- } ).listen( CONFIG.port, CONFIG.host );
-
- console.log( 'Site Online : http://' + CONFIG.host + ':' + CONFIG.port.toString() + '/' );
粗略浏览源码后,首先感觉清晰可读,再则从功能上议,它已经实现了 目录读取、MIME 类型、404 页面还有HTTP 缓存的功能,另外于我个人而言,又再一次温习了 HTTP 协议内容,对于深入理解 缓存 也就是 Etag 的使用很有帮助。总之,这个小小的静态文件服务器例子,不过 250 行,可谓“麻雀虽小,五脏俱全”,呵呵,这还得拜强大的 NodeJS 所赐!
ps: GZip 实现的方法(参考上述提到的 url,最后一个):
- var zlib = require('zlib');
-
- ...
-
-
- function readFile(req, res, realPath, header, type){
- var raw = fs.createReadStream(realPath), cFun;
-
- if(setting.compress && setting.compress.match
- && type.match(setting.compress.match) && req.headers['accept-encoding']){
- if(req.headers['accept-encoding'].match(/\bgzip\b/)){
- header['Content-Encoding'] = 'gzip';
- cFun = 'createGzip';
- }else if(req.headers['accept-encoding'].match(/\bdeflate\b/)){
- header['Content-Encoding'] = 'deflate';
- cFun = 'createDeflate';
- }
- }
- res.writeHead(200, header);
- if(cFun){
- raw.pipe(zlib[cFun]()).pipe(res);
- }else{
- raw.pipe(res);
- }
- }
2012-11-02: 经朴灵大大指点,可以“如果在实际工作中需要用到静态文件服务器的话,npm install anywhere -g 然后在你的任意目录下执行anywhere命令就可以把这个目录变成一个静态文件服务器的根目录哦~。”,十分方便的说~