<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文搞懂 Go 如何利用multipart/form-data實(shí)現(xiàn)文件的上傳與下載

          共 26966字,需瀏覽 54分鐘

           ·

          2021-02-28 20:36


          1. Form簡(jiǎn)介

          Form(中文譯為表單)[1],是HTML標(biāo)記語(yǔ)言中的重要語(yǔ)法元素。一個(gè)Form不僅包含正常的文本內(nèi)容、標(biāo)記等,還包含被稱為控件的特殊元素。用戶通常通過(guò)修改控件(比如:輸入文本、選擇菜單項(xiàng)等)來(lái)“完成”表單,然后將表單數(shù)據(jù)以HTTP Get或Post請(qǐng)求的形式提交(submit)給Web服務(wù)器。

          很多初學(xué)者總是混淆HTML和HTTP。其實(shí),http通常作為html傳輸?shù)某休d體,打個(gè)比方,html就像乘客,http就像出租車,將乘客從一個(gè)地方運(yùn)輸?shù)搅硗庖粋€(gè)地方。但顯然http這輛出租車可不僅僅只拉h(huán)tml這一個(gè)乘客,很多格式均可作為http這輛出租車的乘客,比如json(over http)、xml(over http)。

          在一個(gè)HTML文檔中,一個(gè)表單的標(biāo)準(zhǔn)格式如下:

          <form action="http://localhost:8080/repositories" method="get">
             <input type="text" name="language" value="go" />
             <input type="text" name="since" value="monthly" />
             <input type="submit" />               
          </form> 

          這樣的一個(gè)Form被加載到瀏覽器中后會(huì)呈現(xiàn)為一個(gè)表單的樣式,當(dāng)在兩個(gè)文本框中分別輸入文本(或以默認(rèn)的文本作為輸入)后,點(diǎn)擊“提交(submit)”,瀏覽器會(huì)向http://localhost:8080發(fā)出一個(gè)HTTP請(qǐng)求,由于Form的method屬性為get,因此該HTTP請(qǐng)求會(huì)將表單的輸入文本作為查詢字符串參數(shù)(Query String Parameter,在這里即是?language=go&since=monthly)。服務(wù)器端處理完該請(qǐng)求后,會(huì)返回一個(gè)HTTP承載的應(yīng)答,該應(yīng)答被瀏覽器接收后會(huì)按特定樣式呈現(xiàn)在瀏覽器窗口中。上述這個(gè)過(guò)程可以用總結(jié)為下面這幅示意圖:


          Form中的method也可以使用post,就像下面這樣:

          <form action="http://localhost:8080/repositories" method="post">
             <input type="text" name="language" value="go" />
             <input type="text" name="since" value="monthly" />
             <input type="submit" />
          </form>

          改為post的Form表單在點(diǎn)擊提交后發(fā)出的http請(qǐng)求與method=get時(shí)的請(qǐng)求有何不同呢?不同之處就在于在method=post的情況下,表單的參數(shù)不會(huì)再以查詢字符串參數(shù)的形式放在請(qǐng)求的URL中,而是會(huì)被寫入HTTP的BODY中。我們也將這一過(guò)程用一幅示意圖的形式總結(jié)一下:


          由于表單參數(shù)被放置在HTTP Body中傳輸(body中的數(shù)據(jù)為:language=go&since=monthly),因此在該HTTP請(qǐng)求的headers中我們會(huì)發(fā)現(xiàn)新增一個(gè)header字段:Content-Type,在這里例子中,它的值為application/x-www-form-urlencoded。我們可以在Form中使用enctype屬性改變Form傳輸數(shù)據(jù)的內(nèi)容編碼類型,該屬性的默認(rèn)值就是application/x-www-form-urlencoded(即key1=value1&key2=value2&...的形式)。enctype的其它可選值還包括:

          • text/plain
          • multipart/form-data

          采用method=get的Form的表單參數(shù)以查詢字符串參數(shù)的形式放入http請(qǐng)求,這使得其應(yīng)用場(chǎng)景相對(duì)局限,比如:

          • 當(dāng)參數(shù)值很多,參數(shù)值很長(zhǎng)時(shí),可能會(huì)超出URL最大長(zhǎng)度限制;
          • 傳遞敏感數(shù)據(jù)時(shí),參數(shù)值以明文放在HTTP請(qǐng)求頭是不安全的;
          • 無(wú)法勝任傳遞二進(jìn)制數(shù)據(jù)(比如一個(gè)文件內(nèi)容)的情形。

          因此,在面對(duì)上述這些情形時(shí),method=post的表單更有優(yōu)勢(shì)。當(dāng)enctype為不同值時(shí),method=post的表單在http Body中傳輸?shù)臄?shù)據(jù)形式如下圖:


          我們看到:enctype=application/x-www-urlencoded時(shí),Body中的數(shù)據(jù)呈現(xiàn)為key1=value1&key2=value2&...的形式,好似URL的查詢字符串參數(shù)的組合呈現(xiàn)形式;當(dāng)enctype=text/plain時(shí),這種編碼格式也稱為raw,即將數(shù)據(jù)內(nèi)容原封不動(dòng)的放入Body中傳輸,保持?jǐn)?shù)據(jù)的原先的編碼方式(通常為utf-8);而當(dāng)enctype=multipart/form-data時(shí),HTTP Body中的數(shù)據(jù)以多段(part)的形式呈現(xiàn),段與段之間使用指定的隨機(jī)字符串分隔,該隨機(jī)字符串也會(huì)隨著HTTP Post請(qǐng)求一并傳給服務(wù)端(放在Header中的Content-Type的值中,與multipart/form-data使用分號(hào)相隔),如:

          Content-Type: multipart/form-data; boundary=--------------------------399501358433894470769897

          我們來(lái)看一個(gè)稍微復(fù)雜些的enctype=multipart/form-data的例子的示意圖:


          我們用Postman模擬了一個(gè)包含5個(gè)分段(part)的Post請(qǐng)求,其中包含兩個(gè)文本分段(text)和三個(gè)文件分段,并且這三個(gè)文件是不同格式的文件,分別是txt,png和json。針對(duì)文件分段,Postman使用每個(gè)分段中的Content-Type來(lái)指明這個(gè)分段的數(shù)據(jù)內(nèi)容類型。當(dāng)服務(wù)端接收到這些數(shù)據(jù)時(shí),根據(jù)分段Content-Type的指示,便可以有針對(duì)性的對(duì)分段數(shù)據(jù)進(jìn)行解析了。文件分段的默認(rèn)Content-Type為text/plain;對(duì)于無(wú)法識(shí)別的文件類型(比如:沒(méi)有擴(kuò)展名),文件分段的Content-Type通常會(huì)設(shè)置為application/octet-stream

          通過(guò)Form上傳文件是RFC1867規(guī)范[2]賦予html的一種能力,并且該能力已被證明非常有用,并被廣泛使用,甚至我們可以直接將multipart/form-data作為HTTP Post body的一種數(shù)據(jù)承載協(xié)議在兩個(gè)端之間傳輸文件數(shù)據(jù)。

          2. 支持以multipart/form-data格式上傳文件的Go服務(wù)器

          http.Request提供了ParseMultipartForm的方法對(duì)以multipart/form-data格式傳輸?shù)臄?shù)據(jù)進(jìn)行解析,解析即是將數(shù)據(jù)映射為Request結(jié)構(gòu)的MultipartForm字段的過(guò)程:

          // $GOROOT/src/net/http/request.go

          type Request struct {
              ... ...
              // MultipartForm is the parsed multipart form, including file uploads.
              // This field is only available after ParseMultipartForm is called.
              // The HTTP client ignores MultipartForm and uses Body instead.
              MultipartForm *multipart.Form
              ... ...
          }

          multipart.Form代表了一個(gè)解析后的multipart/form-data的Body,其結(jié)構(gòu)如下:

          // $GOROOT/src/mime/multipart/formdata.go

          // Form is a parsed multipart form.
          // Its File parts are stored either in memory or on disk,
          // and are accessible via the *FileHeader's Open method.
          // Its Value parts are stored as strings.
          // Both are keyed by field name.
          type Form struct {
                  Value map[string][]string
                  File  map[string][]*FileHeader
          }

          我們看到這個(gè)Form結(jié)構(gòu)由兩個(gè)map組成,一個(gè)map中存放了所有的value part(就像前面的name、age),另外一個(gè)map存放了所有的file part(就像前面的part1.txt、part2.png和part3.json)。value part集合沒(méi)什么可說(shuō)的,map的key就是每個(gè)值分段中的"name";我們的重點(diǎn)在file part上。每個(gè)file part對(duì)應(yīng)一組FileHeader,F(xiàn)ileHeader的結(jié)構(gòu)如下:

          // $GOROOT/src/mime/multipart/formdata.go
          type FileHeader struct {
                  Filename string
                  Header   textproto.MIMEHeader
                  Size     int64

                  content []byte
                  tmpfile string
          }

          每個(gè)file part的FileHeader包含五個(gè)字段:

          • Filename - 上傳文件的原始文件名
          • Size - 上傳文件的大小(單位:字節(jié))
          • content - 內(nèi)存中存儲(chǔ)的上傳文件的(部分或全部)數(shù)據(jù)內(nèi)容
          • tmpfile - 在服務(wù)器本地的臨時(shí)文件中存儲(chǔ)的部分上傳文件的數(shù)據(jù)內(nèi)容(如果上傳的文件大小大于傳給ParseMultipartForm的參數(shù)maxMemory,剩余部分存儲(chǔ)在臨時(shí)文件中)
          • Header - file part的header內(nèi)容,它亦是一個(gè)map,其結(jié)構(gòu)如下:
          // $GOROOT/src/net/textproto/header.go

          // A MIMEHeader represents a MIME-style header mapping
          // keys to sets of values.
          type MIMEHeader map[string][]string

          我們可以將ParseMultipartForm方法實(shí)現(xiàn)的數(shù)據(jù)映射過(guò)程表述為下面這張示意圖,這樣看起來(lái)更為直觀:


          有了上述對(duì)通過(guò)multipart/form-data格式上傳文件的原理的拆解,我們就可以很容易地利用Go http包實(shí)現(xiàn)一個(gè)簡(jiǎn)單的支持以multipart/form-data格式上傳文件的Go服務(wù)器:

          // github.com/bigwhite/experiments/multipart-formdata/server/file_server1.go
          package main

          import (
           "fmt"
           "io"
           "net/http"
           "os"
          )

          const uploadPath = "./upload"

          func handleUploadFile(w http.ResponseWriter, r *http.Request) {
           r.ParseMultipartForm(100)
           mForm := r.MultipartForm

           for k, _ := range mForm.File {
            // k is the key of file part
            file, fileHeader, err := r.FormFile(k)
            if err != nil {
             fmt.Println("inovke FormFile error:", err)
             return
            }
            defer file.Close()
            fmt.Printf("the uploaded file: name[%s], size[%d], header[%#v]\n",
             fileHeader.Filename, fileHeader.Size, fileHeader.Header)

            // store uploaded file into local path
            localFileName := uploadPath + "/" + fileHeader.Filename
            out, err := os.Create(localFileName)
            if err != nil {
             fmt.Printf("failed to open the file %s for writing", localFileName)
             return
            }
            defer out.Close()
            _, err = io.Copy(out, file)
            if err != nil {
             fmt.Printf("copy file err:%s\n", err)
             return
            }
            fmt.Printf("file %s uploaded ok\n", fileHeader.Filename)
           }
          }

          func main() {
           http.HandleFunc("/upload", handleUploadFile)
           http.ListenAndServe(":8080", nil)
          }

          我們可以用Postman或下面curl命令向上述文件服務(wù)器同時(shí)上傳兩個(gè)文件part1.txt和part3.json:

          curl --location --request POST ':8080/upload' \
          --form 'name="tony bai"' \
          --form 'age="23"' \
          --form 'file1=@"/your_local_path/part1.txt"' \
          --form 'file3=@"/your_local_path/part3.json"'

          文件上傳服務(wù)器的運(yùn)行輸出日志如下:

          $go run file_server1.go
          the uploaded file: name[part3.json], size[130], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file3\"; filename=\"part3.json\""}, "Content-Type":[]string{"application/json"}}]
          file part3.json uploaded ok
          the uploaded file: name[part1.txt], size[15], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"part1.txt\""}, "Content-Type":[]string{"text/plain"}}]
          file part1.txt uploaded ok

          之后我們可以看到:文件上傳服務(wù)器成功地將接收到的part1.txt和part3.json存儲(chǔ)到了當(dāng)前路徑下的upload目錄中了!

          3. 支持以multipart/form-data格式上傳文件的Go客戶端

          前面進(jìn)行文件上傳的客戶端要么是瀏覽器,要么是Postman,要么是curl,如果我們自己構(gòu)要造一個(gè)支持以multipart/form-data格式上傳文件的客戶端,應(yīng)該如何做呢?我們需要按照multipart/form-data的格式構(gòu)造HTTP請(qǐng)求的包體(Body),還好通過(guò)Go標(biāo)準(zhǔn)庫(kù)提供的mime/multipart包,我們可以很容易地構(gòu)建出滿足要求的包體:

          // github.com/bigwhite/experiments/multipart-formdata/client/client1.go

          ... ...
          var (
           filePath string
           addr     string
          )

          func init() {
           flag.StringVar(&filePath, "file""""the file to upload")
           flag.StringVar(&addr, "addr""localhost:8080""the addr of file server")
           flag.Parse()
          }

          func main() {
           if filePath == "" {
            fmt.Println("file must not be empty")
            return
           }

           err := doUpload(addr, filePath)
           if err != nil {
            fmt.Printf("upload file [%s] error: %s", filePath, err)
            return
           }
           fmt.Printf("upload file [%s] ok\n", filePath)
          }

          func createReqBody(filePath string) (string, io.Reader, error) {
           var err error

           buf := new(bytes.Buffer)
           bw := multipart.NewWriter(buf) // body writer

           f, err := os.Open(filePath)
           if err != nil {
            return "", nil, err
           }
           defer f.Close()

           // text part1
           p1w, _ := bw.CreateFormField("name")
           p1w.Write([]byte("Tony Bai"))

           // text part2
           p2w, _ := bw.CreateFormField("age")
           p2w.Write([]byte("15"))

           // file part1
           _, fileName := filepath.Split(filePath)
           fw1, _ := bw.CreateFormFile("file1", fileName)
           io.Copy(fw1, f)

           bw.Close() //write the tail boundry
           return bw.FormDataContentType(), buf, nil
          }

          func doUpload(addr, filePath string) error {
           // create body
           contType, reader, err := createReqBody(filePath)
           if err != nil {
            return err
           }

           url := fmt.Sprintf("http://%s/upload", addr)
           req, err := http.NewRequest("POST", url, reader)

           // add headers
           req.Header.Add("Content-Type", contType)

           client := &http.Client{}
           resp, err := client.Do(req)
           if err != nil {
            fmt.Println("request send error:", err)
            return err
           }
           resp.Body.Close()
           return nil
          }

          顯然上面這個(gè)client端的代碼的核心是createReqBody函數(shù):

          • 該client在body中創(chuàng)建了三個(gè)分段,前兩個(gè)分段僅僅是我為了演示如何創(chuàng)建text part而故意加入的,真正的上傳文件客戶端是不需要?jiǎng)?chuàng)建這兩個(gè)分段(part)的;
          • createReqBody使用bytes.Buffer作為http body的臨時(shí)存儲(chǔ);
          • 構(gòu)建完body內(nèi)容后,不要忘記調(diào)用multipart.Writer的Close方法以寫入結(jié)尾的boundary標(biāo)記。

          我們使用這個(gè)客戶端向前面的支持以multipart/form-data格式上傳文件的服務(wù)器上傳一個(gè)文件:

          // 客戶端
          $go run client1.go -file hello.txt
          upload file [hello.txt] ok

          // 服務(wù)端
          $go run file_server1.go

          http request: http.Request{Method:"POST", URL:(*url.URL)(0xc00016e100), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{"Accept-Encoding":[]string{"gzip"}, "Content-Length":[]string{"492"}, "Content-Type":[]string{"multipart/form-data; boundary=b55090594eaa1aaac1abad1d89a77ae689130d79d6f66af82590036bd8ba"}, "User-Agent":[]string{"Go-http-client/1.1"}}, Body:(*http.body)(0xc000146380), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:492, TransferEncoding:[]string(nil), Close:false, Host:"localhost:8080", Form:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, PostForm:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, MultipartForm:(*multipart.Form)(0xc000110d50), Trailer:http.Header(nil), RemoteAddr:"[::1]:58569", RequestURI:"/upload", TLS:(*tls.ConnectionState)(nil), Cancel:(<-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc0001463c0)}
          the uploaded file: name[hello.txt], size[15], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"hello.txt\""}, "Content-Type":[]string{"application/octet-stream"}}]
          file hello.txt uploaded ok

          我們看到hello.txt這個(gè)文本文件被成功上傳!

          4. 自定義file分段中的header

          從上面file_server1的輸出來(lái)看,client1這個(gè)客戶端上傳文件時(shí)在file分段(part)中設(shè)置的Content-Type為默認(rèn)的application/octet-stream。有時(shí)候,服務(wù)端可能會(huì)需要根據(jù)這個(gè)Content-Type做分類處理,需要客戶端給出準(zhǔn)確的值。上面的client1實(shí)現(xiàn)中,我們使用了multipart.Writer.CreateFormFile這個(gè)方法來(lái)創(chuàng)建file part:

          // file part1
          _, fileName := filepath.Split(filePath)
          fw1, _ := bw.CreateFormFile("file1", fileName)
          io.Copy(fw1, f)

          下面是標(biāo)準(zhǔn)庫(kù)中CreateFormFile方法的實(shí)現(xiàn)代碼:

          // $GOROOT/mime/multipart/writer.go
          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)
          }

          我們看到無(wú)論待上傳的文件是什么類型,CreateFormFile均將Content-Type置為application/octet-stream這一默認(rèn)值。如果我們要自定義file part中Header字段Content-Type的值,我們就不能直接使用CreateFormFile,不過(guò)我們可以參考其實(shí)現(xiàn):

          // github.com/bigwhite/experiments/multipart-formdata/client/client2.go

          var quoteEscaper = strings.NewReplacer("\\""\\\\", `"`, "\\\"")

          func escapeQuotes(s string) string {
           return quoteEscaper.Replace(s)
          }

          func createReqBody(filePath string) (string, io.Reader, error) {
           var err error

           buf := new(bytes.Buffer)
           bw := multipart.NewWriter(buf) // body writer

           f, err := os.Open(filePath)
           if err != nil {
            return "
          ", nil, err
           }
           defer f.Close()

           // text part1
           p1w, _ := bw.CreateFormField("
          name")
           p1w.Write([]byte("
          Tony Bai"))

           // text part2
           p2w, _ := bw.CreateFormField("
          age")
           p2w.Write([]byte("
          15"))

           // file part1
           _, fileName := filepath.Split(filePath)
           h := make(textproto.MIMEHeader)
           h.Set("
          Content-Disposition",
            fmt.Sprintf(`form-data; name="
          %s"; filename="%s"`,
             escapeQuotes("
          file1"), escapeQuotes(fileName)))
           h.Set("
          Content-Type", "text/plain")
           fw1, _ := bw.CreatePart(h)
           io.Copy(fw1, f)

           bw.Close() //write the tail boundry
           return bw.FormDataContentType(), buf, nil
          }

          我們通過(guò)textproto.MIMEHeader實(shí)例來(lái)自定義file part的header部分,然后基于該實(shí)例調(diào)用CreatePart創(chuàng)建file part,之后將hello.txt的文件內(nèi)容寫到該part的header后面。

          我們運(yùn)行client2來(lái)上傳hello.txt文件,在file_server側(cè),我們就能看到如下日志:

          the uploaded file: name[hello.txt], size[15], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"hello.txt\""}, "Content-Type":[]string{"text/plain"}}]
          file hello.txt uploaded ok

          我們看到file part的Content-Type的值已經(jīng)變?yōu)槲覀冊(cè)O(shè)定的text/plain了。

          5. 解決上傳大文件的問(wèn)題

          在上面的客戶端中存在一個(gè)問(wèn)題,那就是我們?cè)跇?gòu)建http body的時(shí)候,使用了一個(gè)bytes.Buffer加載了待上傳文件的所有內(nèi)容,這樣一來(lái),如果待上傳的文件很大的話,內(nèi)存空間消耗勢(shì)必過(guò)大。那么如何將每次上傳內(nèi)存文件時(shí)對(duì)內(nèi)存的使用限制在一個(gè)適當(dāng)?shù)姆秶蛘哒f(shuō)上傳文件所消耗的內(nèi)存空間不因待傳文件的變大而變大呢?我們來(lái)看下面的這個(gè)解決方案:

          // github.com/bigwhite/experiments/multipart-formdata/client/client3.go
          ... ...
          func createReqBody(filePath string) (string, io.Reader, error) {
           var err error
           pr, pw := io.Pipe()
           bw := multipart.NewWriter(pw) // body writer
           f, err := os.Open(filePath)
           if err != nil {
            return "", nil, err
           }

           go func() {
            defer f.Close()
            // text part1
            p1w, _ := bw.CreateFormField("name")
            p1w.Write([]byte("Tony Bai"))

            // text part2
            p2w, _ := bw.CreateFormField("age")
            p2w.Write([]byte("15"))

            // file part1
            _, fileName := filepath.Split(filePath)
            h := make(textproto.MIMEHeader)
            h.Set("Content-Disposition",
             fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
              escapeQuotes("file1"), escapeQuotes(fileName)))
            h.Set("Content-Type""application/pdf")
            fw1, _ := bw.CreatePart(h)
            cnt, _ := io.Copy(fw1, f)
            log.Printf("copy %d bytes from file %s in total\n", cnt, fileName)
            bw.Close() //write the tail boundry
            pw.Close()
           }()
           return bw.FormDataContentType(), pr, nil
          }

          func doUpload(addr, filePath string) error {
           // create body
           contType, reader, err := createReqBody(filePath)
           if err != nil {
            return err
           }

           log.Printf("createReqBody ok\n")
           url := fmt.Sprintf("http://%s/upload", addr)
           req, err := http.NewRequest("POST", url, reader)

           //add headers
           req.Header.Add("Content-Type", contType)

           client := &http.Client{}
           log.Printf("upload %s...\n", filePath)
           resp, err := client.Do(req)
           if err != nil {
            fmt.Println("request send error:", err)
            return err
           }
           resp.Body.Close()
           log.Printf("upload %s ok\n", filePath)
           return nil
          }

          在這個(gè)方案中,我們通過(guò)io.Pipe函數(shù)創(chuàng)建了一個(gè)讀寫管道,其寫端作為io.Writer實(shí)例傳給multipart.NewWriter,讀端返回給調(diào)用者,用于構(gòu)建http request時(shí)使用。io.Pipe基于channel實(shí)現(xiàn),其內(nèi)部不維護(hù)任何內(nèi)存緩存:

          // $GOROOT/src/io/pipe.go
          func Pipe() (*PipeReader, *PipeWriter) {
                  p := &pipe{
                          wrCh: make(chan []byte),
                          rdCh: make(chan int),
                          done: make(chan struct{}),
                  }
                  return &PipeReader{p}, &PipeWriter{p}
          }

          通過(guò)Pipe返回的讀端讀取管道中數(shù)據(jù)時(shí),如果尚未有數(shù)據(jù)寫入管道,那么讀端會(huì)像讀取channel那樣阻塞在那里。由于http request在被發(fā)送時(shí)(client.Do(req))才會(huì)真正基于構(gòu)建req時(shí)傳入的reader對(duì)Body數(shù)據(jù)進(jìn)行讀取,因此client會(huì)阻塞在對(duì)管道的read上。顯然我們不能將讀寫兩端的操作放在一個(gè)goroutine中,那樣會(huì)因所有g(shù)oroutine都掛起而導(dǎo)致panic。在上面的client3.go代碼中,函數(shù)createReqBody內(nèi)部創(chuàng)建了一個(gè)新goroutine,將真正構(gòu)建multipart/form-data body的工作放在了新goroutine中。新goroutine最終會(huì)將待上傳文件的數(shù)據(jù)通過(guò)管道寫端寫入管道:

          cnt, _ := io.Copy(fw1, f)

          而這些數(shù)據(jù)也會(huì)被client讀取并通過(guò)網(wǎng)絡(luò)連接傳輸出去。io.Copy的實(shí)現(xiàn)如下:

          // $GOROOT/src/io/io.go
          func Copy(dst Writer, src Reader) (written int64, err error) {
                  return copyBuffer(dst, src, nil)
          }

          io.copyBuffer內(nèi)部維護(hù)了一個(gè)默認(rèn)32k的小buffer,它每次從src嘗試最大讀取32k的數(shù)據(jù),并寫入到dst中,直到讀完為止。這樣無(wú)論待上傳的文件有多大,我們實(shí)際上每次上傳所分配的內(nèi)存僅有32k。

          下面就是我們用client3.go上傳一個(gè)大小為252M的文件的日志:

          $go run client3.go -file /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf
          2021/01/10 12:56:45 createReqBody ok
          2021/01/10 12:56:45 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf...
          2021/01/10 12:56:46 copy 264517032 bytes from file ICME-2019-Tutorial-final.pdf in total
          2021/01/10 12:56:46 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf ok
          upload file [/Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf] ok

          $go run file_server1.go
          http request: http.Request{Method:"POST", URL:(*url.URL)(0xc000078200), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{"Accept-Encoding":[]string{"gzip"}, "Content-Type":[]string{"multipart/form-data; boundary=4470ba3867218f1130878713da88b5bd79f33dfbed65566e4fd76a1ae58d"}, "User-Agent":[]string{"Go-http-client/1.1"}}, Body:(*http.body)(0xc000026240), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:-1, TransferEncoding:[]string{"chunked"}, Close:false, Host:"localhost:8080", Form:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, PostForm:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, MultipartForm:(*multipart.Form)(0xc0000122a0), Trailer:http.Header(nil), RemoteAddr:"[::1]:54899", RequestURI:"/upload", TLS:(*tls.ConnectionState)(nil), Cancel:(<-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc000026280)}
          the uploaded file: name[ICME-2019-Tutorial-final.pdf], size[264517032], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"ICME-2019-Tutorial-final.pdf\""}, "Content-Type":[]string{"application/pdf"}}]
          file ICME-2019-Tutorial-final.pdf uploaded ok

          $ls -l upload
          -rw-r--r--  1 tonybai  staff  264517032  1 14 12:56 ICME-2019-Tutorial-final.pdf

          如果你覺(jué)得32k仍然很大,每次上傳要使用更小的buffer,你可以用io.CopyBuffer替代io.Copy:

          // github.com/bigwhite/experiments/multipart-formdata/client/client4.go

          func createReqBody(filePath string) (string, io.Reader, error) {
           var err error
           pr, pw := io.Pipe()
           bw := multipart.NewWriter(pw) // body writer
           f, err := os.Open(filePath)
           if err != nil {
            return "", nil, err
           }

           go func() {
            defer f.Close()
            // text part1
            p1w, _ := bw.CreateFormField("name")
            p1w.Write([]byte("Tony Bai"))

            // text part2
            p2w, _ := bw.CreateFormField("age")
            p2w.Write([]byte("15"))

            // file part1
            _, fileName := filepath.Split(filePath)
            h := make(textproto.MIMEHeader)
            h.Set("Content-Disposition",
             fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
              escapeQuotes("file1"), escapeQuotes(fileName)))
            h.Set("Content-Type""application/pdf")
            fw1, _ := bw.CreatePart(h)
            var buf = make([]byte, 1024)
            cnt, _ := io.CopyBuffer(fw1, f, buf)
            log.Printf("copy %d bytes from file %s in total\n", cnt, fileName)
            bw.Close() //write the tail boundry
            pw.Close()
           }()
           return bw.FormDataContentType(), pr, nil
          }

          運(yùn)行這個(gè)client4:

          $go run client4.go -file /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf
          2021/01/10 13:39:06 createReqBody ok
          2021/01/10 13:39:06 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf...
          2021/01/10 13:39:09 copy 264517032 bytes from file ICME-2019-Tutorial-final.pdf in total
          2021/01/10 13:39:09 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf ok
          upload file [/Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf] ok

          你會(huì)看到雖然上傳成功了,但由于每次read僅能讀1k數(shù)據(jù),對(duì)于大文件來(lái)說(shuō),其上傳的時(shí)間消耗增加了不少。

          6. 參考資料

          • Form-based File Upload in HTML[3]
          • Returning Values from Forms: multipart/form-data[4]
          • 《Go Web Programming》[5]
          • Hypertext Transfer Protocol (HTTP/1.1): Range Requests[6]

          本文中涉及的源碼可以在這里[7](https://github.com/bigwhite/experiments/tree/master/multipart-formdata)下載。

          參考資料

          [1] 

          Form(中文譯為表單): https://www.w3.org/TR/html401/interact/forms.html

          [2] 

          RFC1867規(guī)范: https://www.ietf.org/rfc/rfc1867.txt

          [3] 

          Form-based File Upload in HTML: https://www.ietf.org/rfc/rfc1867.txt

          [4] 

          Returning Values from Forms: multipart/form-data: https://www.ietf.org/rfc/rfc2388.txt

          [5] 

          《Go Web Programming》: https://book.douban.com/subject/27204133/

          [6] 

          Hypertext Transfer Protocol (HTTP/1.1): Range Requests: https://www.ietf.org/rfc/rfc7233.txt

          [7] 

          這里: https://github.com/bigwhite/experiments/tree/master/multipart-formdata

          [8] 

          改善Go語(yǔ)?編程質(zhì)量的50個(gè)有效實(shí)踐: https://www.imooc.com/read/87

          [9] 

          Kubernetes實(shí)戰(zhàn):高可用集群搭建、配置、運(yùn)維與應(yīng)用: https://coding.imooc.com/class/284.html

          [10] 

          我愛(ài)發(fā)短信: https://51smspush.com/

          [11] 

          鏈接地址: https://m.do.co/c/bff6eed92687



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 366
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  一本道在线无码 | 俺也来也去成人拍拍网 | 欧美成人一区二区三区高清 | 大香蕉在线大香蕉国产 | 国产精品美女在线 |