nodejs-http 對form表單上傳檔案資料的解析過程
前幾天碰到了一個需求,允許接收前端使用者上傳的檔案。
當時為了解決問題索性就上github搜了下,找了一個基於nodejs的開發外掛。
後來功能實現後覺得意猶未盡,於是自己想試試去寫一個類似功能的外掛,方便以後拓展,然後就這麼開始了。
先來說說應用層的http,資料從前端是怎麼被它包裝然後傳到伺服器的。
我們可以在瀏覽器中檢視我們發一個請求的時候包什麼格式的,例如我們訪問百度時得到的請求包內容:
Remote Address:180.97.33.107:443
Request URL:https://www.baidu.com/
Request Method:GET
Status Code:200 OK
Request Headers
:host:www.baidu.com
:method:GET
:path:/
:scheme:https
:version:HTTP/1.1
accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
accept-encoding:gzip, deflate, sdch
accept-language:zh-CN,zh;q=0.8,en;q=0.6
cookie:BAIDUPSID=9193988659A757F51540F21C3A7DF43B; locale=zh;
referer:http://baidu.com/
user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36
Response Headers
bdpagetype:2
bdqid:0x9ca30ab3000c253d
bduserid:1618160418
cache-control:private
content-encoding:gzip
content-type:text/html
date:Sat, 04 Apr 2015 07:42:42 GMT
expires:Sat, 04 Apr 2015 07:42:42 GMT
server:bfe/1.0.8.1
set-cookie:BDSVRTM=124; path=/
set-cookie:BD_HOME=1; path=/
set-cookie:H_PS_PSSID=11193_1450_13074_10901_12867_13322_12691_13348_12723_12797_12737_13355_13324_13210_13162_13257_13031_8498; path=/; domain=.baidu.com
set-cookie:__bsi=18001627764121693250_31_38_R_N_124_0303_C02F_Y_I_I; expires=Sat, 04-Apr-15 07:42:47 GMT; domain=www.baidu.com; path=/
status:200 OK
version:HTTP/1.1
相同的,伺服器在接收到前端發來的請求時,經過一些邏輯處理,也會對前端傳送一個數據包,包頭格式如下:
bdpagetype:2
bdqid:0x9ca30ab3000c253d
bduserid:1618160418
cache-control:private
content-encoding:gzip
content-type:text/html
date:Sat, 04 Apr 2015 07:42:42 GMT
expires:Sat, 04 Apr 2015 07:42:42 GMT
server:bfe/1.0.8.1
set-cookie:BDSVRTM=124; path=/
set-cookie:BD_HOME=1; path=/
set-cookie:H_PS_PSSID=11193_1450_13074_10901_12867_13322_12691_13348_12723_12797_12737_13355_13324_13210_13162_13257_13031_8498; path=/; domain=.baidu.com
set-cookie:__bsi=18001627764121693250_31_38_R_N_124_0303_C02F_Y_I_I; expires=Sat, 04-Apr-15 07:42:47 GMT; domain=www.baidu.com; path=/
status:200 OK
version:HTTP/1.1
那麼資料包的核心就是伺服器返回給客戶端的內容,也就是包體,一般情況下,我們在寫程式碼與客戶端互動式不會去單獨處理包頭的內容(由http底層處理,淡然也可以人工干涉),我們只關心返回給前端什麼樣的資料也就是包體。
下面我們直接說說常用的post方法,如果你寫過一點PHP,那麼你肯定記得,在PHP裡面,進行檔案上傳的時候,我們可以直接使用全域性變數 $_FILE[‘name’ ]來獲取已經被臨時儲存的檔案資訊。
但是實際上,POST資料實體,會根據資料量的大小進行分包傳送,然後再從這些資料包裡面分析出哪些是檔案的元資料,那些是檔案本身的資料。
PHP是底層做了封裝,但是在nodejs裡面,這個看似常見的功能卻是需要自己來實現的。這篇文章主要就是介紹如何使用nodejs來解析post資料。
簡單的說就是我們通過nodejs的http模組得到的檔案上傳資料是一個半成品,我們需要對資料進行拆分,找到那些是包頭,哪些是包體。如果對接收到的資料直接進行拼接,那麼結果就變成了這樣子。
包頭,包體都在一起。
關於content-type
- get請求的headers中沒有content-type這個欄位post 的 content-type 有兩種
- application/x-www-form-urlencoded
這種就是一般的文字表單用post傳地資料,只要將得到的data用querystring解析下就可以了 - multipart/form-data
檔案表單的傳輸,也是本文介紹的重點
- application/x-www-form-urlencoded
獲取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
2.每一行資料用”CR LF”(\r\n)分隔
3.資料以 邊界分割符 後面加上 –結尾,如:
------WebKitFormBoundaryuP1WvwP2LyvHpNCi--\r\n
4.每個欄位資料的header資訊(content-disposition/content-type)和欄位資料以一個空行分隔:\r\n\r\n
關於form具體的內容可以看下W3C的文件:W3C
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 ) 便可以進行檔案寫入了.
其實github上有一個很不錯的開源外掛,有興趣可以去看一下:node-formidable