nodejs-post文件上傳原理詳解
其中請求報文中的開始行和首部行包含了常見的各種信息,比如http協議版本,方法(GET/POST),accept-language,cookie等等。 而’實體主體’一般在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數據的規則
-
不同字段數據之間以邊界字符串分隔;
-
每一行數據用”CR LF”(\r\n)分隔;
-
數據以 邊界分割符 后面加上 –結尾,如:
-
每個字段數據的header信息(content-disposition/content-type)和字段數據以一個空行分隔:
--boundary\r\n // 注意,如上面的headers的例子,分割字符串應該是 ------WebKitFormBoundaryuP1WvwP2LyvHpNCi\r\n
數據解析基本思路
-
必須使用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 模塊.
var fs = new require( 'fs' ).writeStream, file = new fs( filename ); fs.write( buffer, function(){ fs.end(); });
node-formidable模塊源碼分析
各文件說明
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數據格式中提到的 二級邊界字符串 類型的數據處理。我沒有深究,有興趣的可以再研究下。