今天主要是完成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协议比较复杂的一部分:
1、POST方法发送的Request中协议头部分的区别:
正常情况下,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是浏览器生成的,不会与文件内容相同
2、下面做一个完整的测试过来,来看看chunk里到底是什么样的:
首先我建立一个表单:
<form action="/test/testUpload" method="post" enctype="multipart/form-data">
filename : <input type="text"/>
file : <input type="file"/>
<input type="submit"/>
</form>
文件内容为:
测试内容1
测试内容2
ABCDabcd1234
aaa
然后后台接受的chunks如下:
------WebKitFormBoundarycE8JeFAEuGHTJnRh
Content-Disposition: form-data;
asdfasdf
------WebKitFormBoundarycE8JeFAEuGHTJnRh
Content-Disposition: form-data;; 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结束。
这样我们的解析过程就是:
1、首先从请求头获取boundary
2、获取全部的chunk,并拼接到一个buffer(这个我以后会改成每次来一个chunk,就直接解析)
3、对这个buffer按着上面的过程进行解析
解析的全部代码我就不放上来了,有点长(100行+)。反正根据上面的分析过程大家都能写的出来。
下面的另一个问题是如何包裹上传好的文件参数。这里我先说一下这部分的整体流程:
1、将解析后的chunk中的文件内容存入一个临时文件夹,并分配一个随机文件名
2、将文件原始文件名(可以从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方法。