簡(jiǎn)介
這篇文章作為上一篇文章的實(shí)踐篇兰粉,在掌握了基本的 HTTP 中的 multipart/form-data
這種格式的請(qǐng)求之后潜支,現(xiàn)在通過(guò) Go 語(yǔ)言的官方 multipart 庫(kù)來(lái)深入理解如何發(fā)送和處理 multipart/form-data
格式的請(qǐng)求
先來(lái)看一段客戶(hù)端請(qǐng)求的代碼和一段服務(wù)端處理請(qǐng)求的代碼
1. 客戶(hù)端請(qǐng)求
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
"mime/multipart"
)
const (
destURL = "localhost:8080"
)
func main() {
var bufReader bytes.Buffer
mpWriter := multipart.NewWriter(&bufReader)
fw, err := mpWriter.CreateFormFile("upload_file", "a.txt")
if err != nil {
fmt.Println("Create form file error: ", err)
return
}
f, _ := os.Open("a.txt")
_, err = io.Copy(fw, f)
if err != nil {
return nil, fmt.Errorf("copying f %v", err)
}
mpWriter.Write([]byte("this is test file"))
mpWriter.WriteField("name", "Trump")
mpWriter.Close()
client := &http.Client{
Timeout: 10 * time.Second
}
// resp, err := http.Post(destURL, writer.FormDataContentType(), bufReader)
req, _ := http.NewRequest("POST", destURL, bufReader)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Accept" , "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.Header.Set("Accept-Charset" , "GBK,utf-8;q=0.7,*;q=0.3")
req.Header.Set("Accept-Encoding" , "gzip,deflate,sdch")
response,_ := client.Do(req)
if response.StatusCode == 200 {
body, _ := ioutil.ReadAll(response.Body)
bodystr := string(body)
fmt.Println(bodystr)
}
}
解析
在 Go 語(yǔ)言中毛俏,想要發(fā)送一個(gè) multipart/form-data
格式的請(qǐng)求體奔则,可以使用官方提供的 mime/multipart
庫(kù)的 Writer技即。
這個(gè) Writer 的如下:
// A Writer generates multipart messages.
type Writer struct {
w io.Writer
boundary string
lastpart *part
}
Writer 結(jié)構(gòu)體的三個(gè)成員清晰而簡(jiǎn)單腌闯,對(duì)應(yīng)著 multipart/form-data 格式的 body 的樣式绳瘟。
其中 w 是一個(gè)我們用來(lái)往其中填充 request body 的 buffer writer,boundary 通常是隨機(jī)生成的 random string姿骏,lastpart 就是結(jié)尾符 --boundary--
糖声,
創(chuàng)建 multipart/form-data
格式的請(qǐng)求體分為 4 個(gè)步驟:
- (1) 創(chuàng)建 Writer
- (2) 往 Writer 中寫(xiě)入定制化的 Header
- (3) 往 Writer 中寫(xiě)入 body 內(nèi)容(body 內(nèi)容可以是文件,也可以是字段列表等內(nèi)容)
- (4) 寫(xiě)入結(jié)尾符 boundary(調(diào)用 Writer.Close() 即可)
(1) 創(chuàng)建 Writer
// NewWriter returns a new multipart Writer with a random boundary,
// writing to w.
func NewWriter(w io.Writer) *Writer {
return &Writer{
w: w,
boundary: randomBoundary(),
}
}
(2) 往 Writer 中寫(xiě)入每個(gè) Part 部分的頭信息
通過(guò)調(diào)用 w.CreatePart(mimeHeader)
來(lái)創(chuàng)建 Part 的頭部分
一個(gè)典型的 Part 頭部分包含了 boundary 和 Header 部分分瘦,其樣式如下:
-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name="file"; filename="a.txt"
Content-Type: text/plain
w.CreatePart()
函數(shù)就是用來(lái)創(chuàng)建上面的內(nèi)容的蘸泻,其接受的參數(shù)是 MIMEHeader,返回的也是一個(gè) Writer嘲玫,可以繼續(xù)往這個(gè) Writer 中寫(xiě)入 body 部分的內(nèi)容悦施。
調(diào)用 w.CreatePart()
的步驟如下:
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fieldname), escapeQuotes(filename)))
h.Set("Content-Type", "application/octet-stream")
w.CreatePart(h)
(3) 往 Writer 中寫(xiě)入每個(gè) Part 部分的 Body 內(nèi)容
現(xiàn)在,調(diào)用 w.CreatePart()
成功的創(chuàng)建了一個(gè) Part 的頭部分去团,我們還需要往其中寫(xiě)入該 Part 的 Body 部分的內(nèi)容抡诞。根據(jù)內(nèi)容的不同,我們分為兩部分:
<1> Body 內(nèi)容是文件
對(duì)于文件渗勘,我們可以直接調(diào)用 io.Copy(w, f) 往剛才 w.CreatePart() 返回的 Writer 中寫(xiě)入文件內(nèi)容
對(duì)于文件流 fileReader沐绒,直接調(diào)用 io.Copy()
拷貝到 w 中俩莽,如下:
f, err := os.Open(filename)
_, err = io.Copy(w, f)
if err != nil {
return nil, fmt.Errorf("copying file to w error: %v", err)
}
解釋?zhuān)?/strong>
本質(zhì)上旺坠,表單都是 key - value 的形式,key 就是控件(field)名扮超,而 value 就是具體的值了取刃,在 Part 的頭部信息中我們寫(xiě)入了 key 為 filename,而 value 就是我們要寫(xiě)入的文件內(nèi)容了出刷。
提示:
對(duì)于文件類(lèi)型的 Part 頭部璧疗,Go 語(yǔ)言的 multipart.Writer
提供了 CreateFormFile()
函數(shù),其封裝了創(chuàng)建該 Part 頭部的過(guò)程馁龟,我們直接調(diào)用 w.CreateFromFile() 就可以創(chuàng)建該 Part 的頭部?jī)?nèi)容崩侠,如下:
// CreateFormFile is a convenience wrapper around CreatePart. It creates
// a new form-data header with the provided field name and file name.
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error){
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fieldname), escapeQuotes(filename)))
h.Set("Content-Type", "application/octet-stream")
return w.CreatePart(h)
}
<2> Body 內(nèi)容是 Field 字段
對(duì)于字段類(lèi)型,其方法類(lèi)似文件的處理坷檩,先創(chuàng)建 Part 頭部却音,再創(chuàng)建相應(yīng)的 Body 內(nèi)容。 multipart.Writer
提供了 CreateFormField()
函數(shù)來(lái)創(chuàng)建該 Part 頭部矢炼,其內(nèi)部也調(diào)用了 CreatePart()系瓢,最終也是返回一個(gè) Writer,我們可以繼續(xù)往這個(gè) Writer 中填充 body 內(nèi)容句灌。
當(dāng)然夷陋,如果已經(jīng)有一個(gè) multipart.Writer 的話(huà),可以直接調(diào)用其 WriteField()
函數(shù)來(lái)往里面寫(xiě)入字段, 因?yàn)?WriteField() 內(nèi)部封裝了上述的 CreateFormField() 函數(shù)骗绕,示例如下:
func main() {
writer := multipart.NewWriter(body)
fields := map[string]string{
"filename": filename,
"age": "88",
"ip": "198.162.5.1",
"city": "New York",
}
for k, v := range fields {
_ = writer.WriteField(k, v)
}
}
注意:由 multipart 創(chuàng)建的 field 字段藐窄,每個(gè) Part 只能有一個(gè) <Key,Value> 對(duì),也就是說(shuō)酬土,一個(gè)部分只能對(duì)應(yīng)一個(gè) field枷邪。
生成的請(qǐng)求樣式如下:
(4) 寫(xiě)入結(jié)尾符 --boudary--
這一步非常重要,如果不寫(xiě)入結(jié)尾符诺凡,那么服務(wù)端收到請(qǐng)求后只能解析第一個(gè) Part东揣。
直接調(diào)用 multipart.Writer 的 Close() 方法即可寫(xiě)入結(jié)尾符。
// Close finishes the multipart message and writes the trailing
// boundary end line to the output.
func (w *Writer) Close() error {
if w.lastpart != nil {
if err := w.lastpart.close(); err != nil {
return err
}
w.lastpart = nil
}
_, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary)
return err
}
2. 服務(wù)端處理
package main
import (
"fmt"
"net/http"
)
func SimpleHandler(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
mediatype, _, _ := mime.ParseMediaType(contentType)
w.Header().Set("Content-Type", "text/plain")
// w.WriteHeader(http.StatusOK)
w.WriteString("Hello world!")
// w.Write([]byte("This is an example.\n"))
}
func main() {
http.HandleFunc("/", IndexHandler)
http.ListenAndServe("127.0.0.0:8000", nil)
}
在我們拿到 Request 之后可以根據(jù)請(qǐng)求頭中的 "Content-Type"
來(lái)決定如何處理相應(yīng)的數(shù)據(jù)腹泌。
比如嘶卧,有一個(gè)請(qǐng)求的 Header 頭信息如下:
POST /t2/upload.do HTTP/1.1
User-Agent: SOHUWapRebot
Accept-Language: zh-cn,zh;q=0.5
Accept-Charset: GBK,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Content-Length: 10780
Content-Type:multipart/form-data; boundary=---------------------------9051914041544843365972754266
Host: w.sohu.com
我們?nèi)缦陆馕鲱^部 "Content-Type" 字段,如果是 "multipart/form-data" 則根據(jù) Request 的 body 創(chuàng)建一個(gè) multipart.Reader
func ReceiveHandler(w http.ResponseWriter, r *http.Request)
contentType := r.Header.Get("Content-Type")
mediatype, param, err := mime.ParseMediaType(contentType)
if mediatype == "multipart/form-data" {
boundary, _ := params["boundary"]
reader := multipart.NewReader(r.Body, boundary)
...
}
}
上述代碼我們最終通過(guò) NewReader() 函數(shù)創(chuàng)建了一個(gè) multipart.Reader
類(lèi)型
// NewReader creates a new multipart Reader reading from r using the given MIME boundary.
// The boundary is usually obtained from the "boundary" parameter of the message's "Content-Type" header.
// Use mime.ParseMediaType to parse such headers.
func NewReader(r io.Reader, boundary string) *Reader {
b := []byte("\r\n--" + boundary + "--")
return &Reader{
bufReader: bufio.NewReaderSize(&stickyErrorReader{r: r}, peekBufferSize),
nl: b[:2],
nlDashBoundary: b[:len(b)-2],
dashBoundaryDash: b[2:],
dashBoundary: b[2 : len(b)-2],
}
}
實(shí)際上凉袱,Request 結(jié)構(gòu)體提供了一個(gè) MultipartReader()
來(lái)簡(jiǎn)化上述的步驟芥吟,其源碼如下:
func (r *Request) MultipartReader() (*multipart.Reader, error) {
if r.MultipartForm == multipartByReader {
return nil, errors.New("http: MultipartReader called twice")
}
if r.MultipartForm != nil {
return nil, errors.New("http: multipart handled by ParseMultipartForm")
}
r.MultipartForm = multipartByReader
return r.multipartReader()
}
func (r *Request) multipartReader() (*multipart.Reader, error) {
v := r.Header.Get("Content-Type")
if v == "" {
return nil, ErrNotMultipart
}
d, params, err := mime.ParseMediaType(v)
if err != nil || d != "multipart/form-data" {
return nil, ErrNotMultipart
}
boundary, ok := params["boundary"]
if !ok {
return nil, ErrMissingBoundary
}
return multipart.NewReader(r.Body, boundary), nil
}
現(xiàn)在來(lái)看 multipart.Reader 的定義:
// Reader is an iterator over parts in a MIME multipart body.
// Reader's underlying parser consumes its input as needed. Seeking isn't supported.
type Reader struct {
bufReader *bufio.Reader
currentPart *Part
partsRead int
nl []byte // "\r\n" or "\n" (set after seeing first boundary line)
nlDashBoundary []byte // nl + "--boundary"
dashBoundaryDash []byte // "--boundary--"
dashBoundary []byte // "--boundary"
}
解析:
通過(guò) Reader 結(jié)構(gòu)體的成員構(gòu)成我們?cè)僖淮蝸?lái)理解 multipart/form-data
格式的請(qǐng)求體。
其中专甩,這個(gè)結(jié)構(gòu)體主要包含了 bufReader钟鸵,currentPart, 和 boundar 的定義。
- bufReader 就對(duì)應(yīng) Writer 結(jié)構(gòu)體中的 w涤躲,從 w 中讀取內(nèi)容棺耍。
- currentPart 是一個(gè)指向 Part 類(lèi)型的指針,顧名思義种樱,Part 類(lèi)型代表了 multipart 中的每個(gè) Part蒙袍。
- boudary 變量則定義了 Part 之間的邊界標(biāo)識(shí)符以及結(jié)束符。
下面來(lái)看 Part 結(jié)構(gòu)體的定義
// A Part represents a single part in a multipart body.
type Part struct {
Header textproto.MIMEHeader
mr *Reader
disposition string
dispositionParams map[string]string
// r is either a reader directly reading from mr, or it's a
// wrapper around such a reader, decoding the
// Content-Transfer-Encoding
r io.Reader
n int // known data bytes waiting in mr.bufReader
total int64 // total data bytes read already
err error // error to return when n == 0
readErr error // read error observed from mr.bufReader
}
對(duì)于一個(gè)如下請(qǐng)求體的 Part嫩挤,從 Content-Disposition
我們可以看到它是一個(gè)文件類(lèi)型的 Part害幅,
文件內(nèi)容的部分都是不可打印字符
--49d03132746bfd98bffc0be04783d061e8acaeec7e0054b4bea84fc0ea2c
Content-Disposition: form-data; name="file"; filename="husky.jpg"
Content-Type: application/octet-stream
JFIF ( %!%)+...383,7(-.+
--++++--+-++++-+++--+------+--7---+77-+--+++7++++76!1AQa"qB
BHf[tTN4t'(4"?i\m=,52?1Nf%* OCW6jWIlWZ.P3<+7V9u?
jeIp=z-v$_e\YZω4 CvXdY(?8wHv%:h?`? 1*6L+X3\9 i)z
?K{j
K{@)9>$#r'gE?-CA1V{qZ?,^SdIdWu;e\1KJJЭ
-G('db}HaHVKmU521XRjc|dFO1fY[ \WYpF9`}e
Part 結(jié)構(gòu)體的 Header 成員就對(duì)應(yīng)了 boundary 下面的 Content-Disposition
,Content-Type
等屬性岂昭,空過(guò)一個(gè)換行之后就是文件內(nèi)容以现。
通過(guò)調(diào)用 Reader 的 NextPart() 函數(shù),我們可以遍歷一個(gè) multipart 請(qǐng)求體中所有的 Part约啊,其實(shí)現(xiàn)如下(已簡(jiǎn)化):
func (r *Reader) NextPart() (*Part, error) {
if r.currentPart != nil {
r.currentPart.Close()
}
for {
line, err := r.bufReader.ReadSlice('\n')
if r.isBoundaryDelimiterLine(line) {
r.partsRead++
bp, err := newPart(r)
if err != nil {
return nil, err
}
r.currentPart = bp
return bp, nil
}
if r.isFinalBoundary(line) {
// Expected EOF
return nil, io.EOF
}
return nil, fmt.Errorf("multipart: unexpected line in Next(): %q", line)
}
}