参考:https://studygolang.com/pkgdoc

导入方式:

import "mime/multipart"

multipart实现了MIME的multipart解析,参见RFC 2046。该实现适用于HTTP(RFC 2388)和常见浏览器生成的multipart主体。

1.什么是multipart/form-data(来自https://blog.csdn.net/five3/article/details/7181521)

multipart/form-data的基础是post请求,即基于post请求来实现的

multipart/form-data形式的post与普通post请求的不同之处体现在请求头,请求体2个部分

1)请求头:

必须包含Content-Type信息,且其值也必须规定为multipart/form-data,同时还需要规定一个内容分割符用于分割请求体中不同参数的内容(普通post请求的参数分割符默认为&,参数与参数值的分隔符为=)。具体的头信息格式如下:

Content-Type: multipart/form-data; boundary=${bound}    

其中${bound} 是一个占位符,代表我们规定的具体分割符;可以自己任意规定,但为了避免和正常文本重复了,尽量要使用复杂一点的内容。如:--0016e68ee29c5d515f04cedf6733
比如有一个body为:

--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO--\r\nContent-Disposition: form-data; name=text\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nwords words words wor=\r\nds words words =\r\nwords words wor=\r\nds words words =\r\nwords words\r\n--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO--\r\nContent-Disposition: form-data; name=submit\r\n\r\nSubmit\r\n--0016e68ee29c5d515f04cedf6733--

2)请求体:

它也是一个字符串,不过和普通post请求体不同的是它的构造方式。普通post请求体是简单的键值对连接,格式如下:

k1=v1&k2=v2&k3=v3

而multipart/form-data则是添加了分隔符、参数描述信息等内容的构造体。具体格式如下:

--${bound}
Content-Disposition: form-data; name="Filename" //第一个参数,相当于k1;然后回车;然后是参数的值,即v1 HTTP.pdf //参数值v1
--${bound} //其实${bound}就相当于上面普通post请求体中的&的作用
Content-Disposition: form-data; name="file000"; filename="HTTP协议详解.pdf" //这里说明传入的是文件,下面是文件提
Content-Type: application/octet-stream //传入文件类型,如果传入的是.jpg,则这里会是image/jpeg %PDF-1.5
file content
%%EOF
--${bound}
Content-Disposition: form-data; name="Upload" Submit Query
--${bound}--

⚠️都是以${bound}为开头的,并且最后一个${bound}后面要加--

2.当传送的是文件时

type File

type File interface {
io.Reader
io.ReaderAt
io.Seeker
io.Closer
}

File是一个接口,实现了对一个multipart信息中文件记录的访问,只能读取文件而不能写入。它的内容可以保持在内存或者硬盘中,如果保持在硬盘中,底层类型就会是*os.File。

type FileHeader

type FileHeader struct {
Filename string
Header textproto.MIMEHeader
// 内含隐藏或非导出字段
}

FileHeader描述一个multipart请求的(一个)文件记录的信息。

func (*FileHeader) Open

func (fh *FileHeader) Open() (File, error)

Open方法打开并返回其关联的文件。

举例

net/http的方法:

func (*Request) FormFile

func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)

FormFile返回以key为键查询request.MultipartForm字段(是解析好的多部件表单,包括上传的文件,只有在调用ParseMultipartForm后才有效)得到结果中的第一个文件和它的信息。

如果必要,本函数会隐式调用ParseMultipartForm和ParseForm。查询失败会返回ErrMissingFile错误。

可见其返回的文件信息,即文件句柄的类型为*multipart.FileHeader。

举例:

 通过表单上传文件,在服务器端处理文件

package main
import(
"fmt"
"net/http"
"log"
"text/template"
"crypto/md5"
"time"
"io"
"strconv"
) func upload(w http.ResponseWriter, r *http.Request){
fmt.Println("method", r.Method) //获得请求的方法 if r.Method == "GET"{ //
html := `<html>
<head>
<title>上传文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://localhost:9090/upload" method="post">
<input type="file" name="uploadfile" />
<input type="hidden" name="token" value="{{.}}" />
<input type="submit" value="upload" />
</form>
</body>
</html>`
crutime := time.Now().Unix()
h := md5.New()
io.WriteString(h, strconv.FormatInt(crutime, ))
token := fmt.Sprintf("%x", h.Sum(nil)) t := template.Must(template.New("test").Parse(html))
t.Execute(w, token)
}else{
r.ParseMultipartForm( << ) //表示maxMemory,调用ParseMultipart后,上传的文件存储在maxMemory大小的内存中,如果大小超过maxMemory,剩下部分存储在系统的临时文件中
file, handler, err := r.FormFile("uploadfile") //根据input中的name="uploadfile"来获得上传的文件句柄
if err != nil{
fmt.Println(err)
return
}
defer file.Close()
fmt.Fprintf(w, "%v,%s", handler.Header, handler.Filename)//得到上传文件的Header和文件名 //然后打开该文件
openFile, err := handler.Open()
if err != nil {
fmt.Println(err)
return
}
data := make([]byte, )
count, err := openFile.Read(data) //读取传入文件的内容
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("read %d bytes: %q\n", count, data[:count])
}
} func main() {
http.HandleFunc("/upload", upload) //设置访问的路由
err := http.ListenAndServe(":9090", nil) //设置监听的端口
if err != nil{
log.Fatal("ListenAndServe : ", err)
}
}

终端返回:

userdeMBP:go-learning user$ go run test.go
method POST
read bytes: "hello\nTest the mime/multipart file"

浏览器返回:

获取其他非文件字段信息的时候就不需要调用r.ParseForm,因为在需要的时候Go自动会去调用。而且ParseMultipartForm调用一次之后,后面再调用不会再有效果

⚠️如果上面的表单form没有设置enctype="multipart/form-data"就会报错:

Content-Type isn't multipart/form-data

上传文件主要三步处理:

  • 表单中增加enctype="multipart/form-data"
  • 服务器调用r.ParseMultipartForm,把上传的文件存储在内存和临时文件中
  • 使用r.FormFile获取文件句柄,然后对文件进行存储等处理

3.Reader

1)Part

type Part

type Part struct {
// 主体的头域,如果存在,是按Go的http.Header风格标准化的,如"foo-bar"改变为"Foo-Bar"。
// 有一个特殊情况,如果"Content-Transfer-Encoding"头的值是"quoted-printable"。
// 该头将从本map中隐藏,而主体会在调用Read时透明的解码。
Header textproto.MIMEHeader
// 内含隐藏或非导出字段
}

Part代表multipart主体的单独一个记录。

func (*Part) FileName

func (p *Part) FileName() string

返回Part 的Content-Disposition 头的文件名参数。

func (*Part) FormName

func (p *Part) FormName() string

如果p的Content-Disposition头值为"form-data",则返回名字参数;否则返回空字符串。

func (*Part) Read

func (p *Part) Read(d []byte) (n int, err error)

Read方法读取一个记录的主体,也就是其头域之后到下一记录之前的部分。

func (*Part) Close

func (p *Part) Close() error

2)Form

type Form

type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}

Form是一个解析过的multipart表格。它的File参数部分保存在内存或者硬盘上,可以使用*FileHeader类型属性值的Open方法访问。它的Value 参数部分保存为字符串,两者都以属性名为键。

func (*Form) RemoveAll

func (f *Form) RemoveAll() error

删除Form关联的所有临时文件。

3)

type Reader

type Reader struct {
// 内含隐藏或非导出字段
}

Reader是MIME的multipart主体所有记录的迭代器。Reader的底层会根据需要解析输入,不支持Seek。

func NewReader

func NewReader(r io.Reader, boundary string) *Reader

函数使用给出的MIME边界和r创建一个multipart读取器。

边界一般从信息的"Content-Type" 头的"boundary"属性获取。可使用mime.ParseMediaType函数解析这种头域。

func (*Reader) ReadForm

func (r *Reader) ReadForm(maxMemory int64) (f *Form, err error)

ReadForm解析整个multipart信息中所有Content-Disposition头的值为"form-data"的记录。它会把最多maxMemory字节的文件记录保存在内存里,其余保存在硬盘的临时文件里。

func (*Reader) NextPart

func (r *Reader) NextPart() (*Part, error)

NextPart返回multipart的下一个记录或者返回错误。如果没有更多记录会返回io.EOF。

1)举例1:

package main
import(
"fmt"
"log"
"io"
"strings"
"net/mail"
"mime"
"mime/multipart"
"io/ioutil"
) func main() {
msg := &mail.Message{
Header: map[string][]string{
"Content-Type": []string{"multipart/mixed; boundary=foo"},
},
Body: strings.NewReader(
"--foo\r\nFoo: one\r\n\r\nA section\r\n" +
"--foo\r\nFoo: two\r\n\r\nAnd another\r\n" +
"--foo--\r\n"),
}
mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
if err != nil {
log.Fatal("1 :",err)
}
if strings.HasPrefix(mediaType, "multipart/") {
mr := multipart.NewReader(msg.Body, params["boundary"])
for {
p, err := mr.NextPart() //p的类型为Part if err == io.EOF {
return
}
if err != nil {
log.Fatal("2 :",err)
}
slurp, err := ioutil.ReadAll(p)
if err != nil {
log.Fatal("3 :",err)
}
fmt.Printf("Part %q: %q\n", p.Header.Get("Foo"), slurp)
}
} }

返回:

userdeMBP:go-learning user$ go run test.go
Part "one": "A section"
Part "two": "And another"

2)举例2:

package main
import(
"fmt"
"log"
"io"
"strings"
"bytes"
"os"
"mime/multipart"
) const (
fileaContents = "This is a test file."
filebContents = "Another test file."
textaValue = "foo"
textbValue = "bar"
boundary = `MyBoundary`
) const message = `
--MyBoundary
Content-Disposition: form-data; name="filea"; filename="filea.txt"
Content-Type: text/plain ` + fileaContents + `
--MyBoundary
Content-Disposition: form-data; name="fileb"; filename="fileb.txt"
Content-Type: text/plain ` + filebContents + `
--MyBoundary
Content-Disposition: form-data; name="texta" ` + textaValue + `
--MyBoundary
Content-Disposition: form-data; name="textb" ` + textbValue + `
--MyBoundary--
` func testFile(fh *multipart.FileHeader, efn, econtent string) multipart.File{
if fh.Filename != efn {
fmt.Printf("filename = %q, want %q\n", fh.Filename, efn)
}else{
fmt.Printf("filename = %q\n", fh.Filename)
}
if fh.Size != int64(len(econtent)) {
fmt.Printf("size = %d, want %d\n", fh.Size, len(econtent))
}else{
fmt.Printf("size = %d\n", fh.Size)
} f, err := fh.Open()
if err != nil {
log.Fatal("opening file:", err)
}
b := new(bytes.Buffer)
_, err = io.Copy(b, f) //复制文件中的内容到b中 if err != nil {
log.Fatal("copying contents:", err)
}
if g := b.String(); g != econtent {
fmt.Printf("contents = %q, want %q\n", g, econtent)
}else{
fmt.Printf("contents = %q\n", g)
}
return f
} func main() {
b := strings.NewReader(strings.Replace(message, "\n", "\r\n", -1))
r := multipart.NewReader(b, boundary)
f, err := r.ReadForm(25) //f为Form类型
if err != nil {
log.Fatal("ReadForm:", err)
}
defer f.RemoveAll() //最后删除Form关联的所有临时文件 //读取Form表格中的内容
if g, e := f.Value["texta"][0], textaValue; g != e {
fmt.Printf("texta value = %q, want %q\n", g, e)
}else{
fmt.Printf("texta value = %q\n", g)
}
if g, e := f.Value["textb"][0], textbValue; g != e {
fmt.Printf("texta value = %q, want %q\n", g, e)
}else{
fmt.Printf("textb value = %q\n", g)
} fd := testFile(f.File["filea"][0], "filea.txt", fileaContents)
if _, ok := fd.(*os.File); ok { //查看fd是否为*os.File类型
fmt.Printf("file is *os.File, should not be")
}
fd.Close()
fd = testFile(f.File["fileb"][0], "fileb.txt", filebContents)
if _, ok := fd.(*os.File); !ok {
fmt.Printf("file has unexpected underlying type %T", fd)
}
fd.Close() }

返回:

userdeMBP:go-learning user$ go run test.go
texta value = "foo"
textb value = "bar"
filename = "filea.txt"
size = 20
contents = "This is a test file."
filename = "fileb.txt"
size = 18
contents = "Another test file."

4.Writer

type Writer

type Writer struct {
// 内含隐藏或非导出字段
}

Writer类型用于生成multipart信息。

func NewWriter

func NewWriter(w io.Writer) *Writer

NewWriter函数返回一个设定了一个随机边界的Writer,数据写入w。

func (*Writer) FormDataContentType

func (w *Writer) FormDataContentType() string

方法返回w对应的HTTP multipart请求的Content-Type的值,多以multipart/form-data起始。

func (*Writer) Boundary

func (w *Writer) Boundary() string

方法返回该Writer的边界。

func (*Writer) SetBoundary

func (w *Writer) SetBoundary(boundary string) error

SetBoundary方法重写Writer默认的随机生成的边界为提供的boundary参数。方法必须在创建任何记录之前调用,boundary只能包含特定的ascii字符,并且长度应在1-69字节之间。

func (*Writer) CreatePart

func (w *Writer) CreatePart(header textproto.MIMEHeader) (io.Writer, error)

CreatePart方法使用提供的header创建一个新的multipart记录。该记录的主体应该写入返回的Writer接口。调用本方法后,任何之前的记录都不能再写入。

func (*Writer) CreateFormField

func (w *Writer) CreateFormField(fieldname string) (io.Writer, error)

CreateFormField方法使用给出的属性名调用CreatePart方法。

func (*Writer) CreateFormFile

func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error)

CreateFormFile是CreatePart方法的包装, 使用给出的属性名和文件名创建一个新的form-data头。

func (*Writer) WriteField

func (w *Writer) WriteField(fieldname, value string) error

WriteField方法调用CreateFormField并写入给出的value。

func (*Writer) Close

func (w *Writer) Close() error

Close方法结束multipart信息,并将结尾的边界写入底层io.Writer接口。

举例:

package main
import(
"fmt"
"log"
"bytes"
"mime/multipart"
"io/ioutil"
) func main() {
fileContents := []byte("my file contents") var b bytes.Buffer
w := multipart.NewWriter(&b) //返回一个设定了一个随机boundary的Writer w,并将数据写入&b
{
part, err := w.CreateFormFile("myfile", "my-file.txt")//使用给出的属性名(对应name)和文件名(对应filename)创建一个新的form-data头,part为io.Writer类型
if err != nil {
fmt.Printf("CreateFormFile: %v\n", err)
}
part.Write(fileContents) //然后将文件的内容添加到form-data头中
err = w.WriteField("key", "val") //WriteField方法调用CreateFormField,设置属性名(对应name)为"key",并在下一行写入该属性值对应的value = "val"
if err != nil {
fmt.Printf("WriteField: %v\n", err)
}
err = w.Close()
if err != nil {
fmt.Printf("Close: %v\n", err)
}
s := b.String()
if len(s) == {
fmt.Println("String: unexpected empty result")
}
if s[] == '\r' || s[] == '\n' {
log.Fatal("String: unexpected newline")
}
fmt.Println(s)
}
fmt.Println(w.Boundary()) //随机生成的boundary为284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9
r := multipart.NewReader(&b, w.Boundary()) part, err := r.NextPart()
if err != nil {
fmt.Printf("part 1: %v\n", err)
}
if g, e := part.FormName(), "myfile"; g != e {
fmt.Printf("part 1: want form name %q, got %q\n", e, g)
}else{
fmt.Printf("part 1: want form name %q\n", e)
}
slurp, err := ioutil.ReadAll(part)
if err != nil {
fmt.Printf("part 1: ReadAll: %v\n", err)
}
if e, g := string(fileContents), string(slurp); e != g {
fmt.Printf("part 1: want contents %q, got %q\n", e, g)
}else{
fmt.Printf("part 1: want contents %q\n", e)
} part, err = r.NextPart()
if err != nil {
fmt.Printf("part 2: %v\n", err)
}
if g, e := part.FormName(), "key"; g != e {
fmt.Printf("part 2: want form name %q, got %q\n", e, g)
}else{
fmt.Printf("part 2: want form name %q\n", e)
}
slurp, err = ioutil.ReadAll(part)
if err != nil {
fmt.Printf("part 2: ReadAll: %v\n", err)
}
if e, g := "val", string(slurp); e != g {
fmt.Printf("part 2: want contents %q, got %q\n", e, g)
}else{
fmt.Printf("part 1: want contents %q\n", e)
} part, err = r.NextPart() //上面的例子只有两个part
if part != nil || err == nil {
fmt.Printf("expected end of parts; got %v, %v\n", part, err)
} }

返回:

userdeMBP:go-learning user$ go run test.go
--284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9
Content-Disposition: form-data; name="myfile"; filename="my-file.txt"
Content-Type: application/octet-stream my file contents
--284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9
Content-Disposition: form-data; name="key" val
--284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9-- 284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9
part : want form name "myfile"
part : want contents "my file contents"
part : want form name "key"
part : want contents "val"

go标准库的学习-mime/multipart的更多相关文章

  1. go标准库的学习-mime

    参考:https://studygolang.com/pkgdoc 导入方法: import "mime" mime实现了MIME的部分规定. 什么是MIME: MIME(Mult ...

  2. go标准库的学习-net/http

    参考:https://studygolang.com/pkgdoc 概念解释: request:用户请求的信息,用来解析用户的请求信息,包括post.get.cookie.url等信息 respons ...

  3. go标准库的学习-database/sql

    参考:https://studygolang.com/pkgdoc 导入方式: import "database/sql" sql包提供了保证SQL或类SQL数据库的泛用接口. 使 ...

  4. go标准库的学习-crypto/md5

    参考:https://studygolang.com/pkgdoc 导入方式: import "crypto/md5" md5包实现了MD5哈希算法,参见RFC 1321. Con ...

  5. go标准库的学习-crypto/sha1

    参考:https://studygolang.com/pkgdoc 导入方式: import "crypto/sha1" sha1包实现了SHA1哈希算法,参见RFC 3174. ...

  6. go标准库的学习-crypto/sha256

    参考:https://studygolang.com/pkgdoc 导入方式: import "crypto/sha256" sha256包实现了SHA224和SHA256哈希算法 ...

  7. python 标准库基础学习之开发工具部分1学习

    #2个标准库模块放一起学习,这样减少占用地方和空间#标准库之compileall字节编译源文件import compileall,re,sys#作用是查找到python文件,并把它们编译成字节码表示, ...

  8. python calendar标准库基础学习

    # -*- coding: utf-8 -*-# 作者:新手__author__ = 'Administrator'#标准库:日期时间基础学习:calendar:处理日期#例1import calen ...

  9. 《C标准库》学习笔记整理

    简介 <C标准库>书中对 C 标准库中的 15 个头文件的内容进行了详细的介绍,包括各头文件设计的背景知识.头文件中的内容.头文件中定义的函数和变量的使用.实现.测试等. 我学习此书的目的 ...

随机推荐

  1. Maven配置国内镜像仓库

    eclipse 位置

  2. [Linux] Linux Shell查找文件

    #!/bin/bash # 查找当前目录中所有大于1mb的文件,把文件名写到文本文件中,统计个数 # # find命令,参数:路径地址,命令参数,-size n (查找长度为n的文件) -type f ...

  3. csharp: Use of Is and As operators in csharp

    /// <summary> /// Geovin Du 20170622 /// </summary> /// <param name="sender" ...

  4. 使用 NGINX 流控和 fail2ban 防止 CC 攻击

    背景知识 CC 攻击 攻击者通过创建大量请求导致服务器资源耗尽,主要针对特定服务接口,属于实现 DoS 攻击的一种方式(DoS 攻击更多是针对网络端口,而不是具体服务接口). NGINX 流控 lim ...

  5. PostgreSQL 10 如何使用 PgAdmin3

    自从 PgAdmin4 出来以后,PgAdmin3 就停止开发了,PgAdmin 官网下载的 PgAdmin3 无法支持 PostgreSQL 10 或者更高版本的数据库服务器端. 但是 PgAdmi ...

  6. Linux 下的 PostgreSQL 数据库+文件通用自动备份脚本

    由于 Odoo(原名 OpenERP)自 8.0 以来会生成 CSS,并把附件存储在文件系统中,因此以前单纯备份数据库的脚本已经不够用了.出于实际部署的考虑,我专门写了个较为通用的 Odoo 备份脚本 ...

  7. 【转】64位系统下无法使用libpam-mysql的md5

    转自:http://superwf.dyndns.info/?p=331 Aug 23 09:05:57 wfoffice saslauthd[7235]: pam_mysql – non-crypt ...

  8. 【Python】keras使用Lenet5识别mnist

    原始论文中的网络结构如下图: keras生成的网络结构如下图: 代码如下: import numpy as np from keras.preprocessing import image from ...

  9. 使用wxpy来实现自动发送消息统计微信好友信息的功能

    发送消息太频繁会出现禁言消息 1:导入wxpy模块 pip install wxpy pip3 install wxpy #二者选一 调用模块 # 导入模块 from wxpy import * # ...

  10. Appium初识

    一. Appium工作原理 基本工作流程如下: Appium提供了一套web服务,Appium起一个Server(4723端口),用于与脚本client通信. server接收web driver(即 ...