nodejs-post文件上傳原理詳解


轉自: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 有兩種



        1. application/x-www-form-urlencoded 
          這種就是一般的文本表單用post傳地數據,只要將得到的data用querystring解析下就可以了


        1. 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數據的規則 


    1. 不同字段數據之間以邊界字符串分隔: 
       --boundary\r\n // 注意,如上面的headers的例子,分割字符串應該是 ------WebKitFormBoundaryuP1WvwP2LyvHpNCi\r\n


    1. 每一行數據用”CR LF”(\r\n)分隔


    1. 數據以 邊界分割符 后面加上 –結尾,如: 
       ------WebKitFormBoundaryuP1WvwP2LyvHpNCi--\r\n


    1. 每個字段數據的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被調用有二種情況: 


    1. 在解析的數據部分的末尾在當前buffer的內部,這個時候mark記錄的開始點和當前遍歷到的i這個區段就是需要的數據,因此start = mark(name), end = i, 並且由於解析結束,需要將mark清除掉。


    1. 在當前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

 





上圖是incoming_form解析的主要過程(文件類型),其中 

 

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



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM