Go?net?http超時應(yīng)用場景全面詳解
一、前言
在Go中編寫HTTP服務(wù)器或客戶端時,超時是最容易出錯、最微妙的事情之一:有很多選擇,錯誤在很長一段時間內(nèi)都不會產(chǎn)生任何后果,直到網(wǎng)絡(luò)出現(xiàn)故障,進(jìn)程掛起。
二、超時時間
2.1 SetDeadline
首先,您需要了解Go為實現(xiàn)超時而公開的網(wǎng)絡(luò)原語:Deadlines。
由net.Conn使用Set[Read|Write]Deadline(time.time)方法公開,Deadlines是一個絕對時間,當(dāng)達(dá)到該時間時,所有I/O操作都會失敗并出現(xiàn)超時錯誤。
Deadlines不是超時。一旦設(shè)置,它們將永遠(yuǎn)有效(或直到下一次調(diào)用SetDeadline),無論在此期間是否以及如何使用連接。因此,要使用SetDeadline構(gòu)建超時,您必須在每次讀/寫操作之前調(diào)用它。
您可能不想自己調(diào)用SetDeadline,而是讓net/http使用其更高級別的超時為您調(diào)用它。但是,請記住所有超時都是根據(jù)Deadlines實現(xiàn)的,因此它們不會在每次發(fā)送或接收數(shù)據(jù)時重置。
2.2 Server Timeouts
因此你想在互聯(lián)網(wǎng)上公開Go的帖子發(fā)現(xiàn)更多關(guān)于服務(wù)器超時的信息,特別是關(guān)于HTTP/2和Go 1.7錯誤的信息:
對于暴露在Internet上的HTTP服務(wù)器來說,強(qiáng)制客戶端連接超時是至關(guān)重要的。否則,速度非常慢或正在消失的客戶端可能會泄漏文件描述符,并最終導(dǎo)致以下情況:
http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms
在http.Server中公開了兩個超時:ReadTimeout和WriteTimeout。您可以通過顯式使用服務(wù)器來設(shè)置它們:
srv := &http.Server{ ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } log.Println(srv.ListenAndServe())
ReadTimeout涵蓋了從接受連接到完全讀取請求正文的時間(如果您確實讀取了正文,否則到標(biāo)頭末尾)。它是通過在Accept之后立即調(diào)用SetReadDeadline在net/http中實現(xiàn)的。
WriteTimeout通常通過在readRequest結(jié)束時調(diào)用SetWriteDeadline來覆蓋從請求標(biāo)頭讀取結(jié)束到響應(yīng)寫入結(jié)束的時間(也稱為ServeHTTP的生存期)。
但是,當(dāng)連接是HTTPS時,在Accept之后會立即調(diào)用SetWriteDeadline,以便它也覆蓋作為TLS握手一部分寫入的數(shù)據(jù)包。令人煩惱的是,這意味著(僅在這種情況下)WriteTimeout最終包括頭讀取和第一個字節(jié)等待。
當(dāng)你處理不受信任的客戶端和/或網(wǎng)絡(luò)時,你應(yīng)該設(shè)置這兩個超時,這樣客戶端就不會因為寫或讀速度慢而中斷連接。
最后是http.TimeoutHandler。它不是Server參數(shù),而是一個限制ServeHTTP調(diào)用最長持續(xù)時間的Handler包裝器。它的工作方式是緩沖響應(yīng),如果超過最后期限,則發(fā)送504網(wǎng)關(guān)超時。請注意,它在1.6中被提出,在1.6.2中。
三、http.ListenAndServe 做的是錯誤的
順便說一句,這意味著繞過http.Server的包級便利功能,如http.ListenAndServe、http.Listen AndServeTLS和http.Serve,不適合公共Internet服務(wù)器。
這些函數(shù)將Tmeouts保留為默認(rèn)的off值,無法啟用它們,因此如果使用它們,很快就會出現(xiàn)連接泄漏和文件描述符用完的情況。我至少犯過六次這個錯誤。
相反,使用ReadTimeout和WriteTimeout創(chuàng)建一個http.Server實例,并使用其相應(yīng)的方法,就像上面幾段中的示例一樣。
3.1 streaming
非常令人惱火的是,沒有辦法從ServeHTTP訪問底層的net.Conn,因此打算流式傳輸響應(yīng)的服務(wù)器被迫取消設(shè)置WriteTimeout(這也可能是它們默認(rèn)為0的原因)。這是因為如果沒有net.Conne訪問,就無法在每次寫入之前調(diào)用SetWriteDeadline來實現(xiàn)適當(dāng)?shù)目臻e(而不是絕對)超時。
此外,無法取消被阻止的ResponseWriter.Write,因為沒有記錄ResponseWriter.Close(您可以通過接口升級訪問)來取消阻止并發(fā)寫入。因此,也沒有辦法用Timer手動構(gòu)建超時。
可悲的是,這意味著流媒體服務(wù)器無法真正保護(hù)自己免受慢速閱讀客戶端的攻擊。
3.2 Client Timeouts
客戶端超時可能更簡單,也可能更復(fù)雜,這取決于您使用的超時,但對于防止資源泄漏或陷入困境同樣重要。
最容易使用的是http.Client的Timeout字段。它涵蓋了整個交換,從Dial(如果不重用連接)到讀取主體。
c := &http.Client{ Timeout: 15 * time.Second, } resp, err := c.Get("https://blog.filippo.io/")
與上面的服務(wù)器端案例一樣,包級別的函數(shù)(如http.Get請求客戶端在沒有超時的情況下使用,因此在開放式互聯(lián)網(wǎng)上使用是危險的。
為了進(jìn)行更精細(xì)的控制,您可以設(shè)置許多其他更具體的超時
- net.Dialer.Timeout限制建立TCP連接所花費的時間(如果需要新的連接)。
- http.Transport.TLS握手超時限制執(zhí)行TLS握手所花費的時間
- http.Transport.ResponseHeaderTimeout限制讀取響應(yīng)標(biāo)頭所花費的時間。
- http.Transport.ExpectContinueTimeout限制客戶端在發(fā)送包含Expect:100 continue的請求標(biāo)頭和接收發(fā)送正文的批準(zhǔn)之間等待的時間。
c := &http.Client{ Transport: &http.Transport{ Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).Dial, TLSHandshakeTimeout: 10 * time.Second, ResponseHeaderTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } }
據(jù)我所知,沒有辦法具體限制發(fā)送請求所花費的時間。讀取請求正文所花費的時間可以通過time.Timer手動控制,因為它發(fā)生在Client方法返回之后(有關(guān)如何取消請求,請參閱下文)。
最后,1.7中新增了http.Transport.IdleConnTimeout。它不控制客戶端請求的阻塞階段,而是控制空閑連接在連接池中保持的時間。
請注意,客戶端默認(rèn)情況下會遵循重定向。http.Client.Timeout包括重定向后花費的所有時間,而細(xì)粒度超時是針對每個請求的,因為http.Transport是一個沒有重定向概念的較低級別系統(tǒng)。
3.3 Cancel and Context
nethttp提供了兩種取消客戶端請求的方法:request.cancel和1.7中新增的Context。
Request.Cancel是一個可選通道,當(dāng)設(shè)置并關(guān)閉時,會導(dǎo)致請求中止,就像達(dá)到Request.Timeout一樣。(它們實際上是通過相同的機(jī)制實現(xiàn)的,在寫這篇文章時,我在1.7中發(fā)現(xiàn)了一個錯誤,所有取消都會作為超時錯誤返回。)
我們可以使用Request.Cancel和time.Timer來構(gòu)建一個更精細(xì)的超時,允許流式傳輸,每次我們成功從Body讀取一些數(shù)據(jù)時都會將截止日期向后推:
package main import ( "io" "io/ioutil" "log" "net/http" "time" ) func main() { c := make(chan struct{}) timer := time.AfterFunc(5*time.Second, func() { close(c) }) // Serve 256 bytes every second. req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil) if err != nil { log.Fatal(err) } req.Cancel = c log.Println("Sending request...") resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() log.Println("Reading body...") for { timer.Reset(2 * time.Second) // Try instead: timer.Reset(50 * time.Millisecond) _, err = io.CopyN(ioutil.Discard, resp.Body, 256) if err == io.EOF { break } else if err != nil { log.Fatal(err) } } }
在上面的例子中,我們在請求的Do階段設(shè)置了5秒的超時,但隨后讀取正文,每次超時2秒??梢杂肋h(yuǎn)這樣流媒體,而不會有陷入困境的風(fēng)險。如果在超過2秒的時間內(nèi)沒有收到正文數(shù)據(jù),那么io.CopyN將返回net/http:requestcancelled。
在1.7中,上下文包升級為標(biāo)準(zhǔn)庫。關(guān)于上下文有很多需要學(xué)習(xí)的地方,但就目的而言,您應(yīng)該知道它們會取代和棄用Request.Cancel。
要使用Contexts取消請求,我們只需獲取一個新的Context及其帶有Context.WithCancel的cancel()函數(shù),并使用request.WithContext創(chuàng)建一個綁定到它的request。當(dāng)我們想取消請求時,我們通過調(diào)用cancel來取消Context:
ctx, cancel := context.WithCancel(context.TODO()) timer := time.AfterFunc(5*time.Second, func() { cancel() }) req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil) if err != nil { log.Fatal(err) } req = req.WithContext(ctx)
以上就是Go net http超時應(yīng)用場景全面詳解的詳細(xì)內(nèi)容,更多關(guān)于Go net http超時的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang標(biāo)準(zhǔn)庫time包日常用法小結(jié)
本文主要介紹了Golang標(biāo)準(zhǔn)庫time包日常用法小結(jié),可以通過它們來獲取當(dāng)前時間、創(chuàng)建指定時間、解析時間字符串、控制時間間隔等操作,感興趣的可以了解一下2023-11-11如何解析golang中Context在HTTP服務(wù)中的角色
這篇文章主要介紹了如何解析golang中Context在HTTP服務(wù)中的角色問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03