Go的Pipe应用场景,往服务器提交multipart请求

Stella981
• 阅读 868

multipart 请求是多部件请求体,一般来多用于上传文件等场景,由于文件上传,请求体会比较大,就不适合在内存中构建完整的请求体(例如使用bytes.Buffer)。

这种情况就可以考虑使用Pipe,它会返回一个Writer和一个Reader,管道流,顾名思义,一头读,一头写。读取磁盘文件,写入网络,并不会缓存在内存中。非常适合这种场景。

func Pipe() (*PipeReader, *PipeWriter) {
    p := &pipe{
        wrCh: make(chan []byte),
        rdCh: make(chan int),
        done: make(chan struct{}),
    }
    return &PipeReader{p}, &PipeWriter{p}
}

Demo

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "github.com/gin-gonic/gin"
    "io"
    "io/ioutil"
    "log"
    "mime/multipart"
    "net/http"
    "net/textproto"
    "strings"
    "time"
)

func main(){

    // Http 服务
    ctx, cancel := context.WithCancel(context.Background())
    ch := make(chan struct{})
    go server(ctx, ch)

    // 管道流
    r, w := io.Pipe()
    defer r.Close()

    // 创建 multipart,指定writer
    formWriter := multipart.NewWriter(w)
    go func() {
        defer w.Close()
        var writer io.Writer

        // 快速构建普通表单项,key/value都是字符串
        formWriter.WriteField("lang", "PHP是宇宙最好的语言")

        // 构建普通的表单项,通过Writer写入数据
        writer, _ = formWriter.CreateFormField("lang")
        writer.Write([]byte("Java是世界上最好的语言"))

        // 构建文件表单项,指定表单名称,以及文件名称,通过Writer写入数据,默认的ContentType 是 application/octet-stream
        writer, _ = formWriter.CreateFormFile("file", "app.json")
        jsonVal, _ := json.Marshal(map[string] string {"name": "KevinBlandy"})
        writer.Write(jsonVal)

        // 自定义part表单项,可以添加自定义的header
        header := textproto.MIMEHeader{}
        header.Set("Content-Disposition", `form-data; name="file"; filename="app1.json"`)        // 自定表单字段名称,文件名称,这是必须的
        header.Set("Content-Type", `application/octet-stream`)                                    // 指定ContentType,这是必须的
        writer, _ = formWriter.CreatePart(header)
        writer.Write([]byte("foo"))

        // 完成写入,需要调用close方法
        formWriter.Close()
    }()

    // 创建http客户端
    client := http.Client{}
    // 创建request请求,指定body reader
    req, _ := http.NewRequest(http.MethodPost, "http://127.0.0.1/upload", r)
    req.Header.Set("Content-Type", formWriter.FormDataContentType()) // 需要正确的设置ContentType

    // 执行请求获取响应
    resp, _ := client.Do(req)
    defer resp.Body.Close()

    // 获取响应
    data, _ := ioutil.ReadAll(resp.Body)
    fmt.Println(string(data))

    // 停止服务器
    cancel()

    // 等待退出
    <- ch
}
func server(ctx context.Context, ch chan <- struct{}){
    router := gin.Default()
    router.POST("/upload", func(ctx *gin.Context) {
        form, _ := ctx.MultipartForm()
        fmt.Println("普通表单项-------------------")
        for key, value := range form.Value {
            fmt.Printf("name=%s, value=%s\n", key, strings.Join(value, ","))
        }
        fmt.Println("文件表单项-------------------")
        for key, value := range form.File {
            for _, file := range value {
                fmt.Printf("name=%s, size=%d, fileName=%s, headers=%v\n", key, file.Size, file.Filename, file.Header)
            }
        }
        ctx.Writer.Header().Set("Content-Type", "text/plan")
        ctx.Writer.WriteString("success")
    })
    server := http.Server{
        Addr: ":80",
        Handler: router,
    }

    // 启动服务
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Panic(err)
        }
    }()

    for {
        select {
            case <- ctx.Done():{
                ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)
                server.Shutdown(ctx)
                log.Println("服务器停止...")
                ch <- struct{}{}
                return
            }
        }
    }
}

日志输出

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:    export GIN_MODE=release
 - using code:    gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /upload                   --> main.server.func1 (3 handlers)
普通表单项-------------------
name=lang, value=PHP是宇宙最好的语言,Java是世界上最好的语言
文件表单项-------------------
name=file, size=22, fileName=app.json, headers=map[Content-Disposition:[form-data; name="file"; filename="app.json"] Content-Type:[application/octet-stream]]
name=file, size=3, fileName=app1.json, headers=map[Content-Disposition:[form-data; name="file"; filename="app1.json"] Content-Type:[application/octet-stream]]
[GIN] 2021/01/17 - 14:00:33 | 200 |            0s |       127.0.0.1 | POST     "/upload"
success
2021/01/17 14:00:34 服务器停止...
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Souleigh ✨ Souleigh ✨
3年前
前端性能优化 - 雅虎军规
无论是在工作中,还是在面试中,web前端性能的优化都是很重要的,那么我们进行优化需要从哪些方面入手呢?可以遵循雅虎的前端优化35条军规,这样对于优化有一个比较清晰的方向.35条军规1.尽量减少HTTP请求个数——须权衡2.使用CDN(内容分发网络)3.为文件头指定Expires或CacheControl,使内容具有缓存性。4.避免空的
Easter79 Easter79
3年前
SpringBoot自定义序列化的使用方式
场景及需求:项目接入了SpringBoot开发,现在需求是服务端接口返回的字段如果为空,那么自动转为空字符串。例如:\    {        "id":1,        "name":null    },    {        "id":2,        "name":"x
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
3年前
SpringBoot自定义序列化的使用方式
场景及需求:项目接入了SpringBoot开发,现在需求是服务端接口返回的字段如果为空,那么自动转为空字符串。例如:\    {        "id":1,        "name":null    },    {        "id":2,        "name":"x
Wesley13 Wesley13
3年前
Java多线程导致的的一个事物性问题
业务场景我们现在有一个类似于文件上传的功能,各个子站点接受业务,业务上传文件,各个子站点的文件需要提交到总站点保存,文件是按批次提交到总站点的,也就是说,一个批次下面约有几百个文件。      考虑到白天提交这么多文件会影响到子站点其他系统带宽,我们将分站点的文件提交到总站点这个操作过程独立出来,放到晚上来做,具体时间是晚上7:00到早上7:00。