其中請求報文中的開始行和首部行包含了常見的各類信息,好比http協議版本,方法(GET/POST),accept-language,cookie等等。 而’實體主體’通常在post中使用,好比咱們用表單上傳文件,文件數據就是在這個’實體主體’當中.php
整體上來講,對於post文件上傳這樣的過程,主要有如下幾個部分:java
獲取http請求報文腫的頭部信息,咱們能夠從中得到是否爲POST方法,實體主體的總大小,邊界字符串等,這些對於實體主體數據的解析都是很是重要的node
獲取POST數據(實體主體)服務器
對POST數據進行解析cookie
將數據寫入文件數據結構
獲取http請求報文頭部信息閉包
利用nodejs中的 http.ServerRequest中獲取1):app
request.method異步
用來標識請求類型函數
request.headers
其中咱們關心兩個字段:
content-type 包含了表單類型和邊界字符串(下面會介紹)信息。
content-length post數據的長度
get請求的headers中沒有content-type這個字段
post 的 content-type 有兩種
application/x-www-form-urlencoded
這種就是通常的文本表單用post傳地數據,只要將獲得的data用querystring解析下就能夠了
multipart/form-data
文件表單的傳輸,也是本文介紹的重點
前面已經說過,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數據以前,先介紹一下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(); });
file.js主要是封裝了文件的寫操做
模塊的主體部分
封裝了對於POST數據的分段讀取與解析的方法
封裝了對於GET數據的解析
與我上面提到的思路不同,node-formidable是邊接受數據邊進行解析。
上面那種方式是每一次有數據包到達後, 添加到buffer中,等全部數據都到齊後,再對數據進行解析.這種方式,在每次數據包到達的間隙是空閒的.
第二種方式使用邊接收邊解析的方式,對於大文件來講,能大大提高效率.
模塊的核心文件主要是 multipart_parser.js 和 incoming_from.js 兩個文件, 宏觀上, multipartParser 用於解析數據, 好比給定一個buffer, 將在解析的過程當中調用相應的回調函數.好比解析到字段數據的元信息(文件名,文件類型等), 將會使用 this.onHeaderField( buffer, start, end ) 這樣的形式來傳輸信息. 而這些方法的具體實現則是在 incoming_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數據格式中提到的 二級邊界字符串 類型的數據處理。我沒有深究,有興趣的能夠再研究下。