模擬HTML表單上傳文件(RFC 1867)

模擬HTML表單上傳文件(RFC 1867)

2011-03-27 18:59 by 老趙, 10845 visits

現在使用HTTP協議定製API已是十分常見的事情,在普通的GET和POST請求中傳遞些參數估計人人都會,可是若是咱們須要上傳文件呢?若是隻是傳遞單個文件,那麼將數據流POST給服務器端便可。但若是須要上傳多個文件,或是在文件以外須要附帶一些信息,那麼又該怎麼作呢?以前我遇到過一些朋友是這麼打算的,他們說,不如就把文件流轉化爲文本,而後把它看成一個普通的字段傳遞。這麼作天然能夠「實現功能」,但缺點也不少。首先,將二進制流轉化爲文本會增大致積(例如最多見的BASE64編碼會增大1/3的數據量);其次,既然互聯網上存在相關的協議,又爲什麼要自定義一套規則呢?其實這即是《RFC 1867 - Form-based File Upload in HTML》,它是咱們用HTML表單上傳文件時使用的傳輸協議,雖然十分經常使用,但彷佛瞭解它的人並很少。 html

普通POST操做

提及HTML表單,你們絕對不會陌生。例以下面這樣的HTML表單: 編程

<form action="http://www.baidu.com/" method="post"> <input type="text" name="myText1" /><br /> <input type="text" name="myText2" /><br /> <input type="submit" /> </form> 

提交時會向服務器端發出這樣的數據(已經去除部分不相關的頭信息): 瀏覽器

POST http://www.baidu.com/ HTTP/1.1
Host: www.baidu.com
Content-Length: 74
Content-Type: application/x-www-form-urlencoded

myText1=hello+world&myText2=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C

對於普通的HTML POST表單,它會在頭信息裏使用Content-Length註明內容長度。頭信息每行一條,空行以後即是Body,即「內容」。此外,咱們能夠發現它的Content-Type是application/x-www-form-urlencoded,這意味着消息內容會通過URL編碼,就像在GET請求時URL裏的Query String那樣。在上面的例子中,myText1裏的空格被編碼爲加號,而myText2,您看得出這是「你好世界」這四個漢字嗎? 安全

使用POST上傳文件

不過以前的HTML表單是沒法上傳文件的,所以RFC 1867應運而生,它的目的即是讓HTML表單能夠提交文件。它對HTML表單的擴展主要是: 服務器

  • 爲input標記的type屬性增長一個file選項。
  • 在POST狀況下,爲form標記的enctype屬性定義默認值爲application/x-www-form-urlencoded。
  • 爲form標記的enctype屬性增長multipart/form-data選項。

因而,若是咱們要使用HTML表單提交文件,則可使用以下定義: 網絡

<form action="http://www.baidu.com/" method="post" enctype="multipart/form-data"> <input type="text" name="myText" /><br /> <input type="file" name="upload1" /><br /> <input type="file" name="upload2" /><br /> <input type="submit" /> </form> 

爲了實驗所需,咱們建立兩個文件file1.txt和file2.txt,內容分別爲「This is file1.」及「This is file2, it's bigger.」。在文本框裏寫上「hello world」,並選擇這兩個文件,提交,則會看到瀏覽器傳遞了以下數據: app

POST http://www.baidu.com/ HTTP/1.1
Host: www.baidu.com
Content-Length: 495
Content-Type: multipart/form-data; boundary=---------------------------7db2d1bcc50e6e

-----------------------------7db2d1bcc50e6e
Content-Disposition: form-data; name="myText"

hello world
-----------------------------7db2d1bcc50e6e
Content-Disposition: form-data; name="upload1"; filename="C:\file1.txt"
Content-Type: text/plain

This is file1.
-----------------------------7db2d1bcc50e6e
Content-Disposition: form-data; name="upload2"; filename="C:\file2.txt"
Content-Type: text/plain

This is file2, it's longer.
-----------------------------7db2d1bcc50e6e--

這段內容比較有趣,值得細細觀察。首先,第一個空行以前天然仍是HTTP頭,以後則是Body,而此時的Body也比以前要複雜一些。根據RFC 1867定義,咱們須要選擇一段數據做爲「分割邊界」,這個「邊界數據」不能在內容其餘地方出現,通常來講使用一段從機率上說「幾乎不可能」的數據便可。例如,上面這段數據使用的是IE 9,而我在Chrome下則是這樣的: ide

POST http://www.baidu.com/ HTTP/1.1
Host: www.baidu.com
Content-Length: 473
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryW49oa00LU29E4c5U

------WebKitFormBoundaryW49oa00LU29E4c5U
Content-Disposition: form-data; name="myText"

hello world
------WebKitFormBoundaryW49oa00LU29E4c5U
Content-Disposition: form-data; name="upload1"; filename="file1.txt"
Content-Type: text/plain

This is file1.
------WebKitFormBoundaryW49oa00LU29E4c5U
Content-Disposition: form-data; name="upload2"; filename="file2.txt"
Content-Type: text/plain

This is file2, it's bigger.
------WebKitFormBoundaryW49oa00LU29E4c5U--

很顯然它們兩個選擇了不一樣的數據「模式」做爲邊界——事實上,瀏覽器提交兩次數據時,使用的邊界也可能不會相同,這都沒有問題。 post

選擇了邊界以後,便會將它放在頭部的Content-Type裏傳遞給服務器端,實際須要傳遞的數據即可以分割爲「段」,每段即是「一項」數據。從上面的內容中你們應該都能看出數據傳輸的規範,所以便不作細談了。只強調幾點: 性能

  • 數據均無需額外編碼,直接傳遞便可,例如您能夠看出上面的示例中的「空格」均沒有變成加號。至於這裏您能夠看到清晰地文字內容,是由於咱們上傳了僅僅包含可視ASCII碼的文本文件,若是您上傳一個普通的文件,例如圖片,捕獲到的數據則幾乎徹底不可讀了。
  • IE和Chrome在filename的選擇策略上有所不一樣,前者是文件的完整路徑,然後者則僅僅是文件名。
  • 數據內容以兩條橫線結尾,並一樣以一個換行結束。在網絡協議中通常都以連續的CR、LF(即\r、\n,或0x0D、Ox0A)字符做爲換行,這與Windows的標準一致。若是您使用其餘操做系統,則須要考慮它們的換行符

實現

瞭解上述策略以後,使用編程來實現文件上傳也是瓜熟蒂落的事情,例如我這裏便編寫了一段簡單的代碼實現這一功能。

首先,咱們定義一個Part類,表示每「段」,它的Write方法會寫入整段數據。每段數據分爲Header和Body兩部分,使用WriteHeader和WriteBody兩個抽象方法寫入:

public abstract class Part { protected abstract void WriteHeader(StreamWriter writer); protected abstract void WriteBody(StreamWriter writer); public void Write(StreamWriter writer)
    { this.WriteHeader(writer);
        writer.WriteLine(); this.WriteBody(writer);
    }
}

接着即是表示普通字段的NormalPart和文件上傳得FilePart:

public class NormalPart : Part { public string Name { get; set; } public string Value { get; set; } protected override void WriteHeader(StreamWriter writer)
    {
        writer.WriteLine("Content-Disposition: form-data; name=\"{0}\"", this.Name);
    } protected override void WriteBody(StreamWriter writer)
    {
        writer.WriteLine(this.Value);
    }
} public class FilePart : Part { public string Name { get; set; } public string FilePath { get; set; } protected override void WriteHeader(StreamWriter writer)
    {
        writer.WriteLine( "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"", this.Name, Path.GetFileName(this.FilePath));

        writer.WriteLine("Content-Type: application/octet-stream");
    } protected override void WriteBody(StreamWriter writer)
    { var data = File.ReadAllBytes(this.FilePath);
        writer.Flush();
        writer.BaseStream.Write(data, 0, data.Length);
        writer.WriteLine();
    }
}

最後即是統一寫入各段的Write方法,我在這裏使用新建的GUID做爲「邊界」:

static void Write(StreamWriter writer, IEnumerable<Part> parts)
{ var guidBytes = Guid.NewGuid().ToByteArray(); var boundary = "----------------" + Convert.ToBase64String(guidBytes); foreach (var p in parts)
    {
        writer.WriteLine(boundary);
        p.Write(writer);
    }

    writer.WriteLine(boundary + "--");
}

其實就是這麼簡單。不過在實際狀況中可能會複雜一些。例如,因爲HTTP協議須要先發送頭信息,所以咱們須要提早計算出Content-Length再傳輸全部內容,不過我相信這對您來講也不會是件難事。

其餘

世界上已經有了足夠多的協議,在我看來在絕大部分狀況下都無所謂使用自定義的協議。協議在制定時,每每也會考慮到安全、性能等諸多方面,有時候咱們本身所謂的「顧慮」其理由也並不充分。更重要的是,使用現成的協議,咱們每每都有現成的實現,對於開發和測試都會有很大幫助。

RFC 1867是一個很簡單的協議,固然再簡單也不是我這短短一篇文章能夠完整描述的,其中不少細節(例如在同一個「段」中上傳多個文件)就要靠您本身去挖掘了。

廣告時間:nBazaar技術會議的郵件列表已經正式啓用,全部用戶也已添加完成。目前已經發送了第一封郵件,建議您檢查一下本身的收件箱或垃圾箱,確保能夠收到將來的郵件。若有任何疑問,請發郵件至。

本文基於署名 2.5 中國大陸許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名趙劼(包含連接),具體操做方式可參考此處。如您有任何疑問或者受權方面的協商,請給我留言

ADD YOUR COMMENT

19 條回覆

相關文章
相關標籤/搜索