日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當(dāng)前位置:首頁 > 科技  > 軟件

Go的Net/Http有哪些值得關(guān)注的細(xì)節(jié)?

來源: 責(zé)編: 時(shí)間:2023-08-14 22:01:04 341觀看
導(dǎo)讀golang的net/http庫是我們平時(shí)寫代碼中,非常常用的標(biāo)準(zhǔn)庫。由于go語言擁有g(shù)oroutine,goroutine的上下文切換成本比普通線程低很多,net/http庫充分利用了這個(gè)優(yōu)勢(shì),因此,它的內(nèi)部實(shí)現(xiàn)跟其他語言會(huì)有一些區(qū)別。其中最大的區(qū)別

RFO28資訊網(wǎng)——每日最新資訊28at.com

golang的net/http庫是我們平時(shí)寫代碼中,非常常用的標(biāo)準(zhǔn)庫。由于go語言擁有g(shù)oroutine,goroutine的上下文切換成本比普通線程低很多,net/http庫充分利用了這個(gè)優(yōu)勢(shì),因此,它的內(nèi)部實(shí)現(xiàn)跟其他語言會(huì)有一些區(qū)別。RFO28資訊網(wǎng)——每日最新資訊28at.com

其中最大的區(qū)別在于,其他語言中,一般是多個(gè)網(wǎng)絡(luò)句柄共用一個(gè)或多個(gè)線程,以此來減少線程之間的切換成本。而golang則會(huì)為每個(gè)網(wǎng)絡(luò)句柄創(chuàng)建兩個(gè)goroutine,一個(gè)用于讀數(shù)據(jù),一個(gè)用于寫數(shù)據(jù)。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

讀寫協(xié)程RFO28資訊網(wǎng)——每日最新資訊28at.com

下圖是net/http源碼中創(chuàng)建這兩個(gè)goroutine的地方。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

源碼中創(chuàng)建兩個(gè)協(xié)程的地方RFO28資訊網(wǎng)——每日最新資訊28at.com

了解它的內(nèi)部實(shí)現(xiàn)原理,可以幫助我們寫出更高性能的代碼,以及避免協(xié)程泄露造成的內(nèi)存泄漏問題。RFO28資訊網(wǎng)——每日最新資訊28at.com

這篇文章是希望通過幾個(gè)例子讓大家對(duì)net/http的內(nèi)部實(shí)現(xiàn)有更直觀的理解。RFO28資訊網(wǎng)——每日最新資訊28at.com

連接與協(xié)程數(shù)量的關(guān)系

首先我們來看一個(gè)例子。RFO28資訊網(wǎng)——每日最新資訊28at.com

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,            Timeout:   3 * time.Second,        }        resp, _ := client.Do(req)        _, _ = ioutil.ReadAll(resp.Body)        _ = resp.Body.Close()    }    time.Sleep(time.Second * 5)    fmt.Printf("goroutine num is %d/n", runtime.NumGoroutine())}

上面的代碼做的事情很簡(jiǎn)單,執(zhí)行5次循環(huán)http請(qǐng)求,最終通過runtime.NumGoroutine()方法打印當(dāng)前的goroutine數(shù)量。RFO28資訊網(wǎng)——每日最新資訊28at.com

代碼里只有三個(gè)地方需要注意:RFO28資訊網(wǎng)——每日最新資訊28at.com

  • Transport設(shè)置了一個(gè)3s的空閑連接超時(shí)。
  •  for循環(huán)執(zhí)行了5次http請(qǐng)求。
  • 程序退出前執(zhí)行了5s sleep。

答案輸出1。也就是說當(dāng)程序退出的時(shí)候,當(dāng)前的goroutine數(shù)量為1,毫無疑問它指的是正在運(yùn)行main方法的goroutine,后面我們都叫它main goroutine。RFO28資訊網(wǎng)——每日最新資訊28at.com

再來看個(gè)例子。RFO28資訊網(wǎng)——每日最新資訊28at.com

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,            Timeout:   3 * time.Second,        }        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())}

在原來的基礎(chǔ)上,我們程序退出前的睡眠時(shí)間,從5s改成1s,此時(shí)輸出3。也就是說除了main方法所在的goroutine,還多了兩個(gè)goroutine,我們大概也能猜到,這就是文章開頭提到的讀goroutine和寫goroutine。也就是說程序在退出時(shí),還有一個(gè)網(wǎng)絡(luò)連接沒有斷開。RFO28資訊網(wǎng)——每日最新資訊28at.com

這是一個(gè)TCP長(zhǎng)連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

HTTP1.1底層依賴TCPRFO28資訊網(wǎng)——每日最新資訊28at.com

網(wǎng)絡(luò)五層模型中,HTTP處于應(yīng)用層,它的底層依賴了傳輸層的TCP協(xié)議。RFO28資訊網(wǎng)——每日最新資訊28at.com

當(dāng)我們發(fā)起http請(qǐng)求時(shí),如果每次都要建立新的TCP協(xié)議,那就需要每次都經(jīng)歷三次握手,這會(huì)影響性能,因此更好的方式就是在http請(qǐng)求結(jié)束后,不立馬斷開TCP連接,將它放到一個(gè)空閑連接池中,后續(xù)有新的http請(qǐng)求時(shí)就復(fù)用該連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

像這種長(zhǎng)時(shí)間存活,被多個(gè)http請(qǐng)求復(fù)用的TCP連接,就是所謂的長(zhǎng)連接。反過來,如果每次HTTP請(qǐng)求結(jié)束就將TCP連接進(jìn)行四次揮手?jǐn)嚅_,下次有需要執(zhí)行HTTP調(diào)用時(shí)就再建立,這樣的TCP連接就是所謂的短連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

HTTP1.1之后默認(rèn)使用長(zhǎng)連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

連接池復(fù)用連接RFO28資訊網(wǎng)——每日最新資訊28at.com

那為什么這跟5s和1s有關(guān)系?RFO28資訊網(wǎng)——每日最新資訊28at.com

這是因?yàn)殚L(zhǎng)連接在空閑連接池也不能一直存放著,如果一直沒被使用放著也是浪費(fèi)資源,因此會(huì)有個(gè)空閑回收時(shí)間,也就是上面代碼中的IdleConnTimeout,我們?cè)O(shè)置的是3s,當(dāng)代碼在結(jié)束前sleep了5s后,長(zhǎng)連接就已經(jīng)被釋放了,因此輸出結(jié)果是只剩一個(gè)main goroutine。當(dāng)sleep 1s時(shí),長(zhǎng)連接還在空閑連接池里,因此程序結(jié)束時(shí),就還剩3個(gè)goroutine(main goroutine+網(wǎng)絡(luò)讀goroutine+網(wǎng)絡(luò)寫goroutine)。RFO28資訊網(wǎng)——每日最新資訊28at.com

我們可以改下代碼下驗(yàn)證這個(gè)說法。我們知道,HTTP可以通過connection的header頭來控制這次的HTTP請(qǐng)求是用的長(zhǎng)連接還是短連接。connection:keep-alive 表示http請(qǐng)求結(jié)束后,tcp連接保持存活,也就是長(zhǎng)連接, connection:close則是短連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

req.Header.Add("connection", "close")

就像下面這樣。RFO28資訊網(wǎng)——每日最新資訊28at.com

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")        req.Header.Add("connection", "close")        client := &http.Client{            Transport: tr,            Timeout:   3 * time.Second,        }        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())}

此時(shí),會(huì)發(fā)現(xiàn),程序重新輸出1。完全符合我們預(yù)期。RFO28資訊網(wǎng)——每日最新資訊28at.com

resp.body是否讀取對(duì)連接復(fù)用的影響

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())}

注意這里沒有執(zhí)行 ioutil.ReadAll(resp.Body)。也就是說http請(qǐng)求響應(yīng)的結(jié)果并沒有被讀取的情況下,net/http庫會(huì)怎么處理。RFO28資訊網(wǎng)——每日最新資訊28at.com

上面的代碼最終輸出3,分別是main goroutine,read goroutine 以及write goroutine。也就是說長(zhǎng)連接沒有斷開,那長(zhǎng)連接是會(huì)在下一次http請(qǐng)求中被復(fù)用嗎?先說答案,不會(huì)復(fù)用。RFO28資訊網(wǎng)——每日最新資訊28at.com

我們可以看代碼。resp.Body.Close() 會(huì)執(zhí)行到 func (es * bodyEOFSignal) Close() error 中,并執(zhí)行到es.earlyCloseFn()中。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

earlyCloseFn的邏輯也非常簡(jiǎn)單,就是將一個(gè)false傳入到waitForBodyRead的channel中。那寫入通道后的數(shù)據(jù)會(huì)在另外一個(gè)地方被讀取,我們來看下讀取的地方。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

bodyEOF為false, 也就不需要執(zhí)行 tryPutIdleConn()方法。RFO28資訊網(wǎng)——每日最新資訊28at.com

tryPutIdleConn會(huì)將連接放到長(zhǎng)連接池中備用)。RFO28資訊網(wǎng)——每日最新資訊28at.com

最終就是alive=bodyEOF ,也就是false,字面意思就是該連接不再存活。因此該長(zhǎng)連接并不會(huì)復(fù)用,而是會(huì)釋放。RFO28資訊網(wǎng)——每日最新資訊28at.com

那為什么output輸出為3?這是因?yàn)殚L(zhǎng)連接釋放需要時(shí)間。RFO28資訊網(wǎng)——每日最新資訊28at.com

我們可以在結(jié)束前加一個(gè)休眠,比如再執(zhí)行休眠1毫秒。RFO28資訊網(wǎng)——每日最新資訊28at.com

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())}

此時(shí)就會(huì)輸出1。說明協(xié)程是退出中的,只是沒來得及完全退出,休眠1ms后徹底退出了。RFO28資訊網(wǎng)——每日最新資訊28at.com

如果我們,將在代碼中重新加入 ioutil.ReadAll(resp.Body),就像下面這樣。RFO28資訊網(wǎng)——每日最新資訊28at.com

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())}

此時(shí),output還是輸出3,但這個(gè)3跟上面的3不太一樣,休眠5s后還是輸出3。這是因?yàn)殚L(zhǎng)連接被推入到連接池了,連接會(huì)重新復(fù)用。RFO28資訊網(wǎng)——每日最新資訊28at.com

下面是源碼的解釋。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com


RFO28資訊網(wǎng)——每日最新資訊28at.com

body.close()不執(zhí)行會(huì)怎么樣

網(wǎng)上都說不執(zhí)行body.close()會(huì)協(xié)程泄漏(導(dǎo)致內(nèi)存泄露),真的會(huì)出現(xiàn)協(xié)程泄漏嗎,如果泄漏,會(huì)泄漏多少?RFO28資訊網(wǎng)——每日最新資訊28at.com

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())}

我們可以運(yùn)行這段代碼,代碼中將resp.body.close()注釋掉,結(jié)果輸出3。debug源碼,會(huì)發(fā)現(xiàn)連接其實(shí)復(fù)用了。代碼執(zhí)行到tryPutIdleConn函數(shù)中,會(huì)將連接歸還到空閑連接池中。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

休眠5s,結(jié)果輸出1,這說明達(dá)到idleConnTimeout,空閑連接斷開??雌饋硪磺姓?。RFO28資訊網(wǎng)——每日最新資訊28at.com

將resp.Body.Close()那一行代碼重新加回來,也就是下面這樣,會(huì)發(fā)現(xiàn)代碼結(jié)果依然輸出3。我們是否刪除這行代碼,對(duì)結(jié)果沒有任何影響。RFO28資訊網(wǎng)——每日最新資訊28at.com

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())}

既然執(zhí)不執(zhí)行body.close()都沒啥區(qū)別,那body.close()的作用是什么呢?RFO28資訊網(wǎng)——每日最新資訊28at.com

它是為了標(biāo)記當(dāng)前連接請(qǐng)求中,response.body是否使用完畢,如果不執(zhí)行body.close(),則resp.Body中的數(shù)據(jù)是可以不斷重復(fù)讀且不報(bào)錯(cuò)的(但不一定能讀到數(shù)據(jù)),執(zhí)行了body.close(),再次去讀取resp.Body則會(huì)報(bào)錯(cuò),如果resp.body數(shù)據(jù)讀一半,處理代碼邏輯就報(bào)錯(cuò)了,此時(shí)你不希望其他地方繼續(xù)去讀,那就需要使用body.close()去關(guān)閉它。這更像是一種規(guī)范約束,它可以更好的保證數(shù)據(jù)正確。RFO28資訊網(wǎng)——每日最新資訊28at.com

也就是說不執(zhí)行body.close(),并不一定會(huì)內(nèi)存泄露。那么什么情況下會(huì)協(xié)程泄露呢?RFO28資訊網(wǎng)——每日最新資訊28at.com

直接說答案,既不執(zhí)行 ioutil.ReadAll(resp.Body) 也不執(zhí)行resp.Body.Close(),并且不設(shè)置http.Client內(nèi)timeout的時(shí)候,就會(huì)導(dǎo)致協(xié)程泄露。RFO28資訊網(wǎng)——每日最新資訊28at.com

比如下面這樣。RFO28資訊網(wǎng)——每日最新資訊28at.com

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())}

最終結(jié)果會(huì)輸出11,也就是1個(gè)main goroutine + (1個(gè)read goroutine + 1個(gè)read goroutine)* 5次http請(qǐng)求。RFO28資訊網(wǎng)——每日最新資訊28at.com

前面提到,不執(zhí)行ioutil.ReadAll(resp.Body),網(wǎng)絡(luò)連接無法歸還到連接池。不執(zhí)行resp.Body.Close(),網(wǎng)絡(luò)連接就無法為標(biāo)記為關(guān)閉,也就無法正常斷開。因此能導(dǎo)致協(xié)程泄露,非常好理解。RFO28資訊網(wǎng)——每日最新資訊28at.com

但http.Client內(nèi)timeout有什么關(guān)系?這是因?yàn)閠imeout是指,從發(fā)起請(qǐng)求到從resp.body中讀完響應(yīng)數(shù)據(jù)的總時(shí)間,如果超過了,網(wǎng)絡(luò)庫會(huì)自動(dòng)斷開網(wǎng)絡(luò)連接,并釋放read+write goroutine。因此如果設(shè)置了timeout,則不會(huì)出現(xiàn)協(xié)程泄露的問題。RFO28資訊網(wǎng)——每日最新資訊28at.com

另外值得一提的是,我看到有不少代碼都是直接用下面的方式去做網(wǎng)絡(luò)請(qǐng)求的。RFO28資訊網(wǎng)——每日最新資訊28at.com

resp, _ := http.Get("https://www.baidu.com")

這種方式用的是DefaultClient,是沒有設(shè)置超時(shí)的,生產(chǎn)環(huán)境中使用不當(dāng),很容易出現(xiàn)問題。RFO28資訊網(wǎng)——每日最新資訊28at.com

func Get(url string) (resp *Response, err error) {    return DefaultClient.Get(url)}var DefaultClient = &Client{}

連接池的結(jié)構(gòu)

我們了解到連接池可以復(fù)用網(wǎng)絡(luò)連接,接下來我們通過一個(gè)例子來看看網(wǎng)絡(luò)連接池的結(jié)構(gòu)。RFO28資訊網(wǎng)——每日最新資訊28at.com

func main() {    tr := &http.Transport{        MaxIdleConns:    100,        IdleConnTimeout: 3 * time.Second,    }    n := 5    for i := 0; i < n; i++ {        req, _ := http.NewRequest("POST", "http://www.baidu.com", nil)        req.Header.Add("content-type", "application/json")        client := &http.Client{            Transport: tr,            Timeout:   3 * time.Second,        }        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())}

注意這里請(qǐng)求的不是https,而是http。最終結(jié)果輸出5,為什么?RFO28資訊網(wǎng)——每日最新資訊28at.com

這是因?yàn)椋琱ttp://www.baidu.com會(huì)返回307,重定向到https://www.baidu.com。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

http重定向?yàn)閔ttpsRFO28資訊網(wǎng)——每日最新資訊28at.com

在網(wǎng)絡(luò)中,我們可以通過一個(gè)五元組來唯一確定一個(gè)TCP連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

五元組RFO28資訊網(wǎng)——每日最新資訊28at.com

它們分別是源ip,源端口,協(xié)議,目的ip,目的端口。只有當(dāng)多次請(qǐng)求的五元組一樣的情況下,才有可能復(fù)用連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

放在我們這個(gè)場(chǎng)景下,源ip、源端口、協(xié)議都是確定的,也就是兩次http請(qǐng)求的目的ip或目的端口有區(qū)別的時(shí)候,就需要使用不同的TCP長(zhǎng)連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

而http用的是80端口,https用的是443端口。于是連接池就為不同的網(wǎng)絡(luò)目的地建立不同的長(zhǎng)連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

因此最終結(jié)果5個(gè)goroutine,其實(shí)2個(gè)goroutine來自http,2個(gè)goroutine來自https,1個(gè)main goroutine。RFO28資訊網(wǎng)——每日最新資訊28at.com

我們來看下源碼的具體實(shí)現(xiàn)。net/http底層通過一個(gè)叫idleConn的map去存空閑連接,也就是空閑連接池。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

idleConn這個(gè)map的key是協(xié)議和地址,其實(shí)本質(zhì)上就是ip和端口。map的value是長(zhǎng)連接的數(shù)組([]*persistConn),說明net/http支持為同一個(gè)地址建立多個(gè)TCP連接,這樣可以提升傳輸?shù)耐掏隆?span style="display:none">RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

連接池的結(jié)構(gòu)和邏輯RFO28資訊網(wǎng)——每日最新資訊28at.com

Transport是什么?

Transport本質(zhì)上是一個(gè)用來控制http調(diào)用行為的一個(gè)組件,里面包含超時(shí)控制,連接池等,其中最重要的是連接池相關(guān)的配置。RFO28資訊網(wǎng)——每日最新資訊28at.com

我們通過下面的例子感受下。RFO28資訊網(wǎng)——每日最新資訊28at.com

func main() {    n := 5    for i := 0; i < n; i++ {        httpClient := &http.Client{}        resp, _ := httpClient.Get("https://www.baidu.com")        _, _ = ioutil.ReadAll(resp.Body)        _ = resp.Body.Close()    }    time.Sleep(time.Second * 1)    fmt.Printf("goroutine num is %d/n", runtime.NumGoroutine())}
func main() {    n := 5    for i := 0; i < n; i++ {        httpClient := &http.Client{            Transport:  &http.Transport{},        }        resp, _ := httpClient.Get("https://www.baidu.com")        _, _ = ioutil.ReadAll(resp.Body)        _ = resp.Body.Close()    }    time.Sleep(time.Second * 1)    fmt.Printf("goroutine num is %d/n", runtime.NumGoroutine())}

上面的代碼第一個(gè)例子的代碼會(huì)輸出3。分別是main goroutine + read goroutine + write goroutine,也就是有一個(gè)被不斷復(fù)用的TCP連接。RFO28資訊網(wǎng)——每日最新資訊28at.com

在第二例子中,當(dāng)我們?cè)诿看蝐lient中都創(chuàng)建一個(gè)新的http.Transport,此時(shí)就會(huì)輸出11。RFO28資訊網(wǎng)——每日最新資訊28at.com

說明TCP連接沒有復(fù)用,每次請(qǐng)求都會(huì)產(chǎn)生新的連接。這是因?yàn)槊總€(gè)http.Transport內(nèi)都會(huì)維護(hù)一個(gè)自己的空閑連接池,如果每個(gè)client都創(chuàng)建一個(gè)新的http.Transport,就會(huì)導(dǎo)致底層的TCP連接無法復(fù)用。如果網(wǎng)絡(luò)請(qǐng)求過大,上面這種情況會(huì)導(dǎo)致協(xié)程數(shù)量變得非常多,導(dǎo)致服務(wù)不穩(wěn)定。RFO28資訊網(wǎng)——每日最新資訊28at.com

因此,最佳實(shí)踐是所有client都共用一個(gè)transport。RFO28資訊網(wǎng)——每日最新資訊28at.com

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,            Timeout:   3 * time.Second,        }        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())}

如果創(chuàng)建客戶端的時(shí)候不指定http.Client,會(huì)默認(rèn)所有http.Client都共用同一個(gè)DefaultTransport。這一點(diǎn)可以從源碼里看出。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

默認(rèn)使用DefaultTransportRFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

DefaultTransportRFO28資訊網(wǎng)——每日最新資訊28at.com

因此當(dāng)?shù)诙未a中,每次都重新創(chuàng)建一個(gè)Transport的時(shí)候,每個(gè)Transport內(nèi)都會(huì)各自維護(hù)一個(gè)空閑連接池。因此每次建立長(zhǎng)連接后都會(huì)多兩個(gè)協(xié)程(讀+寫),對(duì)應(yīng)1個(gè)main goroutine+(read goroutine + write goroutine)* 5 =11。RFO28資訊網(wǎng)——每日最新資訊28at.com

別設(shè)置 Transport.Dail里的SetDeadline

http.Transport.Dial的配置里有個(gè)SetDeadline,它表示連接建立后發(fā)送接收數(shù)據(jù)的超時(shí)時(shí)間。聽起來跟client.Timeout很像。RFO28資訊網(wǎng)——每日最新資訊28at.com

那么他們有什么區(qū)別呢?我們通過一個(gè)例子去看下。RFO28資訊網(wǎng)——每日最新資訊28at.com

package mainimport (    "bytes"    "encoding/json"    "fmt"    "io/ioutil"    "net"    "net/http"    "time")var tr *http.Transportfunc init() {    tr = &http.Transport{        MaxIdleConns: 100,        Dial: func(netw, addr string) (net.Conn, error) {            conn, err := net.DialTimeout(netw, addr, time.Second*2) //設(shè)置建立連接超時(shí)            if err != nil {                return nil, err            }            err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //設(shè)置發(fā)送接受數(shù)據(jù)超時(shí)            if err != nil {                return nil, err            }            return conn, nil        },    }}func main() {    for {        _, err := Get("http://www.baidu.com/")        if err != nil {            fmt.Println(err)            break        }    }}func Get(url string) ([]byte, error) {    m := make(map[string]interface{})    data, err := json.Marshal(m)    if err != nil {        return nil, err    }    body := bytes.NewReader(data)    req, _ := http.NewRequest("Get", url, body)    req.Header.Add("content-type", "application/json")    client := &http.Client{        Transport: tr,    }    res, err := client.Do(req)    if res != nil {        defer res.Body.Close()    }    if err != nil {        return nil, err    }    resBody, err := ioutil.ReadAll(res.Body)    if err != nil {        return nil, err    }    return resBody, nil}

上面這段代碼,我們?cè)O(shè)置了SetDeadline為3s,當(dāng)你執(zhí)行一段時(shí)間,會(huì)發(fā)現(xiàn)請(qǐng)求baidu會(huì)超時(shí),但其實(shí)baidu的接口很快,不可能超過3s。RFO28資訊網(wǎng)——每日最新資訊28at.com

在生產(chǎn)環(huán)境中,假如是你的服務(wù)調(diào)用下游服務(wù),你看到的現(xiàn)象就是,你的服務(wù)顯示3s超時(shí)了,但下游服務(wù)可能只花了200ms就已經(jīng)響應(yīng)你的請(qǐng)求了,并且這是隨機(jī)發(fā)生的問題。遇到這種情況,我們一般會(huì)認(rèn)為是“網(wǎng)絡(luò)波動(dòng)”。RFO28資訊網(wǎng)——每日最新資訊28at.com

但如果我們?nèi)?duì)網(wǎng)絡(luò)抓包,就很容易發(fā)現(xiàn)問題的原因 。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

抓包結(jié)果RFO28資訊網(wǎng)——每日最新資訊28at.com

可以看到,在tcp三次握手之后,就會(huì)開始多次網(wǎng)絡(luò)請(qǐng)求。直到3s的時(shí)候,就會(huì)觸發(fā)RST包,斷開連接。也就是說,我們?cè)O(shè)置的SetDeadline,并不是指單次http請(qǐng)求的超時(shí)是3s,而是指整個(gè)tcp連接的存活時(shí)間是3s,計(jì)算長(zhǎng)連接被連接池回收,這個(gè)時(shí)間也不會(huì)重置。RFO28資訊網(wǎng)——每日最新資訊28at.com

RFO28資訊網(wǎng)——每日最新資訊28at.com

SetDeadline的解釋RFO28資訊網(wǎng)——每日最新資訊28at.com

我實(shí)在想不到什么樣的場(chǎng)景會(huì)需要這個(gè)功能,因此我的建議是,不要使用它。RFO28資訊網(wǎng)——每日最新資訊28at.com

下面是修改后的代碼。這個(gè)問題其實(shí)在我另外一篇文章有過詳細(xì)的解釋,如果你對(duì)源碼解析感興趣的話,可以去看看。RFO28資訊網(wǎng)——每日最新資訊28at.com

package mainimport (    "bytes"    "encoding/json"    "fmt"    "io/ioutil"    "net/http"    "time")var tr *http.Transportfunc init() {    tr = &http.Transport{        MaxIdleConns: 100,        // 下面的代碼被干掉了        //Dial: func(netw, addr string) (net.Conn, error) {        // conn, err := net.DialTimeout(netw, addr, time.Second*2) //設(shè)置建立連接超時(shí)        // if err != nil {        //  return nil, err        // }        // err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //設(shè)置發(fā)送接受數(shù)據(jù)超時(shí)        // if err != nil {        //  return nil, err        // }        // return conn, nil        //},    }}func Get(url string) ([]byte, error) {    m := make(map[string]interface{})    data, err := json.Marshal(m)    if err != nil {        return nil, err    }    body := bytes.NewReader(data)    req, _ := http.NewRequest("Get", url, body)    req.Header.Add("content-type", "application/json")    client := &http.Client{        Transport: tr,        Timeout: 3*time.Second,  // 超時(shí)加在這里,是每次調(diào)用的超時(shí)    }    res, err := client.Do(req)     if res != nil {        defer res.Body.Close()    }    if err != nil {        return nil, err    }    resBody, err := ioutil.ReadAll(res.Body)    if err != nil {        return nil, err    }    return resBody, nil}func main() {    for {        _, err := Get("http://www.baidu.com/")        if err != nil {            fmt.Println(err)            break        }    }}

總結(jié)

golang的net/http部分有不少細(xì)節(jié)點(diǎn),直接上源碼分析怕勸退不少人,所以希望以幾個(gè)例子作為引子展開話題然后深入了解它的內(nèi)部實(shí)現(xiàn)??傮w內(nèi)容比較碎片化,但這個(gè)庫的重點(diǎn)知識(shí)點(diǎn)基本都在這里面了。希望對(duì)大家后續(xù)排查問題有幫助。RFO28資訊網(wǎng)——每日最新資訊28at.com

本文鏈接:http://www.www897cc.com/showinfo-26-5715-0.htmlGo的Net/Http有哪些值得關(guān)注的細(xì)節(jié)?

聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com

上一篇: 警惕 C++ 中的隱式類型轉(zhuǎn)換

下一篇: 計(jì)算機(jī)底層原理~CPU緩存一致性

標(biāo)簽:
  • 熱門焦點(diǎn)
Top 主站蜘蛛池模板: 阳信县| 同仁县| 浦东新区| 凤阳县| 彭阳县| 永年县| 阜南县| 英吉沙县| 乡城县| 建阳市| 盐津县| 石首市| 铜梁县| 宜黄县| 泸西县| 巴马| 漠河县| 泸定县| 都江堰市| 广安市| 永寿县| 枣强县| 九台市| 镇雄县| 晋宁县| 青铜峡市| 宜州市| 武定县| 句容市| 全南县| 宁城县| 清徐县| 渑池县| 莱州市| 通辽市| 古田县| 葫芦岛市| 江北区| 吴忠市| 封丘县| 邳州市|