用Node.js 写web框架(五)

一周没更新啊...不过这周确实挺忙的(我才不说我偷懒来着呢)。 html

今天主要是完成POST方法提交multipart的支持(就是文件上传啦)。
首先贴一下改进过的参数包装函数: 数组

exports.wrap = function(req, callback) {
	if(req.method == POST_METHOD) {
		if(req.headers[TYPE] != POST_DATA) { // POST data
			var chunks = [];
			req.on(DATA_EVENT, function(chunk) {
				chunks.push(chunk);
			})
			req.on(END_EVENT, function() {
				var buffer = Buffer.concat(chunks);
				var param = QS.parse(buffer.toString());
				callback(param);
			})
		} else { // POST multipart form data
			parseMultipart(req, callback);
		}
	} else { // GET(and other kinds of methods) data
		var queryStr = URL.parse(req.url).query;
		if(!queryStr) {
			callback({});
		}
		callback(QS.parse(queryStr));
	}
}

上次的版本是使用Buffer.copy,后来查了一下API,发现有一个Buffer.concat是直接把数组中的全部Buffer链接起来,很不错。其余的跟上一个版本没有什么大变化,这里主要说的是parseMultipart函数。 浏览器

说这个函数之前,必须提一下HTTP协议这部分的细节了(协议都不知道,还解析啥)。这个属于HTTP协议比较复杂的一部分: 服务器

一、POST方法发送的Request中协议头部分的区别: app

正常状况下,POST方法发送的协议头中,content-type这个字段的值为:application/x-www-form-urlencoded。而当在form中添加了enctype="multipart/form-data"这个属性以后,content-type字段为:multipart/form-data; boundary=----WebKitFormBoundarycE8JeFAEuGHTJnRh。这里有两个部分,第一个部分标识这个POST请求的类别为:multipart/form-data,第二部分则标识在整个chunk中,boundary(也就是分隔符)是什么。这里这个boundary是浏览器生成的,不会与文件内容相同 函数

二、下面作一个完整的测试过来,来看看chunk里究竟是什么样的: post

首先我创建一个表单: 性能

<form action="/test/testUpload" method="post" enctype="multipart/form-data">
	filename : <input type="text" name="name"/>
	file : <input type="file" name="file"/>
    <input type="submit"/>
</form>

文件内容为: 测试

测试内容1
测试内容2
ABCDabcd1234


aaa

而后后台接受的chunks以下: url

------WebKitFormBoundarycE8JeFAEuGHTJnRh
Content-Disposition: form-data; name="name"

asdfasdf
------WebKitFormBoundarycE8JeFAEuGHTJnRh
Content-Disposition: form-data; name="file"; filename="a.txt"
Content-Type: text/plain

测试内容1
测试内容2
ABCDabcd1234


aaa
------WebKitFormBoundarycE8JeFAEuGHTJnRh--

首先是分隔符\r\n,而后是这部分的Disposition(我就翻译成配置了,没见到国内有什么好的翻译)。这里会标记出这部分的name是什么,若是这个表单域是文件,还会标识filename。
若是该表单域是文件,那么还会有一个Content-Type来标识文件类别是什么(就目前看解析意义不大,这个我最近会看看其余服务器是如何处理这部分的)。
而后\r\n\r\n,下面接着的就是表单内容(或者文件内容),并以\r\n结束。
以上内容循环,直到最后一个表单域结束,结尾以分隔符--\r\n结束。

更详细的内容见:Form-based File Upload in HTML

这样咱们的解析过程就是:
一、首先从请求头获取boundary
二、获取所有的chunk,并拼接到一个buffer(这个我之后会改为每次来一个chunk,就直接解析)
三、对这个buffer按着上面的过程进行解析

解析的所有代码我就不放上来了,有点长(100行+)。反正根据上面的分析过程你们都能写的出来。

下面的另外一个问题是如何包裹上传好的文件参数。这里我先说一下这部分的总体流程:
一、将解析后的chunk中的文件内容存入一个临时文件夹,并分配一个随机文件名
二、将文件原始文件名(能够从chunk中获得),临时文件名,其余参数,包裹为一个object,并传给views函数。

随机文件名我是使用了当前时间的16进制做为文件名的:

var name = new Date().getTime().toString(16);
而后的问题出在写文件上。完整看下来的人可能知道我在读取服务器配置的时候是使用了同步的API,这是由于我以为这部分只有启动的时候才会执行一次,使用同步不会出现性能问题。可是在服务器运行期间,我对本身的要求是禁止使用同步API的。这就致使可能文件还没写入完毕,views函数就已经开始执行了。

这里我使用了一个barrier的方案:

var barrier = [];
barrier.push(false);

req.on(END_EVENT, function(){
	//..解析

	if(checkBarrier()){
		barrier[0] = true;
		callback(param);
	}
})

function writeFile(filename, content){
	var fileIndex = barrier.length;
	var filepath = TMP_DIR + '/' + filename;

	barrier.push(false);
	FS.writeFile(filepath, content, function(err){
		if(err){
			throw err;
		}
		barrier[fileIndex] = true;
		if(checkBarrier()){
			callback(param);
		}
	})
}

function checkBarrier(){
	for(var i = 0; i < barrier.length; i++){
		if(!barrier[i]){
			return false;
		}
	}
	return true;
}
这个barrier里存放的是当前解析过程,全部文件写入过程是否结束。只有当所有都结束了的时候,才调用callback来执行下一部分的veiws方法。
相关文章
相关标签/搜索