Go 語(yǔ)言 multipart 庫(kù)解析

簡(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-DispositionContent-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)
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末邑遏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子棍苹,更是在濱河造成了極大的恐慌无宿,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枢里,死亡現(xiàn)場(chǎng)離奇詭異孽鸡,居然都是意外死亡蹂午,警方通過(guò)查閱死者的電腦和手機(jī)设预,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)裁眯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人赤套,你說(shuō)我怎么就攤上這事巷疼⊥砗” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵嚼沿,是天一觀的道長(zhǎng)估盘。 經(jīng)常有香客問(wèn)我,道長(zhǎng)骡尽,這世上最難降的妖魔是什么遣妥? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮攀细,結(jié)果婚禮上箫踩,老公的妹妹穿的比我還像新娘。我一直安慰自己谭贪,他們只是感情好境钟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著俭识,像睡著了一般慨削。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鱼的,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天理盆,我揣著相機(jī)與錄音,去河邊找鬼凑阶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛衷快,可吹牛的內(nèi)容都是我干的宙橱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蘸拔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼师郑!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起调窍,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤宝冕,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后邓萨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體地梨,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菊卷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宝剖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洁闰。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖万细,靈堂內(nèi)的尸體忽然破棺而出扑眉,到底是詐尸還是另有隱情,我是刑警寧澤赖钞,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布腰素,位于F島的核電站,受9級(jí)特大地震影響雪营,放射性物質(zhì)發(fā)生泄漏耸弄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一卓缰、第九天 我趴在偏房一處隱蔽的房頂上張望计呈。 院中可真熱鬧,春花似錦征唬、人聲如沸捌显。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)扶歪。三九已至,卻和暖如春摄闸,著一層夾襖步出監(jiān)牢的瞬間善镰,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工年枕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留炫欺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓熏兄,卻偏偏與公主長(zhǎng)得像品洛,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子摩桶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容