go http.Post 指定 body 单次读取的大小(32k?)

背景

最近在做文件的上传,服务端直接通过请求的 body 获取文件数据。发现上传时的 io 和内存性能并没有充分利用,大文件的上传,单次都是读取 32k ,希望能够手动调节单次读取的区块大小,以提升上传文件的速度。

代码实现

func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	return DefaultClient.Post(url, contentType, body)
}

简单看下 http.Post 的3个参数,可见,若想达到更改读取区块的大小,只有在 body io.Reader 这个参数上下手了。
自己定义了一个结构体 CustomReader ,实现了 io.Reader 接口,可以在边上传边计算文件哈希,并做了上传进度回调,废话不多说,直接上代码。

//#region custom reader
type CustomReader struct {
	curSize int64
	totalSize int64
	reader *os.File
	sha256W hash.Hash
	callback func(curSize, totalSize int64)
}
func NewCustomReader(filePath string, callback func(curSize, totalSize int64)) (*CustomReader, error) {
	reader, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	fInfo, err := reader.Stat()
	if err != nil {
		return nil, err
	}
	totalSize := fInfo.Size()
	sha256W := sha256.New()

	return &CustomReader{
		totalSize:totalSize,
		reader:reader,
		sha256W:sha256W,
		callback:callback,
	}, nil
}
func (this *CustomReader) Read(p []byte) (n int, err error) {
	n, err = this.reader.Read(p)
	if n > 0 {
		if n, err := this.sha256W.Write(p[:n]); err != nil {
			return n, err
		}
		this.curSize += int64(n)
		if this.callback != nil {
			this.callback(this.curSize, this.totalSize)
		}
	}
	return
}
func (this *CustomReader) Close() error {
	return this.reader.Close()
}
func (this *CustomReader) WriteTo(w io.Writer) (written int64, err error) {
	buf := make([]byte, 100 * 1024 * 1024) // 指定100M
	for {
		nr, er := this.reader.Read(buf)
		if nr > 0 {
			nt, et := this.sha256W.Write(buf[0:nr])
			if et != nil {
				err = et
				return
			}
			if nt != nr {
				err = errors.New("invalid write hash")
				return
			}

			nw, ew := w.Write(buf[0:nr])
			if ew != nil {
				err = ew
				return
			}
			if nw != nr {
				err = errors.New("invalid write result")
				return
			}

			written += int64(nw)
			this.curSize = written
		}
		if er != nil {
			if er != io.EOF {
				err = er
			}
			break
		}
		if this.callback != nil {
			this.callback(written, this.totalSize)
		}
	}
	return written, err
}
func (this *CustomReader) Sha256Sum(b []byte) []byte{
	return this.sha256W.Sum(b)
}
//#endregion

// 自定义 reader 使用 WriteTo 来做io的buffer
func UploadByCustomReader(urlStr, filePath string, callback func(curSize, totalSize int64)) (err error) {
	reader, err := NewCustomReader(filePath, callback)
	if err != nil {
		return
	}
	defer reader.Close()

	resp, err := http.Post(urlStr, "multipart/form-data", reader)
	if err != nil {
		return
	}

	if resp.StatusCode != http.StatusOK {
		err = errors.New("no 200 status code")
		return
	}

	fmt.Printf("UploadByCustomReader 上传成功,sha256:%x\n", reader.Sha256Sum(nil))
	return
}

源码分析

go 版本:go1.16.5 windows/amd64
为什么 http.Postbody 默认单次读取的大小是 32k

通过源码,可以看到,gohttp 请求的发送,最终都是调用了 net/http/transport.goTransportRoundTrip 方法。

简单给个链式关系:

`net/http/client.go`
这里会使用 `DefaultClient`
http.Post -> Client.Post-> Client.Do -> Client.do -> Client.send -> send ->

`net/http/roundtrip.go`
若没自定义 `transport`,会使用 `DefaultTransport`
Transport.roundTrip ->

`net/http/transport.go`
Transport.roundTrip -> Transport.getConn -> Transport.queueForDial -> Transport.dialConnFor -> Transport.dialConn -> persistConn.writeLoop -> persistConn.writech -> 

`net/http/request.go`
Request.write -> 

`net/http/transfer.go`
transferWriter.writeBody -> transferWriter.doBodyCopy -> 

`io/io.go`
Copy -> copyBuffer

最终发现,上传写数据到 body 中,是通过 io.Copy 方法来实现的,简单看下这个方法,就能发现,是这里定义的 32k 的区块传输。

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
	// If the reader has a WriteTo method, use it to do the copy.
	// Avoids an allocation and a copy.
	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)
	}
	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
	if rt, ok := dst.(ReaderFrom); ok {
		return rt.ReadFrom(src)
	}
	if buf == nil {
		size := 32 * 1024 // 这里定义了size为32k
		if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
			if l.N < 1 {
				size = 1
			} else {
				size = int(l.N)
			}
		}
		buf = make([]byte, size)
	}
	for {
		nr, er := src.Read(buf)
		if nr > 0 {
			nw, ew := dst.Write(buf[0:nr])
			if nw < 0 || nr < nw {
				nw = 0
				if ew == nil {
					ew = errInvalidWrite
				}
			}
			written += int64(nw)
			if ew != nil {
				err = ew
				break
			}
			if nr != nw {
				err = ErrShortWrite
				break
			}
		}
		if er != nil {
			if er != EOF {
				err = er
			}
			break
		}
	}
	return written, err
}

显而易见,若想要自定义区块传输的大小,可通过实现 WriterTo 接口的 WriteTo 方法。
这里需要注意一点,就是自定义的结构体,还需要实现 Close 方法,因为在 io.Copy 之前,需要 transferWriter.unwrapBody,这里会将我们通过 http.Post 传入的 reader 进行解包,最终传入 io.Copy 的才会是我们自定义的 reader

// net/http/transfer.go  421
func (t *transferWriter) unwrapBody() io.Reader {
	if reflect.TypeOf(t.Body) == nopCloserType {
		return reflect.ValueOf(t.Body).Field(0).Interface().(io.Reader)
	}
	if r, ok := t.Body.(*readTrackingBody); ok {
		// 因为前面有过打包了,会在这里解包,将我们自定义的reader返回
		r.didRead = true
		return r.ReadCloser
	}
	return t.Body
}

总结

通过本次的学习总结,对 http.Post 是如何发送请求有了一个更加深刻的认知,在无从下手的时候,还是读源码最为有效,附带夸一下,goland 的断点调试还是很好用的。

我自己在做上传的时候,还对比了其他很多的上传方式,感兴趣的可以去我的 gitee仓库GoTest,上传相关的代码在 iotest/uploadAndCalcHash.go,服务端代码在 ginServer/api/file.go

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值