一,利用Node搭建静态服务器
这个是这个项目的底层支撑部分。用来支持静态资源文件像html, css, gif, jpg, png, javascript, json, plain text等等静态资源的访问。这里面是有一个mime类型的文件映射。
mime.js
/**
* mime类型的 map
* @ author Cheng Liufeng
* @ date 2014/8/30
* 当请求静态服务器文件的类型 html, css, gif, jpg, png, javascript, json, plain text, 我们会在此文件进行映射
*/
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"
};
这里面我先解释一下从输入网址到页面出现的过程。 当用户在浏览器地址栏里面输入一个url的时候。
接下来会发生一系列的过程。首先是DNS解析, 将域名转换成对应的IP地址,之后浏览器与远程Web服务器通过TCP三次握手协商来建立一个TCP/IP连接。该握手包括一个同步报文,开一个同步-应答报文和一个应答报文,这三个报文在浏览器和服务器之间传递。该握手首先由客户端尝试建立起通信,而后服务器应答并接受客户端的请求,最后由客户端发出该请求已经被接受的报文。一旦TCP/IP连接建立,浏览器会通过该连接向远程服务器发送HTTP的GET请求。
远程服务器找到资源并使用HTTP响应返回该资源,值为200的HTTP响应状态表示一个正确的响应。此时,Web服务器提供资源服务,客户端开始下载资源。下载的资源包括了html文件,css文件,javascript文件,image文件。然后开始构建一颗渲染树和一颗DOM树,期间会有css阻塞和js阻塞。所以底层是需要一个静态服务器支撑。这里面我原生构造一个静态服务器,不采用express框架。
事实上每一次资源文件请求的过程是一次次GET请求。下面我解释一下客户端(浏览器端或者采用linux下采用curl方式)的GET请求所对应的服务端处理过程。一次Get请求发送到服务端后,服务端可以根据GET请求对应一个资源文件的路径。知道了这个路径后,我们就可以采用文件读写的方式获取指定路径下的资源,然后返回给客户端。
我们知道Node里面的文件读写的API有readFile和readFileSync,但是更好的方式是采用流的方式去读取文件,采用流的方式的优点是可以采用缓存和gzip压缩。
OK,那么如何实现缓存呢?通常情况下,客户端第一次去请求的时候,服务端会读取资源文件,返回给客户端。但是第二次再去请求同样的文件时,这个时候还是需要发送一次请求到服务端。服务端会根据Expires, cache-control, If-Modified-Since等Http头信息判断这个资源是否已经缓存过。如果有缓存,服务端则不会再次访问资源文件的实际路径。直接返回缓存的资源。
server.js
/** * 聊天室服务端 * 功能:实现了Node版的静态服务器 * 实现了缓存,gzip压缩等 * @ author Cheng Liufeng * @ date 2014/8/30 */ // 设置端口号 var PORT = 3000; // 引入模块 var http = require('http'); var url = require('url'); var fs = require('fs'); var path = require('path'); var zlib = require('zlib'); // 引入文件 var mime = require('./mime').types; var config = require('./config'); var chatServer = require('./utils/chat_server'); var server = http.createServer(function (req, res) { res.setHeader("Server","Node/V8"); // 获取文件路径 var pathName = url.parse(req.url).pathname; if(pathName.slice(-1) === "https://www.jb51.net/"){ pathName = pathName + "index.html"; //默认取当前默认下的index.html } // 安全处理(当使用Linux 的 curl命令访问时,存在安全隐患) var realPath = path.join("client", path.normalize(pathName.replace(/\.\./g, ""))); // 检查文件路径是否存在 path.exists(realPath, function(exists) { // 当文件不存在时的情况, 输出一个404错误 if (!exists) { res.writeHead(404, "Not Found", {'Content-Type': 'text/plain'}); res.write("The request url" + pathName +" is not found!"); res.end(); } else { // 当文件存在时的处理逻辑 fs.stat(realPath, function(err, stat) { // 获取文件扩展名 var ext = path.extname(realPath); ext = ext ? ext.slice(1) : "unknown"; var contentType = mime[ext] || "text/plain"; // 设置 Content-Type res.setHeader("Content-Type", contentType); var lastModified = stat.mtime.toUTCString(); var ifModifiedSince = "If-Modified-Since".toLowerCase(); res.setHeader("Last-Modified", lastModified); if (ext.match(config.Expires.fileMatch)) { var expires = new Date(); expires.setTime(expires.getTime() + config.Expires.maxAge * 1000); res.setHeader("Expires", expires.toUTCString()); res.setHeader("Cache-Control", "max-age=" + config.Expires.maxAge); } if (req.headers[ifModifiedSince] && lastModified == req.headers[ifModifiedSince]) { res.writeHead(304, "Not Modified"); res.end(); } else { // 使用流的方式去读取文件 var raw = fs.createReadStream(realPath); var acceptEncoding = req.headers['accept-encoding'] || ""; var matched = ext.match(config.Compress.match); if (matched && acceptEncoding.match(/\bgzip\b/)) { res.writeHead(200, "Ok", {'Content-Encoding': 'gzip'}); raw.pipe(zlib.createGzip()).pipe(res); } else if (matched && acceptEncoding.match(/\bdeflate\b/)) { res.writeHead(200, "Ok", {'Content-Encoding': 'deflate'}); raw.pipe(zlib.createDeflate()).pipe(res); } else { res.writeHead(200, "Ok"); raw.pipe(res); }
//下面是普通的读取文件的方式,不推荐