轉自:http://cnodejs.org/topic/4f16442ccae1f4aa270010ad
基礎知識
簡單介紹下,如下圖:

其中請求報文中的開始行和首部行包含了常見的各種信息,比如http協議版本,方法(GET/POST),accept-language,cookie等等。 而'實體主體'一般在post中使用,比如我們用表單上傳文件,文件數據就是在這個'實體主體'當中。
引子
寫這篇教程的起因是因為在學習nodejs的過程中,想要自己實現一些文件上傳的功能,於是不得不去研究POST。
如果你寫過一點PHP,那么你肯定記得,在PHP里面,進行文件上傳的時候,我們可以直接使用全局變量 $_FILE['name' ]來獲取已經被臨時存儲的文件信息。
但是實際上,POST數據實體,會根據數據量的大小進行分包傳送,然后再從這些數據包里面分析出哪些是文件的元數據,那些是文件本身的數據。
PHP是底層做了封裝,但是在nodejs里面,這個看似常見的功能卻是需要自己來實現的。這篇文章主要就是介紹如何使用nodejs來解析post數據。
正文
總體上來說,對於post文件上傳這樣的過程,主要有以下幾個部分:
獲取http請求報文腫的頭部信息,我們可以從中獲得是否為POST方法,實體主體的總大小,邊界字符串等,這些對於實體主體數據的解析都是非常重要的
獲取POST數據(實體主體)
對POST數據進行解析
將數據寫入文件
獲取http請求報文頭部信息
利用nodejs中的 http.ServerRequest中獲取 1):
request.method
用來標識請求類型
request.headers

其中我們關心兩個字段:
content-type
包含了表單類型和邊界字符串(下面會介紹)信息。
content-length
post數據的長度
關於content-type
get請求的headers中沒有content-type這個字段
post 的 content-type 有兩種
application/x-www-form-urlencoded
這種就是一般的文本表單用post傳地數據,只要將得到的data用querystring解析下就可以了
multipart/form-data
文件表單的傳輸,也是本文介紹的重點
獲取POST數據
前面已經說過,post數據的傳輸是可能分包的,因此必然是異步的。post數據的接受過程如下:
var postData = '';
request.addListener("data", function(postDataChunk) { // 有新的數據包到達就執行
postData += postDataChunk;
console.log("Received POST data chunk '"+
postDataChunk + "'.");
});
request.addListener("end", function() { // 數據傳輸完畢
console.log('post data finish receiving: ' + postData );
});
注意,對於非文件post數據,上面以字符串接收是沒問題的,但其實 postDataChunk 是一個 buffer 類型數據,在遇到二進制時,這樣的接受方式存在問題。
POST數據的解析(multipart/form-data)
在解析POST數據之前,先介紹一下post數據的格式:
multipart/form-data類型的post數據
例如我們有表單如下
<FORM action="http://server.com/cgi/handle"
enctype="multipart/form-data"
method="post">
<P>
What is your name? <INPUT type="text" name="submit-name"><BR>
What files are you sending? <INPUT type="file" name="files"><BR>
<INPUT type="submit" value="Send"> <INPUT type="reset">
FORM>
若用戶在text字段中輸入‘Neekey',並且在file字段中選擇文件‘text.txt',那么服務器端收到的post數據如下:
--AaB03x
Content-Disposition: form-data; name="submit-name"
Neekey
--AaB03x
Content-Disposition: form-data; name="files"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
若file字段為空:
--AaB03x
Content-Disposition: form-data; name="submit-name"
Neekey
--AaB03x
Content-Disposition: form-data; name="files"; filename=""
Content-Type: text/plain
--AaB03x--
若將file 的 input修改為可以多個文件一起上傳:
<FORM action="http://server.com/cgi/handle"
enctype="multipart/form-data"
method="post">
<P>
What is your name? <INPUT type="text" name="submit-name"><BR>
What files are you sending? <INPUT type="file" name="files" multiple="multiple"><BR>
<INPUT type="submit" value="Send"> <INPUT type="reset">
FORM>
那么在text中輸入‘Neekey',並在file字段中選中兩個文件’a.jpg'和'b.jpg'后:
--AaB03x
Content-Disposition: form-data; name="submit-name"
Neekey
--AaB03x
Content-Disposition: form-data; name="files"; filename="a.jpg"
Content-Type: image/jpeg
/* data of a.jpg */
--AaB03x
Content-Disposition: form-data; name="files"; filename="b.jpg"
Content-Type: image/jpeg
/* data of b.jpg */
--AaB03x--
// 可以發現 兩個文件數據部分,他們的name值是一樣的
數據規則
簡單總結下post數據的規則
不同字段數據之間以邊界字符串分隔:
--boundary\r\n // 注意,如上面的headers的例子,分割字符串應該是 ------WebKitFormBoundaryuP1WvwP2LyvHpNCi\r\n
每一行數據用”CR LF”(\r\n)分隔
數據以 邊界分割符 后面加上 –結尾,如:
------WebKitFormBoundaryuP1WvwP2LyvHpNCi--\r\n
每個字段數據的header信息(content-disposition/content-type)和字段數據以一個空行分隔:
\r\n\r\n
更加詳細的信息可以參考W3C的文檔 Forms,不過文檔中對於 multiple=“multiple” 的文件表單的post數據格式使用了二級邊界字符串,但是在實際測試中,multiple類型的表單和多個單文件表單上傳數據的格式一致,有更加清楚的可以交流下:
If the user selected a second (image) file "file2.gif", the user agent might construct the parts as follows:
Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
Content-Disposition: form-data; name="submit-name"
Larry
--AaB03x
Content-Disposition: form-data; name="files"
Content-Type: multipart/mixed; boundary=BbC04y
--BbC04y
Content-Disposition: file; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--BbC04y
Content-Disposition: file; filename="file2.gif"
Content-Type: image/gif
Content-Transfer-Encoding: binary
...contents of file2.gif...
--BbC04y--
--AaB03x--
數據解析基本思路
必須使用buffer來進行post數據的解析
利用文章一開始的方法(data += chunk, data為字符串 ),可以利用字符串的操作,輕易地解析出各自端的信息,但是這樣有兩個問題:
文件的寫入需要buffer類型的數據
二進制buffer轉化為string,並做字符串操作后,起索引和字符串是不一致的(若原始數據就是字符串,一致),因此是先將不總的buffer數據的toString()復制給一個字符串,再利用字符串解析出個數據的start,end位置這樣的方案也是不可取的。
利用邊界字符串來分割各字段數據
每個字段數據中,使用空行(\r\n\r\n)來分割字段信息和字段數據
所有的數據都是以\r\n分割
利用上面的方法,我們以某種方式確定了數據在buffer中的start和end,利用buffer.splice( start, end ) 便可以進行文件寫入了
文件寫入
比較簡單,使用 File System 模塊(nodejs的文件處理,我很弱很弱….)
var fs = new require( 'fs' ).writeStream,
file = new fs( filename );
fs.write( buffer, function(){
fs.end();
});
node-formidable模塊源碼分析
node-formidable是比較流行的處理表單的nodejs模塊。 github主頁
項目中的lib目錄
lib
|-file.js
|-incoming_form.js
|-index.js
|-multipart_parser.js
|-querystring_parser.js
|-util.js
各文件說明
file.js
file.js主要是封裝了文件的寫操作
incoming_from.js
模塊的主體部分
multipart_parser.js
封裝了對於POST數據的分段讀取與解析的方法
querystring_parser.js
封裝了對於GET數據的解析
總體思路
與我上面提到的思路不一樣,node-formidable是邊接受數據邊進行解析。

上面那種方式是每一次有數據包到達后, 添加到buffer中,等所有數據都到齊后,再對數據進行解析.這種方式,在每次數據包到達的間隙是空閑的.
第二種方式使用邊接收邊解析的方式,對於大文件來說,能大大提升效率.
模塊的核心文件主要是 multipart_parser.js 和 incoming_from.js 兩個文件, 宏觀上, multipartParser 用於解析數據, 比如給定一個buffer, 將在解析的過程中調用相應的回調函數.比如解析到字段數據的元信息(文件名,文件類型等), 將會使用 this.onHeaderField( buffer, start, end ) 這樣的形式來傳輸信息. 而這些方法的具體實現則是在 incoming_form.js 文件中實現的. 下面着重對這兩個文件的源碼進行分析

multipart_form.js
這個模塊是POST數據接受的核心。起核心思想是對每個接受到的partData進行解析,並觸發相應時間,由於每次write方法的調用都將產生內部私有方法,所以partData將會被傳送到各個觸發事件當中,而觸發事件(即對於partData的具體處理)的具體實現則是在incoming_form中實現,從這一點來說,兩個模塊是高度耦合的。
multipart_form 的源碼讀起來會比較吃力。必須在對post數據結構比較清楚的情況下,在看源碼。
源碼主要是四個部分:
全局變量(閉包內)
構造函數
初始化函數(initWithBoundary)
解析函數(write)
其中全局變量,構造函數比較簡單。
初始化函數用 用傳進的 邊界字符串 構造boundary的buffer,主要用於在解析函數中做比較。 下面主要介紹下解析函數
幾個容易迷惑的私有方法
make( name )
將當前索引(對buffer的遍歷)復制給 this[ name ]. 這個方法就是做標記,用於記錄一個數據段在buffer中的開始位置
callback( name , buffer, start, end )
調用this的onName方法,並傳入buffer和start以及end三個參數。 比如當文件post數據中的文件部分的數據解析完畢,則通過callback( 'partData', buffer, start, end ) 將該數據段的首尾位置和buffer傳遞給 this.onPartData 方法,做進一步處理。
dataCallback( name, clear )
前面的callback,如果不看后面的三個參數,其本質不過是一個調用某個方法的橋接函數。而dataCallback則是對callback的一個封裝,他將start和end傳遞給callback。
從源碼中可以看到,start是通過mark(name)的返回值獲得,而end則可能是當前遍歷到的索引或者是buffer的末尾。
因此dataCallback被調用有二種情況:
在解析的數據部分的末尾在當前buffer的內部,這個時候mark記錄的開始點和當前遍歷到的i這個區段就是需要的數據,因此start = mark(name), end = i, 並且由於解析結束,需要將mark清除掉。
在當前buffer內,解析的數據部分尚未解析完畢(剩下的內容在下一個buffer里),因此start = mark(name), end = buffer.length
解析的主要部分
解析的主要部分是對buffer進行遍歷,然后對於每一個字符,都根據當前的狀態進行switch的各個case進行操作。
switch的每一個case都是一個解析狀態。
具體看源碼和注釋,然后對照post的數據結構就會比較清楚。
其中 在狀態:S.PART_DATA 這邊,node-formidable做了一些處理,應該是針對 文章一開始介紹post數據格式中提到的 二級邊界字符串 類型的數據處理。我沒有深究,有興趣的可以再研究下。
var Buffer = require('buffer').Buffer,
s = 0,
S =
{ PARSER_UNINITIALIZED: s++, // 解析尚未初始化
START: s++, // 開始解析
START_BOUNDARY: s++, // 開始找到邊界字符串
HEADER_FIELD_START: s++, // 開始解析到header field
HEADER_FIELD: s++,
HEADER_VALUE_START: s++, // 開始解析到header value
HEADER_VALUE: s++,
HEADER_VALUE_ALMOST_DONE: s++, // header value 解析完畢
HEADERS_ALMOST_DONE: s++, // header 部分 解析完畢
PART_DATA_START: s++, // 開始解析 數據段
PART_DATA: s++,
PART_END: s++,
END: s++,
},
f = 1,
F =
{ PART_BOUNDARY: f,
LAST_BOUNDARY: f *= 2,
},
/* 一些字符的ASCII值 */
LF = 10,
CR = 13,
SPACE = 32,
HYPHEN = 45,
COLON = 58,
A = 97,
Z = 122,
/* 將所有大寫小寫字母的ascii一律轉化為小寫的ascii值 */
lower = function(c) {
return c | 0x20;
};
for (var s in S) {
exports[s] = S[s];
}
/* 構造函數 */
function MultipartParser() {
this.boundary = null;
this.boundaryChars = null;
this.lookbehind = null;
this.state = S.PARSER_UNINITIALIZED;
this.index = null;
this.flags = 0;
};
exports.MultipartParser = MultipartParser;
/* 給定邊界字符串以初始化 */
MultipartParser.prototype.initWithBoundary = function(str) {
this.boundary = new Buffer(str.length+4);
this.boundary.write('\r\n--', 'ascii', 0);
this.boundary.write(str, 'ascii', 4);
this.lookbehind = new Buffer(this.boundary.length+8);
this.state = S.START;
this.boundaryChars = {};
for (var i = 0; i < this.boundary.length; i++) {
this.boundaryChars[this.boundary[i]] = true;
}
};
/* 每個數據段到達時的處理函數 */
MultipartParser.prototype.write = function(buffer) {
var self = this,
i = 0,
len = buffer.length,
prevIndex = this.index,
index = this.index,
state = this.state,
flags = this.flags,
lookbehind = this.lookbehind,
boundary = this.boundary,
boundaryChars = this.boundaryChars,
boundaryLength = this.boundary.length,
boundaryEnd = boundaryLength - 1,
bufferLength = buffer.length,
c,
cl,
/* 標記了name這個標記點的buffer偏移 */
mark = function(name) {
self[name+'Mark'] = i;
},
/* 清除標記 */
clear = function(name) {
delete self[name+'Mark'];
},
/* 回調函數,將調用onName,並傳入對應的buffer和對應的offset區間,可知這些回調都在 incoming_form 模塊中被具體實現 */
callback = function(name, buffer, start, end) {
if (start !== undefined && start === end) {
return;
}
var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1);
if (callbackSymbol in self) {
self[callbackSymbol](buffer, start, end);
}
},
/* 數據回調 */
dataCallback = function(name, clear) {
var markSymbol = name+'Mark';
if (!(markSymbol in self)) {
return;
}
if (!clear) {
/* 傳入回調的名稱,buffer,buffer的開始位置(可見mark方法就是用來存儲offset的),end為數據的重點 */
callback(name, buffer, self[markSymbol], buffer.length);
self[markSymbol] = 0;
} else {
/* 區別是 end 的值為i,在一個數據已經判斷到達其結束位置時,就刪除這個mark點,因為這個時候已經知道了這個數據的起始位置 */
callback(name, buffer, self[markSymbol], i);
delete self[markSymbol];
}
};
/* 對buffer逐個字節遍歷,進行解析判斷,並觸發相應事件 */
for (i = 0; i < len; i++) {
c = buffer[i];
switch (state) {
case S.PARSER_UNINITIALIZED:
return i;
case S.START:
index = 0;
state = S.START_BOUNDARY;
case S.START_BOUNDARY:
/**
* 對於邊界的判斷 ====
* 這里看了很多次,一直很迷惑
* 因為首先:除了最后一個邊界字符串,其他(包括第一個邊界字符串)都是這個樣子:
* --boundary\r\t
* 但是在初始化函數中,對於this.boundary的賦值是這樣的:
* \r\n--boundary
*
* 但是仔細看下面部分的代碼,作者只是為了初始化方便,或者出於其他的考慮
*/
/**
* 判斷是否到了邊界字符串的結尾\r處
* 如果當前字符與 \r不匹配,則return ,算是解析出錯了
* 注意,雖然this.boundary != --boundary\r\n 但是長度是一致的,因此這里的判斷是沒有問題的
*/
if (index == boundary.length - 2) {
if (c != CR) {
return i;
}
index++;
break;
}
/**
* 判斷是否到了邊界字符串的結尾\n處
* 如果是,則置index = 0
* 回調 partBegin,並將狀態設置為還是header 的 field 信息的讀取狀態
*/
else if (index - 1 == boundary.length - 2) {
if (c != LF) {
return i;
}
index = 0;
callback('partBegin');
state = S.HEADER_FIELD_START;
break;
}
/**
* 除了boundary的最后的\r\n外,其他字符都要進行檢查
* 注意這里用的是 index+2 進行匹配,證實了
* 作者是因為某種意圖將 boundary設置成\r\n--boundary的形式
* (其實這種形式也沒有錯,對處第一個邊界字符串外的其他邊界字符串,這個形式都是適用的)
*/
if (c != boundary[index+2]) {
return i;
}
index++;
break;
/* 對於header field的掃描開始,這里記錄了標記了開始點在buffer中的位置 */
case S.HEADER_FIELD_START:
state = S.HEADER_FIELD;
mark('headerField');
index = 0;
/* header field的掃描過程 */
case S.HEADER_FIELD:
/* 這里是header和data中間的那個空行,所以第一個字符就是\r */
if (c == CR) {
clear('headerField');
state = S.HEADERS_ALMOST_DONE;
break;
}
index++;
/* 如果是小橫線 '-' 比如在 Content-Disposition */
if (c == HYPHEN) {
break;
}
/**
* 如果是冒號,那么說明 field結束了
* dataCallback,注意第二個參數為true,則它將調用this.onHeaderField,並且將buffer,start(之前mark(headerField)記錄的位置),end(當前的i)傳遞過去,最后將這個mark清理掉
* 之后進入 header value 的開始階段
*/
if (c == COLON) {
if (index == 1) {
// empty header field
return i;
}
dataCallback('headerField', true);
state = S.HEADER_VALUE_START;
break;
}
/**
* 對於所有其他不是冒號和小橫線的字符,必須為字母,否則解析結束
*/
cl = lower(c);
if (cl < A || cl > Z) {
return i;
}
break;
/**
* value 的讀取開始
* 做記號,設置state
*/
case S.HEADER_VALUE_START:
if (c == SPACE) {
break;
}
mark('headerValue');
state = S.HEADER_VALUE;
/**
* value 的分析階段
*/
case S.HEADER_VALUE:
/**
* 如果是 \r,則value結束
* 同樣是調用 dataCallback,參數為true
* 注意這里還 callback了 headerEnd,沒有給定任何參數,這里是作為一個trigger拋出一行header結束的事件
*/
if (c == CR) {
dataCallback('headerValue', true);
callback('headerEnd');
state = S.HEADER_VALUE_ALMOST_DONE;
}
break;
/**
* value 結束的檢查,之前是檢查到了 \r ,如果下一個字符不是 \n 肯定問題
* 一個value結束,可能下面還是一行header,所以不會直接 header done,而是重新進入掃描 fields的階段
*/
case S.HEADER_VALUE_ALMOST_DONE:
if (c != LF) {
return i;
}
state = S.HEADER_FIELD_START;
break;
/**
* 同樣是先檢查一下字符是否有誤(看一下 case S.HEADER_FIELD 的第一個if )
* 拋出headers解析完畢的事件
*/
case S.HEADERS_ALMOST_DONE:
if (c != LF) {
return i;
}
callback('headersEnd');
state = S.PART_DATA_START;
break;
/**
* 開始解析post數據
* 設置狀態,做記號
*/
case S.PART_DATA_START:
state = S.PART_DATA
mark('partData');
/**
* 進入post數據的解析狀態
* 上一次設置index是在 HEADER_FIELD_START中設置為0
*/
case S.PART_DATA:
prevIndex = index;
if (index == 0) {
// boyer-moore derrived algorithm to safely skip non-boundary data
i += boundaryEnd;
while (i < bufferLength && !(buffer[i] in boundaryChars)) {
i += boundaryLength;
}
i -= boundaryEnd;
c = buffer[i];
}
if (index < boundary.length) {
if (boundary[index] == c) {
if (index == 0) {
dataCallback('partData', true);
}
index++;
} else {
index = 0;
}
} else if (index == boundary.length) {
index++;
if (c == CR) {
// CR = part boundary
flags |= F.PART_BOUNDARY;
} else if (c == HYPHEN) {
// HYPHEN = end boundary
flags |= F.LAST_BOUNDARY;
} else {
index = 0;
}
} else if (index - 1 == boundary.length) {
if (flags & F.PART_BOUNDARY) {
index = 0;
if (c == LF) {
// unset the PART_BOUNDARY flag
flags &= ~F.PART_BOUNDARY;
callback('partEnd');
callback('partBegin');
state = S.HEADER_FIELD_START;
break;
}
} else if (flags & F.LAST_BOUNDARY) {
if (c == HYPHEN) {
callback('partEnd');
callback('end');
state = S.END;
} else {
index = 0;
}
} else {
index = 0;
}
}
if (index > 0) {
// when matching a possible boundary, keep a lookbehind reference
// in case it turns out to be a false lead
lookbehind[index-1] = c;
} else if (prevIndex > 0) {
// if our boundary turned out to be rubbish, the captured lookbehind
// belongs to partData
callback('partData', lookbehind, 0, prevIndex);
prevIndex = 0;
mark('partData');
// reconsider the current character even so it interrupted the sequence
// it could be the beginning of a new sequence
i--;
}
break;
case S.END:
break;
default:
return i;
}
}
/**
* 下面這三個是在當前數據段解析完全后調用的。
* 如果一個數據部分(比如如field信息)已經在上面的解析過程中解析完畢,那么自然已經調用過clear方法,那下面的dataCallback將什么也不做
* 否則,下面的調用將會把這次數據段中的數據部分傳遞到回調函數中
*/
dataCallback('headerField');
dataCallback('headerValue');
dataCallback('partData');
this.index = index;
this.state = state;
this.flags = flags;
return len;
};
MultipartParser.prototype.end = function() {
if (this.state != S.END) {
return new Error('MultipartParser.end(): stream ended unexpectedly');
}
};
incoming_form.js
parse
根據傳入的requeset對象開始啟動整個解析的過程
writeHeaders
從request對象中獲取post數據長度,解析出邊界字符串,用來初始化multipartParser
為request對象添加監聽事件
write
request對象的 'data'時間到達會調用該方法,而write方法實質上是調用multipartParser.write
_initMultipart
利用邊界字符串初始化multipartParser,並實現在multipart_form.js中write解析方法中會觸發的事件回調函數
具體細節看源碼會比較清楚。
if (global.GENTLY) require = GENTLY.hijack(require);
var util = require('./util'),
path = require('path'),
File = require('./file'),
MultipartParser = require('./multipart_parser').MultipartParser,
QuerystringParser = require('./querystring_parser').QuerystringParser,
StringDecoder = require('string_decoder').StringDecoder,
EventEmitter = require('events').EventEmitter;
function IncomingForm() {
if (!(this instanceof IncomingForm)) return new IncomingForm;
EventEmitter.call(this);
this.error = null;
this.ended = false;
this.maxFieldsSize = 2 * 1024 * 1024; // 設置最大文件限制
this.keepExtensions = false;
this.uploadDir = '/tmp'; // 設置文件存放目錄
this.encoding = 'utf-8';
this.headers = null; // post請求的headers信息
// 收到的post數據類型(一般的字符串數據,還是文件)
this.type = null;
this.bytesReceived = null; // 已經接受的字節
this.bytesExpected = null; // 預期接受的字節
this._parser = null;
this._flushing = 0;
this._fieldsSize = 0;
};
util.inherits(IncomingForm, EventEmitter);
exports.IncomingForm = IncomingForm;
IncomingForm.prototype.parse = function(req, cb) {
// 每次調用都重新建立方法,用於對req和cb的閉包使用
this.pause = function() {
try {
req.pause();
} catch (err) {
// the stream was destroyed
if (!this.ended) {
// before it was completed, crash & burn
this._error(err);
}
return false;
}
return true;
};
this.resume = function() {
try {
req.resume();
} catch (err) {
// the stream was destroyed
if (!this.ended) {
// before it was completed, crash & burn
this._error(err);
}
return false;
}
return true;
};
// 記錄下headers信息
this.writeHeaders(req.headers);
var self = this;
req
.on('error', function(err) {
self._error(err);
})
.on('aborted', function() {
self.emit('aborted');
})
// 接受數據
.on('data', function(buffer) {
self.write(buffer);
})
// 數據傳送結束
.on('end', function() {
if (self.error) {
return;
}
var err = self._parser.end();
if (err) {
self._error(err);
}
});
// 若回調函數存在
if (cb) {
var fields = {}, files = {};
this
// 一個字段解析完畢,觸發事件
.on('field', function(name, value) {
fields[name] = value;
})
// 一個文件解析完畢,處罰事件
.on('file', function(name, file) {
files[name] = file;
})
.on('error', function(err) {
cb(err, fields, files);
})
// 所有數據接收完畢,執行回調函數
.on('end', function() {
cb(null, fields, files);
});
}
return this;
};
// 保存header信息
IncomingForm.prototype.writeHeaders = function(headers) {
this.headers = headers;
// 從頭部中解析數據的長度和form類型
this._parseContentLength();
this._parseContentType();
};
IncomingForm.prototype.write = function(buffer) {
if (!this._parser) {
this._error(new Error('unintialized parser'));
return;
}
/* 累加接收到的信息 */
this.bytesReceived += buffer.length;
this.emit('progress', this.bytesReceived, this.bytesExpected);
// 解析數據
var bytesParsed = this._parser.write(buffer);
if (bytesParsed !== buffer.length) {
this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed'));
}
return bytesParsed;
};
IncomingForm.prototype.pause = function() {
// this does nothing, unless overwritten in IncomingForm.parse
return false;
};
IncomingForm.prototype.resume = function() {
// this does nothing, unless overwritten in IncomingForm.parse
return false;
};
/**
* 開始接受數據(這個函數在headers被分析完成后調用,這個時候剩下的data還沒有解析過來
*/
IncomingForm.prototype.onPart = function(part) {
// this method can be overwritten by the user
this.handlePart(part);
};
IncomingForm.prototype.handlePart = function(part) {
var self = this;
/* post數據不是文件的情況 */
if (!part.filename) {
var value = ''
, decoder = new StringDecoder(this.encoding);
/* 有數據過來時 */
part.on('data', function(buffer) {
self._fieldsSize += buffer.length;
if (self._fieldsSize > self.maxFieldsSize) {
self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data'));
return;
}
value += decoder.write(buffer);
});
part.on('end', function() {
self.emit('field', part.name, value);
});
return;
}
this._flushing++;
// 創建新的file實例
var file = new File({
path: this._uploadPath(part.filename),
name: part.filename,
type: part.mime,
});
this.emit('fileBegin', part.name, file);
file.open();
/* 當文件數據達到,一點一點寫入文件 */
part.on('data', function(buffer) {
self.pause();
file.write(buffer, function() {
self.resume();
});
});
// 一個文件的數據解析完畢,出發事件
part.on('end', function() {
file.end(function() {
self._flushing--;
self.emit('file', part.name, file);
self._maybeEnd();
});
});
};
/**
* 解析表單類型
* 如果為文件表單,則解析出邊界字串,初始化multipartParser
*/
IncomingForm.prototype._parseContentType = function() {
if (!this.headers['content-type']) {
this._error(new Error('bad content-type header, no content-type'));
return;
}
// 如果是一般的post數據
if (this.headers['content-type'].match(/urlencoded/i)) {
this._initUrlencoded();
return;
}
// 如果為文件類型
if (this.headers['content-type'].match(/multipart/i)) {
var m;
if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) {
// 解析出邊界字符串,並利用邊界字符串初始化multipart組件
this._initMultipart(m[1] || m[2]);
} else {
this._error(new Error('bad content-type header, no multipart boundary'));
}
return;
}
this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type']));
};
IncomingForm.prototype._error = function(err) {
if (this.error) {
return;
}
this.error = err;
this.pause();
this.emit('error', err);
};
// 從 this.headers 中獲取數據總長度
IncomingForm.prototype._parseContentLength = function() {
if (this.headers['content-length']) {
this.bytesReceived = 0;
this.bytesExpected = parseInt(this.headers['content-length'], 10);
}
};
IncomingForm.prototype._newParser = function() {
return new MultipartParser();
};
// 初始化multipartParset 組件
IncomingForm.prototype._initMultipart = function(boundary) {
this.type = 'multipart';
// 實例化組件
var parser = new MultipartParser(),
self = this,
headerField,
headerValue,
part;
parser.initWithBoundary(boundary);
/**
* 下面這些方法便是multipartParser中的callback以及dataCallback調用的函書
* 當開始解析一個數據段(比如一個文件..)
* 並重置相關信息
*/
parser.onPartBegin = function() {
part = new EventEmitter();
part.headers = {};
part.name = null;
part.filename = null;
part.mime = null;
headerField = '';
headerValue = '';
};
/**
* 數據段的頭部信息解析完畢(或者數據段的頭部信息在當前接受到的數據段的尾部,並且尚未結束)
* 下面的onHeaderValue和onPartData也是一樣的道理
*/
parser.onHeaderField = function(b, start, end) {
headerField += b.toString(self.encoding, start, end);
};
/* 數據段的頭部信息value的解析過程 */
parser.onHeaderValue = function(b, start, end) {
headerValue += b.toString(self.encoding, start, end);
};
/* header信息(一行)解析完畢,並儲存起來 */
parser.onHeaderEnd = function() {
headerField = headerField.toLowerCase();
part.headers[headerField] = headerValue;
var m;
if (headerField == 'content-disposition') {
if (m = headerValue.match(/name="([^"]+)"/i)) {
part.name = m[1];
}
if (m = headerValue.match(/filename="([^;]+)"/i)) {
part.filename = m[1].substr(m[1].lastIndexOf('\\') + 1);
}
} else if (headerField == 'content-type') {
part.mime = headerValue;
}
/* 重置,准備解析下一個header信息 */
headerField = '';
headerValue = '';
};
/* 整個headers信息解析完畢 */
parser.onHeadersEnd = function() {
self.onPart(part);
};
/* 數據部分的解析 */
parser.onPartData = function(b, start, end) {
part.emit('data', b.slice(start, end));
};
/* 數據段解析完畢 */
parser.onPartEnd = function() {
part.emit('end');
};
parser.onEnd = function() {
self.ended = true;
self._maybeEnd();
};
this._parser = parser;
};
/* 初始化,處理application/x-www-form-urlencoded類型的表單 */
IncomingForm.prototype._initUrlencoded = function() {
this.type = 'urlencoded';
var parser = new QuerystringParser()
, self = this;
parser.onField = function(key, val) {
self.emit('field', key, val);
};
parser.onEnd = function() {
self.ended = true;
self._maybeEnd();
};
this._parser = parser;
};
/**
* 根據給定的文件名,構造出path
*/
IncomingForm.prototype._uploadPath = function(filename) {
var name = '';
for (var i = 0; i < 32; i++) {
name += Math.floor(Math.random() * 16).toString(16);
}
if (this.keepExtensions) {
name += path.extname(filename);
}
return path.join(this.uploadDir, name);
};
IncomingForm.prototype._maybeEnd = function() {
if (!this.ended || this._flushing) {
return;
}
this.emit('end');
};
進一步理解請參考:http://cnodejs.org/topic/4f16442ccae1f4aa2700104d