從 HTTP 角度看 Go 如何實現文件提交

早前寫過一篇文章,Go HTTP 請求 QuickStart。當時,主要參考 Python 的 requests 大綱介紹 Go 的 net/http 如何發起 HTTP 請求。html

最近,嘗試錄成它的視頻,訪問地址。發現當時雖然寫得比較詳細,但也只是介紹用法,可能不知其因此然。好比文件上傳那部分,若是不瞭解 http 文件上傳協議 RFC 1867,就很難搞懂爲何代碼這麼寫。git

今天,就以這個話題爲基礎,介紹下 Go 如何實現文件上傳。github

相關代碼請訪問 httpdemo/post。本文視頻地址:Go 上傳文件chrome

簡介

簡單來講,HTTP 上傳文件能夠分三個步驟,分別是組織請求體,設置 Content-Type 和發送 Post 請求。POST 請求就不用介紹了,主要關注請求體和請求體內容類型。json

請求體,即 request body,經常使用於 POST 請求上。請求體並不是 POST 特有,GET 也支持,只不過約定俗成的規定,服務端通常會忽略 GET 的請求體。瀏覽器

Content-Type 是什麼?app

由於,請求體的格式並不固定,可能性不少,爲了明確請求體內容類型,HTTP 定義了一個請求頭 Content-Type。ide

常見的 Content-Type 選項有 application/x-www-form-urlencoded(默認的表單提交)、application/json(json)、text/xml(xml 格式)、text/plain(純文本)、application/octet-stream(二進制流)等。工具

提交表單

文件上傳能夠理解爲是提交表單的特例,先經過表單提交這個簡單的例子介紹下整個流程。post

以下是表單提交的 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 表單的功能,即提交兩個文件,uploadfile一、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

相關文章
相關標籤/搜索