從 HTTP 角度看 Go 如何實現文件提交
- 2019 年 12 月 16 日
- 筆記
早前寫過一篇文章,Go HTTP 請求 QuickStart。當時,主要參考 Python 的 requests 大綱介紹 Go 的 net/http 如何發起 HTTP 請求。
最近,嘗試錄成它的視頻,訪問地址。發現當時雖然寫得比較詳細,但也只是介紹用法,可能不知其所以然。比如文件上傳那部分,如果不了解 http 文件上傳協議 RFC 1867,就很難搞懂為什麼代碼這麼寫。
今天,就以這個話題為基礎,介紹下 Go 如何實現文件上傳。
相關代碼請訪問 httpdemo/post。本文視頻地址:Go 上傳文件
簡介
簡單來說,HTTP 上傳文件可以分三個步驟,分別是組織請求體,設置 Content-Type 和發送 Post 請求。POST 請求就不用介紹了,主要關注請求體和請求體內容類型。
請求體,即 request body,常用於 POST 請求上。請求體並非 POST 特有,GET 也支持,只不過約定俗成的規定,服務端一般會忽略 GET 的請求體。
Content-Type 是什麼?
因為,請求體的格式並不固定,可能性很多,為了明確請求體內容類型,HTTP 定義了一個請求頭 Content-Type。
常見的 Content-Type 選項有 application/x-www-form-urlencoded
(默認的表單提交)、application/json
(json)、text/xml
(xml 格式)、text/plain
(純文本)、application/octet-stream
(二進制流)等。
提交表單
文件上傳可以理解為是提交表單的特例,先通過表單提交這個簡單的例子介紹下整個流程。
如下是表單提交的 HTTP 請求文本。
POST http://httpbin.org/post HTTP/1.1 Content-Type: application/x-www-form-urlencoded username=poloxue&password=123456 複製代碼
Content-Type 是 application/x-www-form-urlencoded
,數據通過 urlencoded 方式組織。
先用 html 的 form 表單實現。如下:
<form method="post"> <input type="text" name="username"> <input type="password" name="password"> <input type="submit"> </form> 複製代碼
通過 Post 提交 form 表單,Content-Type 默認是 application/x-www-form-urlencoded
。
Go 的實現代碼:
data := make(url.Values) data.Set("username", "poloxue") data.Set("password", "123456") // 按 urlencoded 組織數據 body, _ := data.Encode() // 創建請求並設置內容類型 request, _ := http.NewRequest( http.MethodPost, "http://httpbin.org/post", bytes.NewReader(body), ) request.Header.Set( "content-type", "application/x-www-form-urlencoded", ) http.DefaultClient.Do(request)
回想下前面說的三個步驟,組織請求體數據、設置 Content-Type 和發送請求。
Go 的 net/htp 包還提供了一個更簡潔的寫法,http.Post。
http.Post( "http://httpbin.org/post", "application/x-www-form-urlencoded", bytes.NewReader(body), )
上傳文件 RFC 1867
文件上傳的需求很常見,但默認的 form 表單提交方式並不支持。
如果是單文件上傳,通過 body 二進制流就可以實現。但如果是一些更複雜的場景,如上傳多文件,則需要自定義上傳協議,而且客戶端和服務端都要提供相應的支持。
文件上傳這種常見需求,如果有一套標準豈不更好。為了解決這個問題,RFC 1867 就誕生了,它主要內容有:
- input 標籤的類型增加一個 file 選項;
- form 表單的 enctype 增加
multipart/form-data
選項;
如下是一個支持文件提交的 form 表單。
<form action="http://httpbin.org/post" method="post" enctype="multipart/form-data" > <input type="text" name="words"/> <input type="file" name="uploadfile1"> <input type="file" name="uploadfile2"> <input type="submit"> </form> 複製代碼
提交表單後,將會看到請求的內容大致形式,如下:
POST http://httpbin.org/post HTTP/1.1 Content-Type: multipart/form-data; boundary=285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 multipart/form-data; boundary=285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 --285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 Content-Disposition: form-data; name="uploadFile1"; filename="uploadfile1.txt" Content-Type: application/octet-stream upload file1 --285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 Content-Disposition: form-data; name="uploadFile1"; filename="uploadfile2.txt" Content-Type: application/octet-stream upload file2 --285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 Content-Disposition: form-data; name="words" 123 --285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350-- 複製代碼
註:如果使用 chrome 瀏覽器的開發者工具,為了性能考慮,無法看到看到這部分內容。而且,如果提交的是二進制流,只是一串亂碼,也沒什麼可看的。
Content-Type 除了 multipart/form-data
,還另外多了 boundary=xxx
的內容。boundary
是邊界的意思,相當於 application/x-www-form-urlencoded
方式中的 &
,用於分隔不同 input 字段。boundary
之所以這麼複雜,因為,一般的文本內容使用了 &
就能分離,但如果是文件流,&
可能和內容衝突,對邊界的唯一性要求更高。
multipart/form-data
內容的詳細格式就不介紹了。繼續說如何用 Go 實現這個功能。
Go 實現代碼
如何使用 Go 實現文件上傳?
主體邏輯依然是組織數據、設置 Content-Type 和發送請求這三步。但這部分數據的組織比 form 表單的 urlencoded 的方式要複雜的多。
Go 的簡潔性這時就體現出來了,因為,標準庫 mime/multipart
已經提供了非常好用的方法,無需自己手動組織。
假設,現在要實現前面 form 表單的功能,即提交兩個文件,uploadfile1、uploadfile2,和一個字段 words。
首先,創建一個用於保存數據的 byte.Buffer
類型的變量,body
,在它之上創建一個 multipart.Writer
,用這個 writer
組織將要提交的數據。代碼如下:
bodyBuf := &bytes.Buffer{} writer := multipart.NewWriter(payloadBuf)
先組織文件內容,兩個文件的組織邏輯相同,就以 uploadfile1 為例進行介紹。在 writer
之上創建一個 fileWriter
,用於寫入文件 uploadFile1 的內容,
fileWriter, err := writer.CreateFormFile("uploadFile1", filename)
打開要上傳的文件,uploadfile1,將文件內容拷貝到 fileWriter
中,如下:
f, err := os.Open("uploadfile1") ... io.Copy(fileWriter, f)
添加字段就非常簡單了,假設設置 words
為 123,代碼如下:
writer.WriteField("words", "123")
完成所有內容設置後,一定要記得關閉 Writer
,否則,請求體會缺少結束邊界。
writer.Close()
完成了數據的組織。
接下來,只要將數據設置到 http.Post
就好了。
r, err := http.Post( "http://httpbin.org/post", writer.FormDataContentType(), body, )
完成了支持文件上傳的表單提交。
總結
本篇文章主要介紹了如何使用 Go 實現文件上傳,本質上是組織提交文件的請求體。而為了能清晰地了解請求體的組織過程,就必須清楚相關的 HTTP 協議,rfc 1867。