resp.body是否读取对连接复用的影响
func main() { n := 5 for i := 0; i < n; i++ { resp, _ := http.Get("https://www.baidu.com") _ = resp.Body.Close() } fmt.Printf("goroutine num is %d\n", runtime.NumGoroutine()) }
注意这里没有执行 ioutil.ReadAll(resp.Body)
。也就是说http请求响应的结果并没有被读取的情况下,net/http库会怎么处理。
上面的代码最终输出3
,分别是main goroutine,read goroutine 以及write goroutine。也就是说长连接没有断开,那长连接是会在下一次http请求中被复用吗?先说答案,不会复用。
我们可以看代码。resp.Body.Close()
会执行到 func (es * bodyEOFSignal) Close() error
中,并执行到es.earlyCloseFn()
中。
earlyCloseFn
的逻辑也非常简单,就是将一个false传入到waitForBodyRead
的channel中。那写入通道后的数据会在另外一个地方被读取,我们来看下读取的地方。
bodyEOF
为false, 也就不需要执行 tryPutIdleConn()
方法。
tryPutIdleConn会将连接放到长连接池中备用)。
最终就是alive=bodyEOF
,也就是false
,字面意思就是该连接不再存活。因此该长连接并不会复用,而是会释放。
那为什么output输出为3
?这是因为长连接释放需要时间。
我们可以在结束前加一个休眠,比如再执行休眠1毫秒
。
func main() { n := 5 for i := 0; i < n; i++ { resp, _ := http.Get("https://www.baidu.com") _ = resp.Body.Close() } time.Sleep(time.Millisecond * 1) fmt.Printf("goroutine num is %d\n", runtime.NumGoroutine()) }
此时就会输出1
。说明协程是退出中的,只是没来得及完全退出,休眠1ms后彻底退出了。
如果我们,将在代码中重新加入 ioutil.ReadAll(resp.Body)
,就像下面这样。
func main() { n := 5 for i := 0; i < n; i++ { resp, _ := http.Get("https://www.baidu.com") _, _ = ioutil.ReadAll(resp.Body) _ = resp.Body.Close() } fmt.Printf("goroutine num is %d\n", runtime.NumGoroutine()) }
此时,output还是输出3
,但这个3跟上面的3不太一样,休眠5s后还是输出3。这是因为长连接被推入到连接池了,连接会重新复用。
下面是源码的解释。
body.close()不执行会怎么样
网上都说不执行body.close()
会协程泄漏(导致内存泄露),真的会出现协程泄漏吗,如果泄漏,会泄漏多少?
func main() { tr := &http.Transport{ MaxIdleConns: 100, IdleConnTimeout: 3 * time.Second, } n := 5 for i := 0; i < n; i++ { req, _ := http.NewRequest("POST", "https://www.baidu.com", nil) req.Header.Add("content-type", "application/json") client := &http.Client{ Transport: tr, } resp, _ := client.Do(req) _, _ = ioutil.ReadAll(resp.Body) //_ = resp.Body.Close() } time.Sleep(time.Second * 1) fmt.Printf("goroutine num is %d\n", runtime.NumGoroutine()) }
我们可以运行这段代码,代码中将resp.body.close()
注释掉,结果输出3
。debug源码,会发现连接其实复用了。代码执行到tryPutIdleConn
函数中,会将连接归还到空闲连接池中。
休眠5s,结果输出1
,这说明达到idleConnTimeout
,空闲连接断开。看起来一切正常。
将resp.Body.Close()
那一行代码重新加回来,也就是下面这样,会发现代码结果依然输出3
。我们是否删除这行代码,对结果没有任何影响。
func main() { tr := &http.Transport{ MaxIdleConns: 100, IdleConnTimeout: 3 * time.Second, } n := 5 for i := 0; i < n; i++ { req, _ := http.NewRequest("POST", "https://www.baidu.com", nil) req.Header.Add("content-type", "application/json") client := &http.Client{ Transport: tr, } resp, _ := client.Do(req) _, _ = ioutil.ReadAll(resp.Body) _ = resp.Body.Close() } time.Sleep(time.Second * 1) fmt.Printf("goroutine num is %d\n", runtime.NumGoroutine()) }
既然执不执行body.close()都没啥区别,那body.close()的作用是什么呢?
它是为了标记当前连接请求中,response.body
是否使用完毕,如果不执行body.close()
,则resp.Body
中的数据是可以不断重复读且不报错的(但不一定能读到数据),执行了body.close()
,再次去读取resp.Body则会报错,如果resp.body数据读一半,处理代码逻辑就报错了,此时你不希望其他地方继续去读,那就需要使用body.close()去关闭它。这更像是一种规范约束,它可以更好的保证数据正确。
也就是说不执行body.close(),并不一定会内存泄露。那么什么情况下会协程泄露呢?
直接说答案,既不执行 ioutil.ReadAll(resp.Body)
也不执行resp.Body.Close()
,并且不设置http.Client
内timeout
的时候,就会导致协程泄露。
比如下面这样。
func main() { tr := &http.Transport{ MaxIdleConns: 100, IdleConnTimeout: 3 * time.Second, } n := 5 for i := 0; i < n; i++ { req, _ := http.NewRequest("POST", "https://www.baidu.com", nil) req.Header.Add("content-type", "application/json") client := &http.Client{ Transport: tr, } resp, _ := client.Do(req) _ = resp } time.Sleep(time.Second * 5) fmt.Printf("goroutine num is %d\n", runtime.NumGoroutine()) }
最终结果会输出11
,也就是1个main goroutine + (1个read goroutine + 1个read goroutine)* 5次http请求。
前面提到,不执行ioutil.ReadAll(resp.Body),网络连接无法归还到连接池。不执行resp.Body.Close(),网络连接就无法为标记为关闭,也就无法正常断开。因此能导致协程泄露,非常好理解。
但http.Client内timeout有什么关系?这是因为timeout是指,从发起请求到从resp.body中读完响应数据的总时间,如果超过了,网络库会自动断开网络连接,并释放read+write goroutine。因此如果设置了timeout,则不会出现协程泄露的问题。
另外值得一提的是,我看到有不少代码都是直接用下面的方式去做网络请求的。
resp, _ := http.Get("https://www.baidu.com")
这种方式用的是DefaultClient
,是没有设置超时的,生产环境中使用不当,很容易出现问题。
func Get(url string) (resp *Response, err error) { return DefaultClient.Get(url) } var DefaultClient = &Client{}