我是 LEE,老李,一个在 IT 职业摸爬滚打 16 年的技能老兵。

事件背景

到了年末,没有太多工作,总算有时间深度优化自己的 golang http webservice 结构,根据 gin 的。公司现在不少项目都是根据这个 webservice 结构开发的,所以我有职责坚持这个结构的功能和安稳性。

在自己仔细读处理 middleware 中一个通用函数 GenerateRequestBody 时发现,之间写的代码过分粗犷,尽管一向能能安稳运行,可是总感觉哪里不对,一同也没有运用 sync.pool,显着这儿能够优化,对在高并发的时分有很大协助。

越看以前自己完成的 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)) // 创立 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 中的一切的字节全部读出,然后回来一个 []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 完事,流量小或者没有什么大规划运用场景下没有什么问题。假如流量大了?运用规划许多?那怎办?

gin 如何正确屡次读取 http request body 的内容呢? 正确的姿态是什么呢?

追本溯源

gin 只不过是一个 router 结构,真正的 http 恳求处理是 golang 中的 net/http 包来担任的。要找到 gin 如何正确屡次读取 http request body 内容的办法,就必定要往下追。

写过 golang http client 的小伙伴都知道,需求手动履行 resp.Body.Close() 这样的办法开释衔接。要不然会由于底层 tcp 端口耗尽,导致无法创立衔接。咱们经过一个简略例子看下:

package main
import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)
func main() {
	resp, _ := doGet("http://www.baidu.com")
	defer resp.Body.Close() //go的特别语法,main函数履行结束前会履行 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,那跟上面相似履行 req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 替换了 req.Body 原有内容,那么 golang 中 net/http 包的 server 还能正确封闭以前的 req.Body 嘛?假如不能封闭,那么相似 GenerateRequestBody 函数这样的履行进程,必然在大并发下,必然导致内存走漏和大量 GC 收回,影响服务呼应。

值得深化

带着上面的问题,在网上寻找了很久,没有能找到处理问题的办法,也没有人把为什么说清楚。没有思路,在各种不确定的假设上,供给一个公司级的底层 webservice 结构,必然被公司技能委员会的主席们应战。

说到这儿,一不做二不休,直接干便是,往下肝。 顺着服务的发动流程找到了 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。 持续点开看看 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 信息后,在创立 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 中履行封闭。

在 golang 开发提交记录中也找到了相似的阐明,并处理了这个问题。所以说在 Go 1.6 之后已经不必担心这个问题了。

提交信息

提交信息:

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

大致的意思是,不必再担心把 req.Body 设置 nil,其实也便是不必再担心重置 req.Body 了,更加不必手动封闭 req.Body。

上手开发

搞清楚了 golang 中 net/http 包的 server 中对恳求的 request body 处理流程,那么 gin 这边也好开发了。 首要咱们回到之前的 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)) // 创立 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 履行 Get 和 Put 显着不合适。

怎样处理呢?也很简略,在 gin 的结构 http request 会话是跟 Context 目标绑定的,所以直接在 Context 操作,并将 sync.pool Get 目标放入 Context,然后在 Context 销毁之前对 sync.pool 履行 Put 偿还。

流程图如下:

http 测验

gin Middleware 代码

func ginRequestBodyBuffer() gin.HandlerFunc {
	return func(c *gin.Context) {
		var b *RequestBodyBuff
		// 创立缓存目标
		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                  // 开释指向创立的 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)
}

测验代码

对开发好的代码履行循环测验,用短链接测验。

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

http 测验

总结

咱们经过上面的操作和运用,基本确认了 golang 中 net/http 包中对 request body 的处理流程。 经过简略的开发,咱们完成了 gin 正确屡次读取 http request body 内容的办法,一同加入了 sync.pool 支持。减少了频频 bytes.NewBuffer 创立对资源的消耗,以及进步了资源的运用效率。