Go 如何利用multipart/form 您所在的位置:网站首页 如何获取上传文件的文件名字 Go 如何利用multipart/form

Go 如何利用multipart/form

2024-07-13 00:35| 来源: 网络整理| 查看: 265

转载地址:https://mp.weixin.qq.com/s/OHzXxfcBaf5RNT4dA38LCQ

1. Form简介

Form(中文译为表单)[1],是HTML标记语言中的重要语法元素。一个Form不仅包含正常的文本内容、标记等,还包含被称为控件的特殊元素。用户通常通过修改控件(比如:输入文本、选择菜单项等)来“完成”表单,然后将表单数据以HTTP Get或Post请求的形式提交(submit)给Web服务器。

很多初学者总是混淆HTML和HTTP。其实,http通常作为html传输的承载体,打个比方,html就像乘客,http就像出租车,将乘客从一个地方运输到另外一个地方。但显然http这辆出租车可不仅仅只拉html这一个乘客,很多格式均可作为http这辆出租车的乘客,比如json(over http)、xml(over http)。

在一个HTML文档中,一个表单的标准格式如下:

                            

这样的一个Form被加载到浏览器中后会呈现为一个表单的样式,当在两个文本框中分别输入文本(或以默认的文本作为输入)后,点击“提交(submit)”,浏览器会向http://localhost:8080发出一个HTTP请求,由于Form的method属性为get,因此该HTTP请求会将表单的输入文本作为查询字符串参数(Query String Parameter,在这里即是?language=go&since=monthly)。服务器端处理完该请求后,会返回一个HTTP承载的应答,该应答被浏览器接收后会按特定样式呈现在浏览器窗口中。上述这个过程可以用总结为下面这幅示意图:

图片

 

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

           

改为post的Form表单在点击提交后发出的http请求与method=get时的请求有何不同呢?不同之处就在于在method=post的情况下,表单的参数不会再以查询字符串参数的形式放在请求的URL中,而是会被写入HTTP的BODY中。我们也将这一过程用一幅示意图的形式总结一下:

图片

 

由于表单参数被放置在HTTP Body中传输(body中的数据为:language=go&since=monthly),因此在该HTTP请求的headers中我们会发现新增一个header字段:Content-Type,在这里例子中,它的值为application/x-www-form-urlencoded。我们可以在Form中使用enctype属性改变Form传输数据的内容编码类型,该属性的默认值就是application/x-www-form-urlencoded(即key1=value1&key2=value2&...的形式)。enctype的其它可选值还包括:

text/plain

multipart/form-data

采用method=get的Form的表单参数以查询字符串参数的形式放入http请求,这使得其应用场景相对局限,比如:

当参数值很多,参数值很长时,可能会超出URL最大长度限制;

传递敏感数据时,参数值以明文放在HTTP请求头是不安全的;

无法胜任传递二进制数据(比如一个文件内容)的情形。

因此,在面对上述这些情形时,method=post的表单更有优势。当enctype为不同值时,method=post的表单在http Body中传输的数据形式如下图:

图片

 

我们看到:enctype=application/x-www-urlencoded时,Body中的数据呈现为key1=value1&key2=value2&...的形式,好似URL的查询字符串参数的组合呈现形式;当enctype=text/plain时,这种编码格式也称为raw,即将数据内容原封不动的放入Body中传输,保持数据的原先的编码方式(通常为utf-8);而当enctype=multipart/form-data时,HTTP Body中的数据以多段(part)的形式呈现,段与段之间使用指定的随机字符串分隔,该随机字符串也会随着HTTP Post请求一并传给服务端(放在Header中的Content-Type的值中,与multipart/form-data使用分号相隔),如:

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

我们来看一个稍微复杂些的enctype=multipart/form-data的例子的示意图:

图片

 

我们用Postman模拟了一个包含5个分段(part)的Post请求,其中包含两个文本分段(text)和三个文件分段,并且这三个文件是不同格式的文件,分别是txt,png和json。针对文件分段,Postman使用每个分段中的Content-Type来指明这个分段的数据内容类型。当服务端接收到这些数据时,根据分段Content-Type的指示,便可以有针对性的对分段数据进行解析了。文件分段的默认Content-Type为text/plain;对于无法识别的文件类型(比如:没有扩展名),文件分段的Content-Type通常会设置为application/octet-stream。

通过Form上传文件是RFC1867规范[2]赋予html的一种能力,并且该能力已被证明非常有用,并被广泛使用,甚至我们可以直接将multipart/form-data作为HTTP Post body的一种数据承载协议在两个端之间传输文件数据。

2. 支持以multipart/form-data格式上传文件的Go服务器

http.Request提供了ParseMultipartForm的方法对以multipart/form-data格式传输的数据进行解析,解析即是将数据映射为Request结构的MultipartForm字段的过程:

// $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代表了一个解析后的multipart/form-data的Body,其结构如下:

// $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 }

我们看到这个Form结构由两个map组成,一个map中存放了所有的value part(就像前面的name、age),另外一个map存放了所有的file part(就像前面的part1.txt、part2.png和part3.json)。value part集合没什么可说的,map的key就是每个值分段中的"name";我们的重点在file part上。每个file part对应一组FileHeader,FileHeader的结构如下:

// $GOROOT/src/mime/multipart/formdata.go type FileHeader struct {         Filename string         Header   textproto.MIMEHeader         Size     int64         content []byte         tmpfile string }

每个file part的FileHeader包含五个字段:

Filename - 上传文件的原始文件名

Size - 上传文件的大小(单位:字节)

content - 内存中存储的上传文件的(部分或全部)数据内容

tmpfile - 在服务器本地的临时文件中存储的部分上传文件的数据内容(如果上传的文件大小大于传给ParseMultipartForm的参数maxMemory,剩余部分存储在临时文件中)

Header - file part的header内容,它亦是一个map,其结构如下:

// $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方法实现的数据映射过程表述为下面这张示意图,这样看起来更为直观:

图片

 

有了上述对通过multipart/form-data格式上传文件的原理的拆解,我们就可以很容易地利用Go http包实现一个简单的支持以multipart/form-data格式上传文件的Go服务器:

// 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命令向上述文件服务器同时上传两个文件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"'

文件上传服务器的运行输出日志如下:

$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

之后我们可以看到:文件上传服务器成功地将接收到的part1.txt和part3.json存储到了当前路径下的upload目录中了!

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

前面进行文件上传的客户端要么是浏览器,要么是Postman,要么是curl,如果我们自己构要造一个支持以multipart/form-data格式上传文件的客户端,应该如何做呢?我们需要按照multipart/form-data的格式构造HTTP请求的包体(Body),还好通过Go标准库提供的mime/multipart包,我们可以很容易地构建出满足要求的包体:

// 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 }

显然上面这个client端的代码的核心是createReqBody函数:

该client在body中创建了三个分段,前两个分段仅仅是我为了演示如何创建text part而故意加入的,真正的上传文件客户端是不需要创建这两个分段(part)的;

createReqBody使用bytes.Buffer作为http body的临时存储;

构建完body内容后,不要忘记调用multipart.Writer的Close方法以写入结尾的boundary标记。

我们使用这个客户端向前面的支持以multipart/form-data格式上传文件的服务器上传一个文件:

// 客户端 $go run client1.go -file hello.txt upload file [hello.txt] ok // 服务端 $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:(


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有