起因: 困惑
使用了go的http服务后, 发现 request.Body 居然只能读取一次,第二次读取数据为nil.
比如我在gin的服务器中, 先加入了accessLog,需要进行parseForm() 但是后续居然读不到数据.
所以打算深入分析一下,然后简单的解决下这个问题,再优化一下.
分析下源代码
request.Body的类型为: Body io.ReadCloser
// Body is the request's body.
//
// For client requests, a nil body means the request has no 对于客户端请求,nil 正文表示请求没有
// body, such as a GET request. The HTTP Client's Transport 正文,例如 GET 请求。HTTP 客户端的传输
// is responsible for calling the Close method. 负责调用 Close 方法。
// //
// For server requests, the Request Body is always non-nil 对于服务器请求,请求正文始终为非 nil
// but will return EOF immediately when no body is present. 但会在body没有提供数据时立即返回EOF。
// The Server will close the request body. The ServeHTTP 服务器将关闭请求正文。The ServeHTTP
// Handler does not need to. 处理程序不需要。
// //
// Body must allow Read to be called concurrently with Close. 正文必须允许读取与关闭同时调用。
// In particular, calling Close should unblock a Read waiting 特别是,调用 Close 应取消阻止读取等待
// for input. 用于输入。
Body io.ReadCloser
可以看到 这就是一个 reader+closer 接口,真实的数据可以打印出来是:
log.Printf("c.Request.Body is %T", c.Request.Body)
结果为:
c.Request.Body is *http.body
http.body是个未导出的类型
// body turns a Reader into a ReadCloser.
// Close ensures that the body has been fully read
// and then reads the trailer if necessary.
type body struct {
src io.Reader
hdr any // non-nil (Response or Request) value means read trailer
r *bufio.Reader // underlying wire-format reader for the trailer
closing bool // is the connection to be closed after reading body?
doEarlyClose bool // whether Close should stop early
mu sync.Mutex // guards following, and calls to Read and Close
sawEOF bool
closed bool
earlyClose bool // Close called and we didn't read to the end of src
onHitEOF func() // if non-nil, func to call when EOF is Read
}
好家伙 , 里面 src 又是一个 io.Reader 大部分又是 io.LimitReader ,算了不分析这个了, 无法直接进行 reset 的, 那就写一个新的reader 就好了.
纯Reader 是没有Reset或Seek接口的,这个真是应该把 request.Body
设置为 ReadSeekCloser
这种接口 ,那样就不会有此文问题了.
解决方案 (重点)
简单点的就是 读出一个 bytes 然后再覆盖掉 request.Body即可
func readBodyAndSetBodyRepeatRead(c *gin.Context, cb func()) {
//logger.WARN("c.Request.Body is %T", c.Request.Body) // *http.body
if s, ok := c.Request.Body.(io.Seeker); ok {
//logger.WARN("c.Request.Body is io.Seeker:%v", s)
//执行读取Body的操作
cb()
//再次设置可读状态
_, err := s.Seek(0, 0)
if err == nil {
return
}
}
bs, _ := io.ReadAll(c.Request.Body)
//_ = c.Request.Body.Close()// NOTE 原始的 Body 无需手动关闭,会在 response.reqBody中自动关闭的.
//设置可读状态
r := bytes.NewReader(bs)
c.Request.Body = io.NopCloser(r)
//执行读取Body的操作
cb()
//再次设置可读状态
_, _ = r.Seek(0, 0)
//logger.WARN("c.Request.Body is %T", c.Request.Body) //io.nopCloserWriterTo
}
bytes.NewReader 支持进行 Seek 设置,也就是可以重置读取指针(游标)位置,如果下次再次运行,就可以直接设置了
本想着 c.Request.Body 是不是要Close()呢? 查了下,发现可以不管,因为再 Response 结束后,会关闭的. 而且,这种写法也是安全的,可以看参考资料2
至于要不要用 pool 再次优化高并发下的性能,以减少 GC, 可以参考 参考资料2
,我这里 就够用了.
用法
我是在gin下测试的,换做其他库简单修改下即可,反正request 都是原生 http.* 包下的.
readBodyAndSetBodyRepeatRead(c, func() {
_ = c.Request.ParseForm()
})
参考资料:
1 本人学识
2 掘金网: 如何让 gin 正确多次读取 http request body 内容