在go语言中使用http实现multipart/form-data文件上传,不加载文件到内存功能示例
Go  /  管理员 发布于 6个月前   523
go chunked multipart 文件上传
在日常开发中发现,如果要上传文件,在构造 form 表单的时候要将文件内容全部读取到内存中,
所有研究了一下 multipart/form-data, 看看有没有什么方法能够使用流的方式上传,
发现可以使用 chunked 流模式来上传。
下面是一个示例需要将数据全部读取到内存中。
上传文件示例
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
// 打开要上传的文件
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
// 创建一个缓冲区,用于存储multipart/form-data
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
// 创建一个multipart文件字段
part, err := writer.CreateFormFile("file", "example.txt")
if err != nil {
fmt.Println("Error creating form file:", err)
return
}
// 将文件内容复制到part中
_, err = io.Copy(part, file)
if err != nil {
fmt.Println("Error copying file content:", err)
return
}
// 创建一个额外的文本字段
err = writer.WriteField("description", "This is an example file upload.")
if err != nil {
fmt.Println("Error writing form field:", err)
return
}
// 关闭multipart writer
err = writer.Close()
if err != nil {
fmt.Println("Error closing writer:", err)
return
}
// 创建HTTP请求
req, err := http.NewRequest("POST", "http://www.example.com/upload", &requestBody)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// 设置Content-Type头
req.Header.Set("Content-Type", writer.FormDataContentType())
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response:", err)
return
}
fmt.Println("Response:", string(respBody))
}
multipart/form-data 协议的详细介绍
multipart/form-data 是一种 MIME 类型,用于在 HTTP 请求中上传文件和发送表单数据。
它允许在单个请求中包含多个部分,每个部分可以包含不同类型的数据,如文本字段和文件。
协议概述
Content-Type:
请求头中会包含 Content-Type: multipart/form-data;
boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW,
其中 boundary 是一个唯一的字符串,用于分隔每个部分。
每个部分的格式:
每个部分包含其自己的头部和内容,头部描述该部分的数据类型、名称等信息。
请求结构
一个典型的 multipart/form-data 请求由以下几部分组成:
请求行:指定 HTTP 方法和路径。
头部字段:包括 Content-Type 和 boundary。
多个数据部分:
每个部分由分隔符 boundary 开头。
每个部分有自己的头部和内容。
结束标志:使用终止分隔符 –boundary– 表示数据传输结束。
示例
假设我们上传一个名为 example.txt 的文件,并包含一个名为 description 的文本字段。
请求结构如下:
http
Copy code
POST /upload HTTP/1.1
Host: www.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="description"
This is an example file upload.
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plain
<file content here>
------WebKitFormBoundary7MA4YWxkTrZu0gW--
修改后的代码
思路就是,按照 multipart/form-data 协议构建数据,
查看 go 的相关源码会发现,除了 strings.Reader bytes.Reader 这几种已知长度的 reader,
会被指定长度,其他不知道长度的 reader,go http 客户端都是使用 chunked 传递的。
文件放到字段的后面,自定义 read 方法,
当读取到的时候,使用我们自定义的 reader 即可。
具体代码如下github地址
https://github.com/luxun9527/go-lib/tree/master/net/httpclient/stream
如果对您有帮助,帮我点个 star 就是对我的鼓励。
package stream
import (
"bytes"
"errors"
"fmt"
"io"
"mime/multipart"
)
/*
实现功能
1. 创建一个multipart/form-data格式的流,在读取文件的时候不将整个文件读入内存。
使用chunkded模式传输数据
*/
type MultipartReaderWriter struct {
buf *bytes.Buffer
closeData *bytes.Buffer
r io.Reader
hasStartedFile bool
boundary string
contentType string
writer *multipart.Writer
FileFiledList []*FileField
}
func (fr *MultipartReaderWriter) Read(p []byte) (int, error) {
// 如果文件读取还未开始,先从 buffer 中读取数据
if !fr.hasStartedFile {
n, err := fr.buf.Read(p)
if n > 0 || err != io.EOF {
return n, err
}
fr.hasStartedFile = true
}
n, err := fr.r.Read(p)
if err != nil {
//读取文件数据后,将close数据读取出来
if errors.Is(err, io.EOF) {
return fr.closeData.Read(p)
}
return 0, err
}
return n, err
}
func (fr *MultipartReaderWriter) WriteFiled(key, value string) error {
return fr.writer.WriteField(key, value)
}
func (fr *MultipartReaderWriter) WriteFileField(filedName, fileFiledName string, data io.Reader) error {
if data == nil {
return errors.New("data is nil")
}
field, err := NewFileField(data, filedName, fileFiledName, fr.boundary, fr.buf.Len() > 0)
if err != nil {
return err
}
fr.FileFiledList = append(fr.FileFiledList, field)
return err
}
func (fr *MultipartReaderWriter) Close() error {
fields := make([]io.Reader, 0, len(fr.FileFiledList))
for _, v := range fr.FileFiledList {
fields = append(fields, v)
}
fr.r = io.MultiReader(fields...)
return nil
}
func NewMultipartReaderWriter() (*MultipartReaderWriter, error) {
buffer := bytes.NewBuffer(make([]byte, 0, 500))
closeData := bytes.NewBuffer(make([]byte, 0, 500))
writer := multipart.NewWriter(buffer)
if _, err := fmt.Fprintf(closeData, "\r\n--%s--\r\n", writer.Boundary()); err != nil {
return nil, err
}
multipartReader := &MultipartReaderWriter{
buf: buffer,
closeData: closeData,
contentType: writer.FormDataContentType(),
boundary: writer.Boundary(),
writer: writer,
}
return multipartReader, nil
}
type FileField struct {
r io.Reader
buf *bytes.Buffer
hasStartedFile bool
hasPrev bool
}
func NewFileField(r io.Reader, fileName, fieldName, boundary string, hasPrev bool) (*FileField, error) {
buffer := bytes.NewBuffer(make([]byte, 0, 500))
if hasPrev {
if _, err := fmt.Fprintf(buffer, "\r\n"); err != nil {
return nil, err
}
}
writer := multipart.NewWriter(buffer)
if err := writer.SetBoundary(boundary); err != nil {
return nil, err
}
if _, err := writer.CreateFormFile(fieldName, fileName); err != nil {
return nil, err
}
return &FileField{
r: r,
buf: buffer,
hasStartedFile: false,
}, nil
}
func (fr *FileField) Read(p []byte) (int, error) {
// 如果文件读取还未开始,先从 buffer 中读取数据
if !fr.hasStartedFile {
n, err := fr.buf.Read(p)
if n > 0 || err != io.EOF {
return n, err
}
fr.hasStartedFile = true
}
return fr.r.Read(p)
}
package stream
import (
"io"
"log"
"os"
"net/http"
"testing"
)
func TestMultiPart1(t *testing.T) {
rd, err := NewMultipartReaderWriter()
if err := rd.WriteFiled("key", "value"); err != nil {
log.Panicf("WriteFiled failed err %v", err)
}
fs1, err := os.Open("example.txt")
if err != nil {
log.Panicf("err = %v", err)
}
defer fs1.Close()
if err := rd.WriteFileField("example.txt", "file1", fs1); err != nil {
log.Panicf("WriteFileField1 failed err %v", err)
}
fs2, err := os.Open("example.txt")
if err != nil {
log.Panicf("err = %v", err)
}
defer fs2.Close()
if err := rd.WriteFileField("example.txt", "file2", fs2); err != nil {
log.Panicf("WriteFileField2 failed err %v", err)
}
_ = rd.Close()
data, err := io.ReadAll(rd)
if err != nil {
log.Panicf("ReadAll failed err %v", err)
}
log.Println(data)
}
func TestMultiPart2(t *testing.T) {
rd, err := NewMultipartReaderWriter()
if err := rd.WriteFiled("key", "value"); err != nil {
log.Panicf("WriteFiled failed err %v", err)
}
fs1, err := os.Open("example.txt")
if err != nil {
log.Panicf("err = %v", err)
}
defer fs1.Close()
if err := rd.WriteFileField("example.txt", "file1", fs1); err != nil {
log.Panicf("WriteFileField1 failed err %v", err)
}
fs2, err := os.Open("example.txt")
if err != nil {
log.Panicf("err = %v", err)
}
defer fs2.Close()
if err := rd.WriteFileField("example.txt", "file2", fs2); err != nil {
log.Panicf("WriteFileField2 failed err %v", err)
}
_ = rd.Close()
url := "http://localhost:10011/" // 服务器URL
req, err := http.NewRequest("POST", url, rd)
if err != nil {
log.Panicf("new request error %v", err)
}
req.Header.Set("Content-Type", rd.contentType)
req.TransferEncoding = []string{"chunked"}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("do requeset error %v", err)
}
d, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("read error %v", err)
}
log.Println(string(d))
}
func TestMultServer(t *testing.T) {
if err := http.ListenAndServe(":10011", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
// 获取文件
file1, handler, err := request.FormFile("file1")
if err != nil {
http.Error(writer, "Error retrieving file", http.StatusInternalServerError)
return
}
defer file1.Close()
data, _ := io.ReadAll(file1)
log.Printf("file size: %v content %v filename %v size %v", len(data), string(data), handler.Filename, handler.Size)
// 获取文件
file2, handler, err := request.FormFile("file2")
if err != nil {
http.Error(writer, "Error retrieving file", http.StatusInternalServerError)
return
}
defer file2.Close()
data, _ = io.ReadAll(file2)
log.Printf("file size: %v content %v filename %v size %v", len(data), string(data), handler.Filename, handler.Size)
value := request.FormValue("key")
log.Printf("key: %v", value)
writer.Write([]byte("hello world"))
})); err != nil {
log.Panicf("http server error: %v", err)
}
}
123 在
Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..原梓番博客 在
在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..博主 在
佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..1111 在
佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..路人 在
php中使用hyperf框架调用讯飞星火大模型实现国内版chatgpt功能示例中评论 教程很详细,如果加个前端chatgpt对话页面就完美了..Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号