gin正確多次讀取http?request?body內(nèi)容實(shí)現(xiàn)詳解
事件背景
到了年末,沒(méi)有太多事情,總算有時(shí)間深度優(yōu)化自己的 golang http webservice 框架,基于 gin 的。公司目前不少項(xiàng)目都是基于這個(gè) webservice 框架開發(fā)的,所以我有責(zé)任保持這個(gè)框架的性能和穩(wěn)定性。
在自己仔細(xì)讀處理 middleware 中一個(gè)通用函數(shù) GenerateRequestBody 時(shí)發(fā)現(xiàn),之間寫的代碼太過(guò)粗暴,雖然一直能能穩(wěn)定運(yùn)行,但是總感覺(jué)哪里不對(duì),同時(shí)也沒(méi)有利用 sync.pool,明顯這里可以優(yōu)化,對(duì)在高并發(fā)的時(shí)候有很大幫助。
越看以前自己實(shí)現(xiàn)的 GenerateRequestBody 內(nèi)容,越覺(jué)得太過(guò)簡(jiǎn)單,幾乎沒(méi)有什么思考,尤其在 gin middleware 中,這個(gè)函數(shù)在每一個(gè) http 會(huì)話都會(huì)命中,同時(shí)設(shè)置這個(gè)函數(shù)作為 webservice 框架公開函數(shù),也會(huì)被其他小伙伴調(diào)用,所以真的需要認(rèn)真考慮。
前置知識(shí)
GenerateRequestBody 函數(shù)分析
廢話不多說(shuō),先上代碼,我們一起看看代碼的問(wèn)題。
func GenerateRequestBody(c *gin.Context) string { body, err := c.GetRawData() // 讀取 request body 的內(nèi)容 if err != nil { body = utils.StringToBytes("failed to get request body") } c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 創(chuàng)建 io.ReadCloser 對(duì)象傳給 request body return utils.BytesToString(body) // 返回 request body 的值 }
咋一看好像沒(méi)有什么,我們不妨更深入代碼一探究竟。
github.com/gin-gonic/gin@v1.8.1/context.go
// GetRawData returns stream data. func (c *Context) GetRawData() ([]byte, error) { return ioutil.ReadAll(c.Request.Body) }
ReadAll 會(huì)把 request body 中的所有的字節(jié)全部讀出,然后返回一個(gè) []byte 數(shù)組。
src/io/ioutil/ioutil.go
// NopCloser returns a ReadCloser with a no-op Close method wrapping // the provided Reader r. // // As of Go 1.16, this function simply calls io.NopCloser. func NopCloser(r io.Reader) io.ReadCloser { return io.NopCloser(r) }
src/io/io.go
// NopCloser returns a ReadCloser with a no-op Close method wrapping // the provided Reader r. func NopCloser(r Reader) ReadCloser { return nopCloser{r} } type nopCloser struct { Reader } func (nopCloser) Close() error { return nil }
ioutil.NopCloser 實(shí)際就是一個(gè)包裝接口,把 Reader 接口封裝成一個(gè)帶有 Close 方法的對(duì)象,而且 Close 方法是一個(gè)直接返回的空函數(shù)。所以這里就有一個(gè)問(wèn)題,如果你想調(diào)用 Close 關(guān)閉這個(gè) io.ReadCloser 對(duì)象。我只能在邊上,呵呵呵,你懂我的意思。
回歸正題,這些代碼大家應(yīng)該看起來(lái)很眼熟才對(duì)。沒(méi)錯(cuò),這是網(wǎng)絡(luò)上 gin 框架多次讀取 http request body 中內(nèi)容的解決方案。 能想像很多小伙伴就是 copy + paste 了事,流量小或者沒(méi)有什么大規(guī)模應(yīng)用場(chǎng)景下沒(méi)有什么問(wèn)題。如果流量大了?應(yīng)用規(guī)模很多?那怎辦?
gin 如何正確多次讀取 http request body 的內(nèi)容呢? 正確的姿勢(shì)是什么呢?
追本溯源
gin 只不過(guò)是一個(gè) router 框架,真正的 http 請(qǐng)求處理是 golang 中的 net/http 包來(lái)負(fù)責(zé)的。要找到 gin 如何正確多次讀取 http request body 內(nèi)容的方法,就一定要往下追。
寫過(guò) golang http client 的小伙伴都知道,需要手動(dòng)執(zhí)行 resp.Body.Close() 這樣的方法釋放連接。要不然會(huì)因?yàn)榈讓?tcp 端口耗盡,導(dǎo)致無(wú)法創(chuàng)建連接。我們通過(guò)一個(gè)簡(jiǎn)單例子看下:
package main import ( "fmt" "io/ioutil" "log" "net/http" ) func main() { resp, _ := doGet("http://www.baidu.com") defer resp.Body.Close() //go的特殊語(yǔ)法,main函數(shù)執(zhí)行結(jié)束前會(huì)執(zhí)行 resp.Body.Close() fmt.Println(resp.StatusCode) //有http的響應(yīng)碼輸出 if resp.StatusCode == http.StatusOK { //如果響應(yīng)碼為200 body, err := ioutil.ReadAll(resp.Body) //把響應(yīng)的body讀出 if err != nil { //如果有異常 fmt.Println(err) //把異常打印 log.Fatal(err) //日志 } fmt.Println(string(body)) //把響應(yīng)的文本輸出到console } } /** 以GET的方式請(qǐng)求 **/ func doGet(url string) (r *http.Response, e error) { resp, err := http.Get(url) if err != nil { fmt.Println(resp.StatusCode) fmt.Println(err) log.Fatal(err) } return resp, err }
通過(guò)上面的代碼,我們能看到 defer resp.Body.Close() 的代碼,它就是要主動(dòng)關(guān)閉連接。那么也有一個(gè)類似的問(wèn)題,golang 中 net/http 包的 server 代碼是不是也要主動(dòng)管理連接呢?
類似如下:
bodyBytes, _ := ioutil.ReadAll(req.Body) req.Body.Close() // 這里調(diào)用Close req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
但是官方的代碼注釋里卻寫不需要在處理函數(shù)里調(diào)用 Close:Request.Body:"The Server will close the request body. The ServeHTTP Handler does not need to."
感覺(jué)好奇怪,golang 中 net/http 包的 server 自己能關(guān)閉 request,那跟上面類似執(zhí)行 req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 替換了 req.Body 原有內(nèi)容,那么 golang 中 net/http 包的 server 還能正確關(guān)閉以前的 req.Body 嘛?如果不能關(guān)閉,那么類似 GenerateRequestBody 函數(shù)這樣的執(zhí)行過(guò)程,必然在大并發(fā)下,必然導(dǎo)致內(nèi)存泄露和大量 GC 回收,影響服務(wù)響應(yīng)。
值得深入
帶著上面的問(wèn)題,在網(wǎng)上尋找了很久,沒(méi)有能找到解決問(wèn)題的方法,也沒(méi)有人把為什么說(shuō)清楚。沒(méi)有思路,在各種不確定的假設(shè)上,提供一個(gè)公司級(jí)的底層 webservice 框架,必然被公司技術(shù)委員會(huì)的主席們挑戰(zhàn)。
說(shuō)到這里,一不做二不休,直接干就是,往下肝。 順著服務(wù)的啟動(dòng)流程找到了 golang 中 net/http 包的 server.go 文件,然后一個(gè)一個(gè)方法慢慢趴,直到找到了 func (c *conn) serve(ctx context.Context) {} 這個(gè)函數(shù),總算看到了具體內(nèi)容。
src/net/http/server.go
// Serve a new connection. func (c *conn) serve(ctx context.Context) { ... for { w, err := c.readRequest(ctx) // 讀取 request 內(nèi)容 ... } ... // HTTP cannot have multiple simultaneous active requests.[*] // Until the server replies to this request, it can't read another, // so we might as well run the handler in this goroutine. // [*] Not strictly true: HTTP pipelining. We could let them all process // in parallel even if their responses need to be serialized. // But we're not going to implement HTTP pipelining because it // was never deployed in the wild and the answer is HTTP/2. inFlightResponse = w serverHandler{c.server}.ServeHTTP(w, w.req) // 處理請(qǐng)求 inFlightResponse = nil w.cancelCtx() if c.hijacked() { return } w.finishRequest() // 關(guān)閉請(qǐng)求 ... }
看到這里,想要解決問(wèn)題只要看兩個(gè)函數(shù) finishRequest 和 readRequest 就可以了。
finishRequest 函數(shù)分析
func (w *response) finishRequest() { ... // Close the body (regardless of w.closeAfterReply) so we can // re-use its bufio.Reader later safely. w.reqBody.Close() // 關(guān)閉 request body ???,在這里? ... }
是這里? 就在這里關(guān)閉了? 但是這里是 response 啊,不是 request。 繼續(xù)點(diǎn)開看看 response 結(jié)構(gòu)體是什么?
// A response represents the server side of an HTTP response. type response struct { ... req *Request // request for this response reqBody io.ReadCloser ... }
這里有一個(gè) req 是 Request 的指針,那么還有一個(gè) reqBody 作為 io.ReadCloser 是為了干嘛? 不解!不解!不解!
readRequest 函數(shù)分析
// Read next request from connection. func (c *conn) readRequest(ctx context.Context) (w *response, err error) { ... req, err := readRequest(c.bufr) if err != nil { if c.r.hitReadLimit() { return nil, errTooLarge } return nil, err } ... w = &response{ ... req: req, reqBody: req.Body, ... } ... }
看到這里,突然這個(gè)世界晴朗了,所有的事情好像都明白了。心細(xì)的小伙伴一定看出來(lái)眉目了,很有可能真是:一拍大腿的提高。
readRequest 讀取到 req 信息后,在創(chuàng)建 response 的對(duì)象時(shí),同時(shí)將 req 賦值給了 response 中的 req 和 reqBody。 也就是說(shuō) req.Body 和 reqBody 指向了同一個(gè)對(duì)象。 換句話說(shuō),我改變了 req.Body 的指向,reqBody 還保留著最初的 io.ReadCloser 對(duì)象的引用。 不管我怎么改變 req.Body 的值,哪怕是指向了 nil,也不會(huì)影響 server 調(diào)用 finishRequest() 函數(shù)來(lái)關(guān)閉 io.ReadCloser 對(duì)象,因?yàn)?finishRequest 內(nèi)部調(diào)用的是 reqBody。
得出結(jié)論
middleware 中的 req.Body 和 response 中的 reqBody 是兩個(gè)變量。初期,req.Body 和 reqBody 中存放了同一個(gè)地址。但是,當(dāng) req.body = io.NoCloser 時(shí),只是改變了 req.Body 中的指針,而 reqBody 仍舊指向原始請(qǐng)求的 body,故不需要在 middleware 中執(zhí)行關(guān)閉。
在 golang 開發(fā)提交記錄中也找到了類似的說(shuō)明,并解決了這個(gè)問(wèn)題。所以說(shuō)在 Go 1.6 之后已經(jīng)不用擔(dān)心這個(gè)問(wèn)題了。
提交信息:
net/http: don't panic after request if Handler sets Request.Body to nil。
大致的意思是,不用再擔(dān)心把 req.Body 設(shè)置 nil,其實(shí)也就是不用再擔(dān)心重置 req.Body 了,更加不用手動(dòng)關(guān)閉 req.Body。
上手開發(fā)
搞清楚了 golang 中 net/http 包的 server 中對(duì)請(qǐng)求的 request body 處理流程,那么 gin 這邊也好開發(fā)了。 首先我們回到之前的 GenerateRequestBody 函數(shù)。
func GenerateRequestBody(c *gin.Context) string { body, err := c.GetRawData() // 讀取 request body 的內(nèi)容 if err != nil { body = utils.StringToBytes("failed to get request body") } c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 創(chuàng)建 io.ReadCloser 對(duì)象傳給 request body return utils.BytesToString(body) // 返回 request body 的值 }
雖然不需要每次關(guān)閉 c.Request.Body 了,但是我們要注意,沒(méi)調(diào)用一次都會(huì)調(diào)用 bytes.NewBuffer 和 ioutil.NopCloser 一次。ioutil.NopCloser 這個(gè)還好是一個(gè)包裝,之前我們看到了相關(guān)的代碼。但是 bytes.NewBuffer 是一個(gè)重量級(jí)的家伙,我第一反應(yīng)是不是可以用 sync.pool 來(lái)緩存這個(gè)這部分的代碼?
實(shí)際當(dāng)然是可以的,但是 GenerateRequestBody 是一個(gè)函數(shù),c.Request.Body 新的指向在隨后的 gin handler 中也要用,明顯在 GenerateRequestBody 內(nèi)部對(duì) sync.pool 執(zhí)行 Get 和 Put 明顯不合適。
怎么解決呢?也很簡(jiǎn)單,在 gin 的框架 http request 會(huì)話是跟 Context 對(duì)象綁定的,所以直接在 Context 操作,并將 sync.pool Get 對(duì)象放入 Context,然后在 Context 銷毀之前對(duì) sync.pool 執(zhí)行 Put 歸還。
流程圖如下:
gin Middleware 代碼
func ginRequestBodyBuffer() gin.HandlerFunc { return func(c *gin.Context) { var b *RequestBodyBuff // 創(chuàng)建緩存對(duì)象 b = bodyBufferPool.Get().(*RequestBodyBuff) b.bodyBuf.Reset() c.Set(ConstRequestBodyBufferKey, b) // 下一個(gè)請(qǐng)求 c.Next() // 歸還緩存對(duì)象 if o, ok := c.Get(ConstRequestBodyBufferKey); ok { b = o.(*RequestBodyBuff) b.bodyBuf.Reset() // bytes.Buffer 要 reset,但是 slice 就不能,這個(gè)做 io.CopyBuffer 用的 c.Set(ConstRequestBodyBufferKey, nil) // 釋放指向 RequestBodyBuff 對(duì)象 bodyBufferPool.Put(o) // 歸還對(duì)象 c.Request.Body = nil // 釋放指向創(chuàng)建的 io.NopCloser 對(duì)象 } } }
新 GenerateRequestBody 代碼
func GenerateRequestBody(c *gin.Context) string { var b *RequestBodyBuff if o, ok := c.Get(ConstRequestBodyBufferKey); ok { b = o.(*RequestBodyBuff) } else { b = newRequestBodyBuff() } body, err := boostio.ReadAllWithBuffer(c.Request.Body, b.swapBuf) // 讀取 request body 的內(nèi)容,此時(shí) body 的 []byte 是全新的一個(gè)數(shù)據(jù) copy if err != nil { body = utils.StringToBytes("failed to get request body") boost.Logger.Errorw(utils.BytesToString(body), "error", err) } _, err = b.bodyBuf.Write(body) // 把內(nèi)容重新寫入 bytes.Buffer if err != nil { c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) boost.Logger.Errorw(utils.BytesToString(body), "error", err) } else { c.Request.Body = ioutil.NopCloser(b.bodyBuf) } return utils.BytesToString(body) }
測(cè)試代碼
對(duì)開發(fā)好的代碼執(zhí)行循環(huán)測(cè)試,用短鏈接測(cè)試。
while true;do curl -i http://127.0.0.1:8080/yy/; done
總結(jié)
我們通過(guò)上面的操作和使用,基本確認(rèn)了 golang 中 net/http 包中對(duì) request body 的處理流程。 通過(guò)簡(jiǎn)單的開發(fā),我們實(shí)現(xiàn)了 gin 正確多次讀取 http request body 內(nèi)容的方法,同時(shí)加入了 sync.pool 支持。減少了頻繁 bytes.NewBuffer 創(chuàng)建對(duì)資源的消耗,以及提高了資源的利用效率。
以上就是gin正確多次讀取http request body內(nèi)容實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于gin讀取http request body的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang 實(shí)現(xiàn)Location跳轉(zhuǎn)方式
這篇文章主要介紹了golang 實(shí)現(xiàn)Location跳轉(zhuǎn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-05-05go語(yǔ)言實(shí)現(xiàn)短信發(fā)送實(shí)例探究
這篇文章主要為大家介紹了go語(yǔ)言實(shí)現(xiàn)短信發(fā)送實(shí)例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Go實(shí)現(xiàn)socks5服務(wù)器的方法
SOCKS5 是一個(gè)代理協(xié)議,它在使用TCP/IP協(xié)議通訊的前端機(jī)器和服務(wù)器機(jī)器之間扮演一個(gè)中介角色,使得內(nèi)部網(wǎng)中的前端機(jī)器變得能夠訪問(wèn)Internet網(wǎng)中的服務(wù)器,或者使通訊更加安全,這篇文章主要介紹了Go實(shí)現(xiàn)socks5服務(wù)器的方法,需要的朋友可以參考下2023-07-07Go語(yǔ)言遞歸函數(shù)的具體實(shí)現(xiàn)
本文主要介紹了Go語(yǔ)言遞歸函數(shù)的具體實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04Go語(yǔ)言導(dǎo)出內(nèi)容到Excel的方法
這篇文章主要介紹了Go語(yǔ)言導(dǎo)出內(nèi)容到Excel的方法,涉及Go語(yǔ)言操作excel的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02Golang 字符串與字節(jié)數(shù)組互轉(zhuǎn)的實(shí)現(xiàn)
在Go語(yǔ)言中,我們經(jīng)常在字符串和切片之間進(jìn)行轉(zhuǎn)換,本文就詳細(xì)的介紹一下Golang 字符串與字節(jié)數(shù)組互轉(zhuǎn)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02Golang?單元測(cè)試和基準(zhǔn)測(cè)試實(shí)例詳解
這篇文章主要為大家介紹了Golang?單元測(cè)試和基準(zhǔn)測(cè)試實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08