简介
这篇文章主要介绍使用 Go 语言来实现客户端上传文件和服务端处理接收文件的功能。
1) Client 端上传文件:Uploading files
2) Server 端接收文件: Receving files 并且 Saving files
Client
作为 Client 端,读取本地文件,并上传到服务器,通常需要调用 Server 端提供的接口,向其发出 POST 请求。
Client 端的上传方式主要有两种:
一是 Request Body 就是整个文件内容,通过请求头(即 Header )中的 Content-Type 字段来指定文件类型。
二是用 multipart 表单方式来上传
Server
作为 Server 端,需要处理接收 Client 端上传过来的文件,并保存在服务器的某个目录中。
Server 通常需要提供一个接口供 Client 端访问,当接收到 Client 的调用请求,则需要解析这个请求,并把请求中上传过来的文件读取并保存到磁盘。
相应的,Server 端在处理 Client 端请求时也要区分两种方式
- 如果 Client 端上传的是纯二进制流数据,那么直接读取 body 写入到本地文件中即可
- 如果 Client 端是用 multipart 表单上传的文件,那么就解析表单,再把文件写入本地。
(1)第一种,Client 把文件内容放在整个 body 中来传送
client
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
// "time"
)
const (
Url = "http://localhost:8080/upload"
)
// body is file content
func doUpload(filepath string) {
file, err := os.Open(filepath)
if err != nil {
panic(err)
}
defer file.Close()
res, err := http.Post(Url, "binary/octet-stream", file) // 第二个参数用来指定 "Content-Type"
// resp, err := http.Post(Url, "image/jpeg", file)
if err != nil {
panic(err)
}
defer res.Body.Close()
}
func main() {
var filename string
if len(os.Args) < 2 {
filename = "/home/winkee/abc.txt"
} else {
filename = os.Args[1]
}
doUpload(filename)
}
server
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
)
func UploadHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.Header["Content-Type"])
// create a temporary file to hold the content
f, err := os.OpenFile("received.tmp", os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
n, err := io.Copy(f, r.Body)
if err != nil {
panic(err)
}
log.Printf("%d bytes are recieved.\n", n)
// w.Write([]byte(fmt.Sprintf("%d bytes are recieved.\n", n)))
}
func main() {
http.HandleFunc("/upload", UploadHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
(2)第二种,Client 通过 multipart 表单上传文件
Client 通过 multipart.Writer 的 CreateFormFile() 函数把本地文件写入 Form 中。并设置请求头的 Content-Type 为 writer.FormDataContentType()
,然后发送请求。
需要注意的是:发送请求之前需要把 Writer 关闭。
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func exitIfErr(err error) {
if err != nil {
panic(err)
}
return
}
func multipartUpload(destURL string, f io.Reader, fields map[string]string) (*http.Response, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormFile("file", fields["filename"])
if err != nil {
return nil, fmt.Errorf("CreateFormFile %v", err)
}
_, err = io.Copy(fw, f)
if err != nil {
return nil, fmt.Errorf("copying fileWriter %v", err)
}
for k, v := range fields {
_ = writer.WriteField(k, v)
}
err = writer.Close() // close writer before POST request
if err != nil {
return nil, fmt.Errorf("writerClose: %v", err)
}
resp, err := http.Post(destURL, writer.FormDataContentType(), body)
if err != nil {
return nil, err
}
return resp, nil
// req, err := http.NewRequest("POST", destURL, body)
// if err != nil {
// return nil, err
// }
// req.Header.Set("Content-Type", writer.FormDataContentType())
// if req.Close && req.Body != nil {
// defer req.Body.Close()
// }
// return http.DefaultClient.Do(req)
}
func main() {
var filename string
if len(os.Args) < 2 {
filename = "/home/winkee/abc.txt"
} else {
filename = os.Args[1]
}
f, err := os.Open(filename)
exitIfErr(err)
defer f.Close()
fields := map[string]string{
"filename": filename,
}
res, err := multipartUpload("http://localhost:8080/uploadform", f, fields)
exitIfErr(err)
fmt.Println("res: ", res)
}
CreateFormFile() 函数原型:
// 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)
}
第二个参数,实际上是时需要带上来的 filename 域所需的值(也就是文件名),
比如,客户端发送 POST 请求进行上传时,在 Body 中就会把 filename=FieldValue 写上,
而在发送 GET 请求进行下载时,则是在 url 中把 filename 拼上。
如果想要再上传文件的时候,同时传递一些其他的参数,那么就应该使用 writer.WriteField() 函数,如下:
for k, v := range fields {
_ = writer.WriteField(k, v)
}
这样,形成的 request body 内容如下:
注意:
如果要指定上传的每个部分的Content-Type,则需要重写multipart.Writer的CreateFormField和CreateFormFile方法
func CreateFormFile(fieldname, filename, contentType string, w *multipart.Writer) (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", contentType)
return w.CreatePart(h)
}
server
package main
import (
"fmt"
"io"
"log"
"path/filepath"
"net/http"
"os"
"strings"
)
const (
DefaultUploadDir = "/home/winkee"
)
func ReceiveFormFile(w http.ResponseWriter, r *http.Request) {
// if err = req.ParseMultipartForm(2 << 10); err != nil {
// status = http.StatusInternalServerError
// return
// }
// r.Method should be "POST"
file, header, err := r.FormFile("file")
if err != nil {
panic(err)
}
defer file.Close()
nameParts := strings.Split(header.Filename, ".")
filename := nameParts[1]
savedPath := filepath.Join(DefaultUploadDir, filename)
f, err := os.OpenFile(savedPath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
panic(err)
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
panic(err)
}
return
}
func main() {
http.HandleFunc("/uploadform", ReceiveFormFile)
log.Fatal(http.ListenAndServe(":8080", nil))
}
这里的 header.Filename 需要注意,客户端上传时指定的 filename 可能是 ../../../../../test/hello.txt 这种格式,如果不做 sanitize 的话,保存的时候直接用这个 filename 显然会出问题。
server 的另外一种处理
func ReceiveFormFile(w http.ResponseWriter, r *http.Request) {
const _24K = (1 << 10) * 24 // 24 MB
if err = req.ParseMultipartForm(_24K); nil != err {
status = http.StatusInternalServerError
return
}
for _, fheaders := range req.MultipartForm.File {
for _, hdr := range fheaders {
// open uploaded
var infile multipart.File
if infile, err = hdr.Open(); nil != err {
status = http.StatusInternalServerError
return
}
// open destination
var outfile *os.File
if outfile, err = os.Create("./uploaded/" + hdr.Filename); nil != err {
status = http.StatusInternalServerError
return
}
// 32K buffer copy
var written int64
if written, err = io.Copy(outfile, infile); nil != err {
status = http.StatusInternalServerError
return
}
res.Write([]byte("uploaded file:" + hdr.Filename + ";length:" + strconv.Itoa(int(written))))
}
}
}
解析
http 包中的 Request 结构体提供了 ParseForm() 和 ParseMultipartForm() 两个函数。
在理解它们之间的区别之前,先来看一下 Request 结构体的定义,如下:
type Request struct {
// Form contains the parsed form data, including both the URL
// field's query parameters and the POST or PUT form data.
// This field is only available after ParseForm is called.
// The HTTP client ignores Form and uses Body instead.
Form url.Values
// PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
//
// This field is only available after ParseForm is called.
// The HTTP client ignores PostForm and uses Body instead.
PostForm url.Values
// 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
...
}
可见,Request 结构体中,定义了 3 个不同类型的 Form 相关的数据
Form 是一个通用的数据类型 url.Values,原型如下:
type Values map[string][]string
1) 对于 Form 和 PostForm 来说,一个是 GET url raw 请求中带的参数,另一个是在 POST body 中带的参数,格式都一样,比如 ?name=xxx&age=18 等。服务端在接收到请求时,通过调用 r.ParseForm()
来获得客户端传上来的请求参数。
2) 对于这个 MultipartForm,是用户客户端上传文件时使用的,服务端收到客户端的请求时,通过调用 r.ParseMultipartForm()
函数来获得相应的文件内容。r.ParseMultipartForm() 在内部会调用 r.ParseForm()
来进行部分解析。
multipart.Form 则是在 url.Values 的基础上,再添加了一个包含了文件信息的数据类型,原型如下:
// 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
}
通常,服务端在收到客户端上传的文件请求时,先调用 r.ParseMultipartForm(maxMemorySize) ,然后调用 r.FormFile("uploadfile") 来获得其中的对应控件名的文件(注:multipartForm 可以包含多个文件)。
实际上, r.FormFile(") 内部也会调用 r.ParseMultipartForm(),因此,即使不先调用 r.ParseMultipartForm(), 而直接调用 r.FormFile("") 也是可以的。
但是,如果文件太大的话,可能会出错,因此最好还是先调用 r.ParseMultipartForm() 控制读入内存的大小。
默认的最大是 10 M,也就是:
maxFormSize = int64(10 << 20)