背景
最近在做文件的上传,服务端直接通过请求的 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.Post
的 body
默认单次读取的大小是 32k
?
通过源码,可以看到,go
的 http
请求的发送,最终都是调用了 net/http/transport.go
中 Transport
的 RoundTrip
方法。
简单给个链式关系:
`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
。