学习NodeJS第七天:安装静态的文件服务器

  一个最简单的 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 上面实现一个静态服务器应该不是一件很难的事情。以上三个链接只是打算给尚不熟悉 Web Server 或者对 Node 不太了解的朋友去了解一下那些原理的知识,当然还可以深入地 Google/Baidu 之。如果你和我一样,喜欢通过阅读源码来了解 Node 静态服务器是怎么一回事的话,那请您和我走一趟源码之旅(附注释)。本文在 WinXP + Node 0.6.21下通过,服务器源码选用 Andy Green 的开源项目,主要的资源链接如下:

服务器文件 Server.js,加上注释:

/*

	Node.js File Server
	Andy Green
	http://andygrn.co.uk
	November 2011

*/

'use strict';

// 配置对象。使用对象来配置,不错的编程方法!
var CONFIG = {

	'host': '127.0.0.1',			// 服务器地址
	'port': 80,				// 端口
	
	'site_base': './site', 			// 根目录,虚拟目录的根目录
	
	'file_expiry_time': 480, 		// 缓存期限 HTTP cache expiry time, minutes
	
	'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();

// 依赖模块,注意 CUSTARD 是自定义的模块,不是 NODE 类库自带的。 
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');


// 响应对象 An object representing a server response

function ResponseObject( metadata ){

	this.status = metadata.status || 200;
	this.data = metadata.data || false;
	this.type = metadata.type || false;

}

// 返回 HTTP Meta 的 Etag。可以了解 md5 加密方法
ResponseObject.prototype.getEtag = function (){
	var hash = CRYPTO.createHash( 'md5' );
	hash.update( this.data );
	return hash.digest( 'hex' );
};


// Filter server requests by type

function handleRequest( url, callback ){
	// 如果 url 只是 目录 的,则列出目录
	if ( PATH.extname( url ) === '' ){
		getDirectoryResponse( url, function ( response_object ){
			callback( response_object );
		} );
	}
	else {
	// 如果 url 是 目录 + 文件名 的,则返回那个文件
		getFileResponse( url, function ( response_object ){
			callback( response_object );
		} );
	}

}


// 处理文件的函数 Creates a ResponseObject from a local file path

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 ){
//					Internal error
					callback( new ResponseObject( {'data': error.stack, 'status': 500} ) );
				}
				else {
					// 读取 文件 返回 Response 
					callback( new ResponseObject({
							 'data': new Buffer( data )
							,'type': MIME_TYPES[PATH.extname(path)]
						}) 
					);
				}
			} );
		}
		else {
//			Not found
			callback( new ResponseObject( {'status': 404} ) );
		}
	} );

}


// 处理目录的方法 Creates a ResponseObject from a local directory path

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 ){
//						Internal error
						callback( new ResponseObject( {'data': error.stack, 'status': 500} ) );
					}
					else {
						// 列出结果
//						Custard template
						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 ){
//								Internal error
								callback( new ResponseObject( {'data': error.stack, 'status': 500} ) );
							}
							else {
								callback( new ResponseObject( {'data': new Buffer( html ), 'type': 'text/html'} ) );
							}
						} );
					}
				} );
			}
			else {
				// 找不到 文件,就是 404
//				Not found
				callback( new ResponseObject( {'status': 404} ) );
			}
		} );
	} else {
		// 禁止 目录浏览,返回 403
//		Forbidden
		callback( new ResponseObject( {'status': 403} ) );
	}

}


// 启动服务器 Start server

HTTP.createServer( function ( request, response ){

	var headers;
	var etag;
	
	if ( request.method === 'GET' ){ // 静态服务服务器都是 HTTP GET 方法的
//		Get response object
		handleRequest( request.url, function ( response_object ){
			if ( response_object.data && response_object.data.length > 0 ){
				etag = response_object.getEtag();
				// 命中缓存,返回 304
				if ( request.headers.hasOwnProperty('if-none-match') && request.headers['if-none-match'] === etag ){
//					Not Modified
					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 {
//		Forbidden
		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;
  //是否gzip
  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命令就可以把这个目录变成一个静态文件服务器的根目录哦~。”,十分方便的说~

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值