简介
这篇文章作为上一篇文章的实践篇,在掌握了基本的 HTTP 中的 multipart/form-data
这种格式的请求之后,现在通过 Go 语言的官方 multipart 库来深入理解如何发送和处理 multipart/form-data
格式的请求
先来看一段客户端请求的代码和一段服务端处理请求的代码
1. 客户端请求
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 语言中,想要发送一个 multipart/form-data
格式的请求体,可以使用官方提供的 mime/multipart
库的 Writer。
这个 Writer 的如下:
// A Writer generates multipart messages.
type Writer struct {
w io.Writer
boundary string
lastpart *part
}
Writer 结构体的三个成员清晰而简单,对应着 multipart/form-data 格式的 body 的样式。
其中 w 是一个我们用来往其中填充 request body 的 buffer writer,boundary 通常是随机生成的 random string,lastpart 就是结尾符 --boundary--
,
创建 multipart/form-data
格式的请求体分为 4 个步骤:
- (1) 创建 Writer
- (2) 往 Writer 中写入定制化的 Header
- (3) 往 Writer 中写入 body 内容(body 内容可以是文件,也可以是字段列表等内容)
- (4) 写入结尾符 boundary(调用 Writer.Close() 即可)
(1) 创建 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 中写入每个 Part 部分的头信息
通过调用 w.CreatePart(mimeHeader)
来创建 Part 的头部分
一个典型的 Part 头部分包含了 boundary 和 Header 部分,其样式如下:
-----------------------------9051914041544843365972754266
Content-Disposition: form-data; name="file"; filename="a.txt"
Content-Type: text/plain
w.CreatePart()
函数就是用来创建上面的内容的,其接受的参数是 MIMEHeader,返回的也是一个 Writer,可以继续往这个 Writer 中写入 body 部分的内容。
调用 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 中写入每个 Part 部分的 Body 内容
现在,调用 w.CreatePart()
成功的创建了一个 Part 的头部分,我们还需要往其中写入该 Part 的 Body 部分的内容。根据内容的不同,我们分为两部分:
<1> Body 内容是文件
对于文件,我们可以直接调用 io.Copy(w, f) 往刚才 w.CreatePart() 返回的 Writer 中写入文件内容
对于文件流 fileReader,直接调用 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)
}
解释:
本质上,表单都是 key - value 的形式,key 就是控件(field)名,而 value 就是具体的值了,在 Part 的头部信息中我们写入了 key 为 filename,而 value 就是我们要写入的文件内容了。
提示:
对于文件类型的 Part 头部,Go 语言的 multipart.Writer
提供了 CreateFormFile()
函数,其封装了创建该 Part 头部的过程,我们直接调用 w.CreateFromFile() 就可以创建该 Part 的头部内容,如下:
// 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 内容是 Field 字段
对于字段类型,其方法类似文件的处理,先创建 Part 头部,再创建相应的 Body 内容。 multipart.Writer
提供了 CreateFormField()
函数来创建该 Part 头部,其内部也调用了 CreatePart(),最终也是返回一个 Writer,我们可以继续往这个 Writer 中填充 body 内容。
当然,如果已经有一个 multipart.Writer 的话,可以直接调用其 WriteField()
函数来往里面写入字段, 因为 WriteField() 内部封装了上述的 CreateFormField() 函数,示例如下:
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 创建的 field 字段,每个 Part 只能有一个 <Key,Value> 对,也就是说,一个部分只能对应一个 field。
生成的请求样式如下:
(4) 写入结尾符 --boudary--
这一步非常重要,如果不写入结尾符,那么服务端收到请求后只能解析第一个 Part。
直接调用 multipart.Writer 的 Close() 方法即可写入结尾符。
// 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. 服务端处理
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 之后可以根据请求头中的 "Content-Type"
来决定如何处理相应的数据。
比如,有一个请求的 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
我们如下解析头部 "Content-Type" 字段,如果是 "multipart/form-data" 则根据 Request 的 body 创建一个 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)
...
}
}
上述代码我们最终通过 NewReader() 函数创建了一个 multipart.Reader
类型
// 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],
}
}
实际上,Request 结构体提供了一个 MultipartReader()
来简化上述的步骤,其源码如下:
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
}
现在来看 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"
}
解析:
通过 Reader 结构体的成员构成我们再一次来理解 multipart/form-data
格式的请求体。
其中,这个结构体主要包含了 bufReader,currentPart, 和 boundar 的定义。
- bufReader 就对应 Writer 结构体中的 w,从 w 中读取内容。
- currentPart 是一个指向 Part 类型的指针,顾名思义,Part 类型代表了 multipart 中的每个 Part。
- boudary 变量则定义了 Part 之间的边界标识符以及结束符。
下面来看 Part 结构体的定义
// 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
}
对于一个如下请求体的 Part,从 Content-Disposition
我们可以看到它是一个文件类型的 Part,
文件内容的部分都是不可打印字符
--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 结构体的 Header 成员就对应了 boundary 下面的 Content-Disposition
,Content-Type
等属性,空过一个换行之后就是文件内容。
通过调用 Reader 的 NextPart() 函数,我们可以遍历一个 multipart 请求体中所有的 Part,其实现如下(已简化):
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)
}
}