go标准库的学习-mime/multipart
参考: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的更多相关文章
- go标准库的学习-mime
参考:https://studygolang.com/pkgdoc 导入方法: import "mime" mime实现了MIME的部分规定. 什么是MIME: MIME(Mult ...
- go标准库的学习-net/http
参考:https://studygolang.com/pkgdoc 概念解释: request:用户请求的信息,用来解析用户的请求信息,包括post.get.cookie.url等信息 respons ...
- go标准库的学习-database/sql
参考:https://studygolang.com/pkgdoc 导入方式: import "database/sql" sql包提供了保证SQL或类SQL数据库的泛用接口. 使 ...
- go标准库的学习-crypto/md5
参考:https://studygolang.com/pkgdoc 导入方式: import "crypto/md5" md5包实现了MD5哈希算法,参见RFC 1321. Con ...
- go标准库的学习-crypto/sha1
参考:https://studygolang.com/pkgdoc 导入方式: import "crypto/sha1" sha1包实现了SHA1哈希算法,参见RFC 3174. ...
- go标准库的学习-crypto/sha256
参考:https://studygolang.com/pkgdoc 导入方式: import "crypto/sha256" sha256包实现了SHA224和SHA256哈希算法 ...
- python 标准库基础学习之开发工具部分1学习
#2个标准库模块放一起学习,这样减少占用地方和空间#标准库之compileall字节编译源文件import compileall,re,sys#作用是查找到python文件,并把它们编译成字节码表示, ...
- python calendar标准库基础学习
# -*- coding: utf-8 -*-# 作者:新手__author__ = 'Administrator'#标准库:日期时间基础学习:calendar:处理日期#例1import calen ...
- 《C标准库》学习笔记整理
简介 <C标准库>书中对 C 标准库中的 15 个头文件的内容进行了详细的介绍,包括各头文件设计的背景知识.头文件中的内容.头文件中定义的函数和变量的使用.实现.测试等. 我学习此书的目的 ...
随机推荐
- Maven配置国内镜像仓库
eclipse 位置
- [Linux] Linux Shell查找文件
#!/bin/bash # 查找当前目录中所有大于1mb的文件,把文件名写到文本文件中,统计个数 # # find命令,参数:路径地址,命令参数,-size n (查找长度为n的文件) -type f ...
- csharp: Use of Is and As operators in csharp
/// <summary> /// Geovin Du 20170622 /// </summary> /// <param name="sender" ...
- 使用 NGINX 流控和 fail2ban 防止 CC 攻击
背景知识 CC 攻击 攻击者通过创建大量请求导致服务器资源耗尽,主要针对特定服务接口,属于实现 DoS 攻击的一种方式(DoS 攻击更多是针对网络端口,而不是具体服务接口). NGINX 流控 lim ...
- PostgreSQL 10 如何使用 PgAdmin3
自从 PgAdmin4 出来以后,PgAdmin3 就停止开发了,PgAdmin 官网下载的 PgAdmin3 无法支持 PostgreSQL 10 或者更高版本的数据库服务器端. 但是 PgAdmi ...
- Linux 下的 PostgreSQL 数据库+文件通用自动备份脚本
由于 Odoo(原名 OpenERP)自 8.0 以来会生成 CSS,并把附件存储在文件系统中,因此以前单纯备份数据库的脚本已经不够用了.出于实际部署的考虑,我专门写了个较为通用的 Odoo 备份脚本 ...
- 【转】64位系统下无法使用libpam-mysql的md5
转自:http://superwf.dyndns.info/?p=331 Aug 23 09:05:57 wfoffice saslauthd[7235]: pam_mysql – non-crypt ...
- 【Python】keras使用Lenet5识别mnist
原始论文中的网络结构如下图: keras生成的网络结构如下图: 代码如下: import numpy as np from keras.preprocessing import image from ...
- 使用wxpy来实现自动发送消息统计微信好友信息的功能
发送消息太频繁会出现禁言消息 1:导入wxpy模块 pip install wxpy pip3 install wxpy #二者选一 调用模块 # 导入模块 from wxpy import * # ...
- Appium初识
一. Appium工作原理 基本工作流程如下: Appium提供了一套web服务,Appium起一个Server(4723端口),用于与脚本client通信. server接收web driver(即 ...