現在使用HTTP協議定製API已是十分常見的事情,在普通的GET和POST請求中傳遞些參數估計人人都會,可是若是咱們須要上傳文件呢?若是隻是傳遞單個文件,那麼將數據流POST給服務器端便可。但若是須要上傳多個文件,或是在文件以外須要附帶一些信息,那麼又該怎麼作呢?以前我遇到過一些朋友是這麼打算的,他們說,不如就把文件流轉化爲文本,而後把它看成一個普通的字段傳遞。這麼作天然能夠「實現功能」,但缺點也不少。首先,將二進制流轉化爲文本會增大致積(例如最多見的BASE64編碼會增大1/3的數據量);其次,既然互聯網上存在相關的協議,又爲什麼要自定義一套規則呢?其實這即是《RFC 1867 - Form-based File Upload in HTML》,它是咱們用HTML表單上傳文件時使用的傳輸協議,雖然十分經常使用,但彷佛瞭解它的人並很少。 html
提及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,您看得出這是「你好世界」這四個漢字嗎? 安全
不過以前的HTML表單是沒法上傳文件的,所以RFC 1867應運而生,它的目的即是讓HTML表單能夠提交文件。它對HTML表單的擴展主要是: 服務器
因而,若是咱們要使用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裏傳遞給服務器端,實際須要傳遞的數據即可以分割爲「段」,每段即是「一項」數據。從上面的內容中你們應該都能看出數據傳輸的規範,所以便不作細談了。只強調幾點: 性能
瞭解上述策略以後,使用編程來實現文件上傳也是瓜熟蒂落的事情,例如我這裏便編寫了一段簡單的代碼實現這一功能。
首先,咱們定義一個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 中國大陸許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名趙劼(包含連接),具體操做方式可參考此處。如您有任何疑問或者受權方面的協商,請給我留言。