• 欢迎光临~

http 文件上传数据格式及基于 golang 的文件接收服务实现

开发技术 开发技术 2022-05-29 次浏览

背景

最近在实现一个文件上传的需求,学习了一下 http 进行文件上传时的请求数据结构,以及如何基于 golang 实现服务端获取文件信息并存到本地。

http 文件上传

基于 http 的文件上传,主要是利用 http 协议中的 multipart/form-data 这个 Content-Type。利用它上传文件时,其请求体结构如下:

POST /test HTTP/1.1
Host: foo.example
Content-Type: multipart/form-data;boundary="BbC04y"

--BbC04y
Content-Disposition: form-data; name="meta-data"

{
    "reqeust_id" : "abdefg"
}

--BbC04y
Content-Disposition: form-data; name="file"

...file value
--BbC04y--

Content-Typemultipart/form-data 说明了这个请求的请求体可能会包含多个部分的数据。不同的部分会用 boundary 声明的字段作为分界线。示例请求即用 BbC04y 作为分界。从示例中可以看到,最终结束部分的分界,是 ---BbC04y--- 的格式。而除最终结束的分界,都是 ---BbC04y 的格式。这点需要额外注意。

从示例中可以看到,除了上传文件数据,还可以上传其他类型的数据。每个部分都有一个 name 字段用以做相应的标志。后台可以基于该字段区分上传的数据。示例中就在第一个部分携带了一个 json 格式的数据,name 设置为 meta-data
而示例的第二部分(name="file")携带的即是文件数据,假设上传的是一个图片,则这个部分就是图片的原始数据。

基于 golang 的文件接收服务

了解了 http 协议如何进行文件上传后,下面展示一个 demo,展示如何编写一个服务端读取文件信息,写入本地。

读取文件信息,主要是使用 http.RequestMultipartReader() 方法,获得 multipart.Reader。成功获取之后,再调用 NextPart() 方法, 即可按需得到对应的 part 的multipart.Part 实例。

func handler(w http.ResponseWriter, r *http.Request) {
	reader, _ := r.MultipartReader()
	metadata, _ := reader.NextPart()
	filepart, _ := reader.NextPart()
        ...
}

multipart.Part 提供了 FormName 来获取对应部分的名字,实现了 Read 接口方法来读取其中的数据。下面以上述的 HTTP 示例请求为例,编写服务端的代码:

package main

import (
	"encoding/json"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
)

func handler(w http.ResponseWriter, r *http.Request) {
	reader, err := r.MultipartReader()
	if err != nil {
		log.Println("get multi part reader error: ", err.Error())
		return
	}
	metadata, err := reader.NextPart()
	if err != nil {
		log.Println("get metadata part error: ", err.Error())
		return
	}
	readMetaData(metadata)
	log.Println("metadata part name: ", metadata.FormName())
	filepart, err := reader.NextPart()
	if err != nil {
		log.Println("get file part error: ", err.Error())
		return
	}
	log.Println("file part name: ", filepart.FormName())
	readFile(filepart)
	w.Write([]byte("request successfule"))
}

func readMetaData(part *multipart.Part) {
	dataCache := make([]byte, 1024)
	n, err := part.Read(dataCache)
	if err != nil && err != io.EOF {
		log.Println("read meta data error. ", err.Error())
		return
	}
	data := &MetaData{}
	err = json.Unmarshal(dataCache[:n], data)
	if err != nil {
		log.Println("json unmarshal error. ", err.Error())
		return
	}
	log.Printf("read meta data %+v", data)
}

func readFile(part *multipart.Part) {
	f, err := os.Create("temp.png")
	if err != nil {
		log.Println("create file error. ", err.Error())
		return
	}
	buf := make([]byte, 1024)
	for {
		n, err := part.Read(buf)
		if err != nil && err != io.EOF {
			log.Println("read file data error. ", err.Error())
			return
		}
		if err == io.EOF || n == 0 {
			break
		}
		_, err = f.Write(buf[:n])
		if err != nil {
			log.Println("write file error. ", err.Error())
			return
		}
	}
	log.Println("write file success. ")
}

func main() {
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

实现时有几点需要注意的是

  1. 需要声明读取数据的 buf size,这个不宜设置过大,否则一次性读取太多数据到内存,也不宜设置过小,否则会轮询读取太多次。golang 官方包的 io.Copy 方法,设置的默认大小是 32*1024 也就是 32KB
  2. 由于 Read 方法在读取数据写到 buffer 时,如果数据小于 buffer size,那么剩余的部分也会被填入空值。因此将 buffer 中的数据写入到本地文件中时,需要利用返回的读取数据 size(n) 进行截断,即 f.Write(buf[:n]) 的方式调用。否则多余的空值也会被写入本地文件中,导致文件错误。

上述的代码会在终端输出如下信息,并将上传的图片文件保存到本地。

$ read meta data &{RequestID:abcdefg}
$ metadata part name:  meta-data
$ file part name:  file
$ write file success. 
喜欢 (0)