基础功能
之前我们通过http模块创建了一个简单的服务器,但是对于一个网络应用来说肯定是远远不够的,在聚义的业务中我们至少有如下要求:
- 请求方法的判断
- URL的路径解析
- URL中查询字符串的解析
- Cookie的解析
- Basic认证
- 表单数据的解析
- 任意格式的上传处理
- Session管理
一切的开始都是这个函数:
var server = http.createServer(function (req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
请求方法
最常见的请求方法就是GET和POST。我们可以根据这个来决定响应的行为。
function (req, res) {
switch (req.method) {
case 'POST':
update(req, res);
break;
case 'DELETE':
remove(req, res);
break;
case 'PUT':
create(req, res);
break;
case 'GET':
default:
get(req, res);
}
}
路径解析
比较常见的场景是根据路径来选择控制器
我们假装配置一个简单的控制器:
var handles = {};
handles.index = {};
handles.index.index = function (req, res, foo, bar) {
res.writeHead(200);
res.end("Rabbit&Lion"+foo+bar);
};
并在get方法里访问:
function get(req, res){
var pathname = url.parse(req.url).pathname;
var paths = pathname.split('/');
var controller = paths[1] || 'index';
var action = paths[2] || 'index';
var args = paths.slice(3);
if (handles[controller] && handles[controller][action]) {
handles[controller][action].apply(null, [req, res].concat(args));
} else {
res.writeHead(500);
res.end('no such controller');
}
}
查询字符串
可以使用现成的方法将url中的query字符串转为对象:
var query = url.parse(req.url, true).query;
注意如果相同的键在查询中出现两次,值会是一个数组而不是字符串。
Cookie
Cookie的处理分为以下这几个步骤:
- 服务器向客户端发送Cookie
- 浏览器储存住Cookie
- 之后每次访问这个域名时浏览器都会将这个Cookie发向服务器
服务器向客户端发送Cookie时是在响应头部加入:
Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
客户端向服务器发送时,cookie就在header里,所以通过req.headers.cookie就可以获取到,可以给处理成对象:
var parseCookie = function (cookie) {
var cookies = {};
if (!cookie) {
return cookies;
}
var list = cookie.split(';');
for (var i = 0; i < list.length; i++) {
var pair = list[i].split('=');
cookies[pair[0].trim()] = pair[1];
}
return cookies;
};
这里要注意的是Cookie对性能的影响,Cookie对设置路径的所有子路径都有效,所以在太高的根URL上设置就很不妥。
Session
Cookie有个问题,它是可以自己改的,所以在cookie中放一些敏感信息或权限什么的是不合适的。
Session的数据只保留在服务器端,并通过客户端发来的一些身份标识和用户对应起来。
基于Cookie
虽然不能在Cookie里放这些数据,但是将口令放在Cookie里是可以的。因为口令一旦被篡改,就丢失了映射关系。
使用查询字符串
这种直接放在URL里也是个很方便的办法,但是这不太安全,只要别人获得了这个URL就拥有与你相同的身份。
缓存
条件请求:使用If-Modified-Since来询问服务器是否有最新的改动,服务器端做如下处理:
var catchControl = function (req, res) {
fs.stat(filename, function (err, stat) {
var lastModified = stat.mtime.toUTCString();
if (lastModified === req.headers['if-modified-since']) {
res.writeHead(304, "Not Modified");
res.end();
} else {
fs.readFile(filename, function(err, file) {
var lastModified = stat.mtime.toUTCString();
res.setHeader("Last-Modified", lastModified);
res.writeHead(200, "Ok");
res.end(file);
});
}
});
}
这样的处理有些缺陷:只精确到秒级别,时间戳变内容不一定变。
ETag
这个是一个由服务器端生成的值,服务器可以决定它的生成规则,如果根据文件内容生成散列值那么就可以根据这个值精确的判定文件是否有改动。ETag的请求和响应是:If-None-Match/ETag。
var getHash = function (str) {
var shasum = crypto.createHash('sha1');
return shasum.update(str).digest('base64');
};
var ETag = function (req, res) {
fs.readFile(filename, function(err, file) {
var hash = getHash(file);
var noneMatch = req.headers['if-none-match'];
if (hash === noneMatch) {
res.writeHead(304, "Not Modified");
res.end();
} else {
res.setHeader("ETag", hash);
res.writeHead(200, "Ok");
res.end(file);
}
});
};
不过尽管这个方法很灵活,并且在文件没有改动的时候很节省带宽,但他依然会发起一个HTTP请求。
Expires、Cache-Control
这个方法直接告诉浏览器该资源会过期的时间。
数据上传
之前的内容都发生在头部,但是表单提交,文件提交等动作是不能放在头部的。
你可以通过头部中的字段值判断请求中是否包含内容,然后通过Buffer把它存起来:
var hasBody = function(req) {
return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
};
if (hasBody(req)) {
var buffers = [];
req.on('data', function (chunk) {
buffers.push(chunk);
});
req.on('end', function () {
req.rawBody = Buffer.concat(buffers).toString();
handle(req, res);
});
} else {
handle(req, res);
}
表单
对于表单数据,非常好解析,它的报文体内容和查询字符串相同:
var mime = function (req) {
var str = req.headers['content-type'] || '';
return str.split(';')[0];
};
if (mime(req) === 'application/x-www-form-urlencoded') {
req.body = querystring.parse(req.rawBody);
}
JSON文件
if (mime(req) === 'application/json') {
try {
req.body = JSON.parse(req.rawBody);
} catch (e) { // 异常内容响应Bad request
res.writeHead(400);