欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

gin正確多次讀取http?request?body內容實現(xiàn)詳解

 更新時間:2023年01月28日 14:37:52   作者:自在的LEE  
這篇文章主要為大家介紹了gin正確多次讀取http?request?body內容實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

事件背景

到了年末,沒有太多事情,總算有時間深度優(yōu)化自己的 golang http webservice 框架,基于 gin 的。公司目前不少項目都是基于這個 webservice 框架開發(fā)的,所以我有責任保持這個框架的性能和穩(wěn)定性。

在自己仔細讀處理 middleware 中一個通用函數 GenerateRequestBody 時發(fā)現(xiàn),之間寫的代碼太過粗暴,雖然一直能能穩(wěn)定運行,但是總感覺哪里不對,同時也沒有利用 sync.pool,明顯這里可以優(yōu)化,對在高并發(fā)的時候有很大幫助。

越看以前自己實現(xiàn)的 GenerateRequestBody 內容,越覺得太過簡單,幾乎沒有什么思考,尤其在 gin middleware 中,這個函數在每一個 http 會話都會命中,同時設置這個函數作為 webservice 框架公開函數,也會被其他小伙伴調用,所以真的需要認真考慮。

前置知識

GenerateRequestBody 函數分析

廢話不多說,先上代碼,我們一起看看代碼的問題。

func GenerateRequestBody(c *gin.Context) string {
	body, err := c.GetRawData() // 讀取 request body 的內容
	if err != nil {
		body = utils.StringToBytes("failed to get request body")
	}
	c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 創(chuàng)建 io.ReadCloser 對象傳給 request body
	return utils.BytesToString(body) // 返回 request body 的值
}

咋一看好像沒有什么,我們不妨更深入代碼一探究竟。

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 會把 request body 中的所有的字節(jié)全部讀出,然后返回一個 []byte 數組。

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 實際就是一個包裝接口,把 Reader 接口封裝成一個帶有 Close 方法的對象,而且 Close 方法是一個直接返回的空函數。所以這里就有一個問題,如果你想調用 Close 關閉這個 io.ReadCloser 對象。我只能在邊上,呵呵呵,你懂我的意思。

回歸正題,這些代碼大家應該看起來很眼熟才對。沒錯,這是網絡上 gin 框架多次讀取 http request body 中內容的解決方案。 能想像很多小伙伴就是 copy + paste 了事,流量小或者沒有什么大規(guī)模應用場景下沒有什么問題。如果流量大了?應用規(guī)模很多?那怎辦?

gin 如何正確多次讀取 http request body 的內容呢? 正確的姿勢是什么呢?

追本溯源

gin 只不過是一個 router 框架,真正的 http 請求處理是 golang 中的 net/http 包來負責的。要找到 gin 如何正確多次讀取 http request body 內容的方法,就一定要往下追。

寫過 golang http client 的小伙伴都知道,需要手動執(zhí)行 resp.Body.Close() 這樣的方法釋放連接。要不然會因為底層 tcp 端口耗盡,導致無法創(chuàng)建連接。我們通過一個簡單例子看下:

package main
import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)
func main() {
	resp, _ := doGet("http://www.baidu.com")
	defer resp.Body.Close() //go的特殊語法,main函數執(zhí)行結束前會執(zhí)行 resp.Body.Close()
	fmt.Println(resp.StatusCode)          //有http的響應碼輸出
	if resp.StatusCode == http.StatusOK { //如果響應碼為200
		body, err := ioutil.ReadAll(resp.Body) //把響應的body讀出
		if err != nil {                        //如果有異常
			fmt.Println(err) //把異常打印
			log.Fatal(err)   //日志
		}
		fmt.Println(string(body)) //把響應的文本輸出到console
	}
}
/**
  以GET的方式請求
  **/
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
}

通過上面的代碼,我們能看到 defer resp.Body.Close() 的代碼,它就是要主動關閉連接。那么也有一個類似的問題,golang 中 net/http 包的 server 代碼是不是也要主動管理連接呢?

類似如下:

bodyBytes, _ := ioutil.ReadAll(req.Body)
req.Body.Close()  //  這里調用Close
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

但是官方的代碼注釋里卻寫不需要在處理函數里調用 Close:Request.Body:"The Server will close the request body. The ServeHTTP Handler does not need to."

感覺好奇怪,golang 中 net/http 包的 server 自己能關閉 request,那跟上面類似執(zhí)行 req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 替換了 req.Body 原有內容,那么 golang 中 net/http 包的 server 還能正確關閉以前的 req.Body 嘛?如果不能關閉,那么類似 GenerateRequestBody 函數這樣的執(zhí)行過程,必然在大并發(fā)下,必然導致內存泄露和大量 GC 回收,影響服務響應。

值得深入

帶著上面的問題,在網上尋找了很久,沒有能找到解決問題的方法,也沒有人把為什么說清楚。沒有思路,在各種不確定的假設上,提供一個公司級的底層 webservice 框架,必然被公司技術委員會的主席們挑戰(zhàn)。

說到這里,一不做二不休,直接干就是,往下肝。 順著服務的啟動流程找到了 golang 中 net/http 包的 server.go 文件,然后一個一個方法慢慢趴,直到找到了 func (c *conn) serve(ctx context.Context) {} 這個函數,總算看到了具體內容。

src/net/http/server.go

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	...
	for {
		w, err := c.readRequest(ctx) // 讀取 request 內容
		...
		}
	...
	// 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) // 處理請求
	inFlightResponse = nil
	w.cancelCtx()
	if c.hijacked() {
		return
	}
	w.finishRequest() // 關閉請求
	...
	}

看到這里,想要解決問題只要看兩個函數 finishRequestreadRequest 就可以了。

finishRequest 函數分析

func (w *response) finishRequest() {
	...
	// Close the body (regardless of w.closeAfterReply) so we can
	// re-use its bufio.Reader later safely.
	w.reqBody.Close() // 關閉 request body ???,在這里?
	...
}

是這里? 就在這里關閉了? 但是這里是 response 啊,不是 request。 繼續(xù)點開看看 response 結構體是什么?

// A response represents the server side of an HTTP response.
type response struct {
	...
	req              *Request // request for this response
	reqBody          io.ReadCloser
	...
}

這里有一個 req 是 Request 的指針,那么還有一個 reqBody 作為 io.ReadCloser 是為了干嘛? 不解!不解!不解!

readRequest 函數分析

// 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,
		...
	}
	...
}

看到這里,突然這個世界晴朗了,所有的事情好像都明白了。心細的小伙伴一定看出來眉目了,很有可能真是:一拍大腿的提高。

readRequest 讀取到 req 信息后,在創(chuàng)建 response 的對象時,同時將 req 賦值給了 response 中的 req 和 reqBody。 也就是說 req.Body 和 reqBody 指向了同一個對象。 換句話說,我改變了 req.Body 的指向,reqBody 還保留著最初的 io.ReadCloser 對象的引用。 不管我怎么改變 req.Body 的值,哪怕是指向了 nil,也不會影響 server 調用 finishRequest() 函數來關閉 io.ReadCloser 對象,因為 finishRequest 內部調用的是 reqBody。

得出結論

middleware 中的 req.Body 和 response 中的 reqBody 是兩個變量。初期,req.Body 和 reqBody 中存放了同一個地址。但是,當 req.body = io.NoCloser 時,只是改變了 req.Body 中的指針,而 reqBody 仍舊指向原始請求的 body,故不需要在 middleware 中執(zhí)行關閉。

在 golang 開發(fā)提交記錄中也找到了類似的說明,并解決了這個問題。所以說在 Go 1.6 之后已經不用擔心這個問題了。

提交信息:

net/http: don't panic after request if Handler sets Request.Body to nil。

大致的意思是,不用再擔心把 req.Body 設置 nil,其實也就是不用再擔心重置 req.Body 了,更加不用手動關閉 req.Body。

上手開發(fā)

搞清楚了 golang 中 net/http 包的 server 中對請求的 request body 處理流程,那么 gin 這邊也好開發(fā)了。 首先我們回到之前的 GenerateRequestBody 函數。

func GenerateRequestBody(c *gin.Context) string {
	body, err := c.GetRawData() // 讀取 request body 的內容
	if err != nil {
		body = utils.StringToBytes("failed to get request body")
	}
	c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 創(chuàng)建 io.ReadCloser 對象傳給 request body
	return utils.BytesToString(body) // 返回 request body 的值
}

雖然不需要每次關閉 c.Request.Body 了,但是我們要注意,沒調用一次都會調用 bytes.NewBuffer 和 ioutil.NopCloser 一次。ioutil.NopCloser 這個還好是一個包裝,之前我們看到了相關的代碼。但是 bytes.NewBuffer 是一個重量級的家伙,我第一反應是不是可以用 sync.pool 來緩存這個這部分的代碼?

實際當然是可以的,但是 GenerateRequestBody 是一個函數,c.Request.Body 新的指向在隨后的 gin handler 中也要用,明顯在 GenerateRequestBody 內部對 sync.pool 執(zhí)行 Get 和 Put 明顯不合適。

怎么解決呢?也很簡單,在 gin 的框架 http request 會話是跟 Context 對象綁定的,所以直接在 Context 操作,并將 sync.pool Get 對象放入 Context,然后在 Context 銷毀之前對 sync.pool 執(zhí)行 Put 歸還。

流程圖如下:

gin Middleware 代碼

func ginRequestBodyBuffer() gin.HandlerFunc {
	return func(c *gin.Context) {
		var b *RequestBodyBuff
		// 創(chuàng)建緩存對象
		b = bodyBufferPool.Get().(*RequestBodyBuff)
		b.bodyBuf.Reset()
		c.Set(ConstRequestBodyBufferKey, b)
		// 下一個請求
		c.Next()
		// 歸還緩存對象
		if o, ok := c.Get(ConstRequestBodyBufferKey); ok {
			b = o.(*RequestBodyBuff)
			b.bodyBuf.Reset()                     // bytes.Buffer 要 reset,但是 slice 就不能,這個做 io.CopyBuffer 用的
			c.Set(ConstRequestBodyBufferKey, nil) // 釋放指向 RequestBodyBuff 對象
			bodyBufferPool.Put(o)                 // 歸還對象
			c.Request.Body = nil                  // 釋放指向創(chuàng)建的 io.NopCloser 對象
		}
	}
}

新 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 的內容,此時 body 的 []byte 是全新的一個數據 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) // 把內容重新寫入 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)
}

測試代碼

對開發(fā)好的代碼執(zhí)行循環(huán)測試,用短鏈接測試。

while true;do curl -i http://127.0.0.1:8080/yy/; done

總結

我們通過上面的操作和使用,基本確認了 golang 中 net/http 包中對 request body 的處理流程。 通過簡單的開發(fā),我們實現(xiàn)了 gin 正確多次讀取 http request body 內容的方法,同時加入了 sync.pool 支持。減少了頻繁 bytes.NewBuffer 創(chuàng)建對資源的消耗,以及提高了資源的利用效率。

以上就是gin正確多次讀取http request body內容實現(xiàn)詳解的詳細內容,更多關于gin讀取http request body的資料請關注腳本之家其它相關文章!

相關文章

  • 詳解go-zero是如何做路由管理的

    詳解go-zero是如何做路由管理的

    go-zero 是一個微服務框架,包含了 web 和 rpc 兩大部分,而對于 web 框架來說,路由管理是必不可少的一部分,那么本文就來探討一下 go-zero 的路由管理是怎么做的吧
    2023-08-08
  • golang 實現(xiàn)Location跳轉方式

    golang 實現(xiàn)Location跳轉方式

    這篇文章主要介紹了golang 實現(xiàn)Location跳轉方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • 利用Go語言實現(xiàn)二叉搜索樹

    利用Go語言實現(xiàn)二叉搜索樹

    二叉樹是一種常見并且非常重要的數據結構,在很多項目中都能看到二叉樹的身影,當然它也有很多變種,本文要介紹的是二叉搜索樹的實現(xiàn),希望對大家有所幫助
    2023-07-07
  • go語言實現(xiàn)短信發(fā)送實例探究

    go語言實現(xiàn)短信發(fā)送實例探究

    這篇文章主要為大家介紹了go語言實現(xiàn)短信發(fā)送實例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2024-01-01
  • Go實現(xiàn)socks5服務器的方法

    Go實現(xiàn)socks5服務器的方法

    SOCKS5 是一個代理協(xié)議,它在使用TCP/IP協(xié)議通訊的前端機器和服務器機器之間扮演一個中介角色,使得內部網中的前端機器變得能夠訪問Internet網中的服務器,或者使通訊更加安全,這篇文章主要介紹了Go實現(xiàn)socks5服務器的方法,需要的朋友可以參考下
    2023-07-07
  • Go語言遞歸函數的具體實現(xiàn)

    Go語言遞歸函數的具體實現(xiàn)

    本文主要介紹了Go語言遞歸函數的具體實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-04-04
  • Go語言導出內容到Excel的方法

    Go語言導出內容到Excel的方法

    這篇文章主要介紹了Go語言導出內容到Excel的方法,涉及Go語言操作excel的技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-02-02
  • Golang 字符串與字節(jié)數組互轉的實現(xiàn)

    Golang 字符串與字節(jié)數組互轉的實現(xiàn)

    在Go語言中,我們經常在字符串和切片之間進行轉換,本文就詳細的介紹一下Golang 字符串與字節(jié)數組互轉的實現(xiàn),文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-02-02
  • Golang?單元測試和基準測試實例詳解

    Golang?單元測試和基準測試實例詳解

    這篇文章主要為大家介紹了Golang?單元測試和基準測試實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-08-08
  • GIN的路由以及傳參問題

    GIN的路由以及傳參問題

    本文主要介紹了GIN的路由以及傳參問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-06-06

最新評論