1. 程式人生 > >nodejs-http 對form表單上傳檔案資料的解析過程

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
      檔案表單的傳輸,也是本文介紹的重點

獲取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