深入了解Go的HttpClient超時(shí)機(jī)制
Java HttpClient 超時(shí)底層原理
在介紹 Go 的 HttpClient 超時(shí)機(jī)制之前,我們先看看 Java 是如何實(shí)現(xiàn)超時(shí)的。
寫一個(gè) Java 原生的 HttpClient,設(shè)置連接超時(shí)、讀取超時(shí)時(shí)間分別對應(yīng)到底層的方法分別是:
再追溯到 JVM 源碼,發(fā)現(xiàn)是對系統(tǒng)調(diào)用的封裝,其實(shí)不光是 Java,大部分的編程語言都借助了操作系統(tǒng)提供的超時(shí)能力。
然而 Go 的 HttpClient 卻提供了另一種超時(shí)機(jī)制,挺有意思,我們來盤一盤。但在開始之前,我們先了解一下 Go 的 Context。
Go Context 簡介
Context 是什么
根據(jù) Go 源碼的注釋:
// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
// Context's methods may be called by multiple goroutines simultaneously.
Context 簡單來說是一個(gè)可以攜帶超時(shí)時(shí)間、取消信號(hào)和其他數(shù)據(jù)的接口,Context 的方法會(huì)被多個(gè)協(xié)程同時(shí)調(diào)用。
Context 有點(diǎn)類似 Java 的ThreadLocal,可以在線程中傳遞數(shù)據(jù),但又不完全相同,它是顯示傳遞,ThreadLocal 是隱式傳遞,除了傳遞數(shù)據(jù)之外,Context 還能攜帶超時(shí)時(shí)間、取消信號(hào)。
Context 只是定義了接口,具體的實(shí)現(xiàn)在 Go 中提供了幾個(gè):
- Background :空的實(shí)現(xiàn),啥也沒做
- TODO:還不知道用什么 Context,先用 TODO 代替,也是啥也沒做的空 Context
- cancelCtx:可以取消的 Context
- timerCtx:主動(dòng)超時(shí)的 Context
針對 Context 的三個(gè)特性,可以通過 Go 提供的 Context 實(shí)現(xiàn)以及源碼中的例子來進(jìn)一步了解下。
Context 三個(gè)特性例子
這部分的例子來源于 Go 的源碼,位于 src/context/example_test.go
攜帶數(shù)據(jù)
使用 context.WithValue
來攜帶,使用 Value
來取值,源碼中的例子如下:
// 來自 src/context/example_test.go func ExampleWithValue() { type favContextKey string f := func(ctx context.Context, k favContextKey) { if v := ctx.Value(k); v != nil { fmt.Println("found value:", v) return } fmt.Println("key not found:", k) } k := favContextKey("language") ctx := context.WithValue(context.Background(), k, "Go") f(ctx, k) f(ctx, favContextKey("color")) // Output: // found value: Go // key not found: color }
取消
先起一個(gè)協(xié)程執(zhí)行一個(gè)死循環(huán),不停地往 channel 中寫數(shù)據(jù),同時(shí)監(jiān)聽 ctx.Done()
的事件
// 來自 src/context/example_test.go gen := func(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): return // returning not to leak the goroutine case dst <- n: n++ } } }() return dst }
然后通過 context.WithCancel
生成一個(gè)可取消的 Context,傳入 gen
方法,直到 gen
返回 5 時(shí),調(diào)用 cancel
取消 gen
方法的執(zhí)行。
// 來自 src/context/example_test.go ctx, cancel := context.WithCancel(context.Background()) defer cancel() // cancel when we are finished consuming integers for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } // Output: // 1 // 2 // 3 // 4 // 5
這么看起來,可以簡單理解為在一個(gè)協(xié)程的循環(huán)中埋入結(jié)束標(biāo)志,另一個(gè)協(xié)程去設(shè)置這個(gè)結(jié)束標(biāo)志。
超時(shí)
有了 cancel 的鋪墊,超時(shí)就好理解了,cancel 是手動(dòng)取消,超時(shí)是自動(dòng)取消,只要起一個(gè)定時(shí)的協(xié)程,到時(shí)間后執(zhí)行 cancel 即可。
設(shè)置超時(shí)時(shí)間有2種方式:context.WithTimeout
與 context.WithDeadline
,WithTimeout 是設(shè)置一段時(shí)間后,WithDeadline 是設(shè)置一個(gè)截止時(shí)間點(diǎn),WithTimeout 最終也會(huì)轉(zhuǎn)換為 WithDeadline。
// 來自 src/context/example_test.go func ExampleWithTimeout() { // Pass a context with a timeout to tell a blocking function that it // should abandon its work after the timeout elapses. ctx, cancel := context.WithTimeout(context.Background(), shortDuration) defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) // prints "context deadline exceeded" } // Output: // context deadline exceeded }
Go HttpClient 的另一種超時(shí)機(jī)制
基于 Context 可以設(shè)置任意代碼段執(zhí)行的超時(shí)機(jī)制,就可以設(shè)計(jì)一種脫離操作系統(tǒng)能力的請求超時(shí)能力。
超時(shí)機(jī)制簡介
看一下 Go 的 HttpClient 超時(shí)配置說明:
client := http.Client{ Timeout: 10 * time.Second, } // 來自 src/net/http/client.go type Client struct { // ... 省略其他字段 // Timeout specifies a time limit for requests made by this // Client. The timeout includes connection time, any // redirects, and reading the response body. The timer remains // running after Get, Head, Post, or Do return and will // interrupt reading of the Response.Body. // // A Timeout of zero means no timeout. // // The Client cancels requests to the underlying Transport // as if the Request's Context ended. // // For compatibility, the Client will also use the deprecated // CancelRequest method on Transport if found. New // RoundTripper implementations should use the Request's Context // for cancellation instead of implementing CancelRequest. Timeout time.Duration }
翻譯一下這個(gè)注釋:Timeout
包括了連接、redirect、讀取數(shù)據(jù)的時(shí)間,定時(shí)器會(huì)在 Timeout 時(shí)間后打斷數(shù)據(jù)的讀取,設(shè)為0則沒有超時(shí)限制。
也就是說這個(gè)超時(shí)是一個(gè)請求的總體超時(shí)時(shí)間,而不必再分別去設(shè)置連接超時(shí)、讀取超時(shí)等等。
這對于使用者來說可能是一個(gè)更好的選擇,大部分場景,使用者不必關(guān)心到底是哪部分導(dǎo)致的超時(shí),而只是想這個(gè) HTTP 請求整體什么時(shí)候能返回。
超時(shí)機(jī)制底層原理
以一個(gè)最簡單的例子來闡述超時(shí)機(jī)制的底層原理。
這里我起了一個(gè)本地服務(wù),用 Go HttpClient 去請求,超時(shí)時(shí)間設(shè)置為 10 分鐘,建議使 Debug 時(shí)設(shè)置長一點(diǎn),否則可能超時(shí)導(dǎo)致無法走完全流程。
client := http.Client{ Timeout: 10 * time.Minute, } resp, err := client.Get("http://127.0.0.1:81/hello")
1. 根據(jù) timeout 計(jì)算出超時(shí)的時(shí)間點(diǎn)
// 來自 src/net/http/client.go deadline = c.deadline()
2. 設(shè)置請求的 cancel
// 來自 src/net/http/client.go stopTimer, didTimeout := setRequestCancel(req, rt, deadline)
這里返回的 stopTimer 就是可以手動(dòng) cancel 的方法,didTimeout 是判斷是否超時(shí)的方法。這兩個(gè)可以理解為回調(diào)方法,調(diào)用 stopTimer() 可以手動(dòng) cancel,調(diào)用 didTimeout() 可以返回是否超時(shí)。
設(shè)置的主要代碼其實(shí)就是將請求的 Context 替換為 cancelCtx,后續(xù)所有的操作都將攜帶這個(gè) cancelCtx:
// 來自 src/net/http/client.go var cancelCtx func() if oldCtx := req.Context(); timeBeforeContextDeadline(deadline, oldCtx) { req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline) }
同時(shí),再起一個(gè)定時(shí)器,當(dāng)超時(shí)時(shí)間到了之后,將 timedOut 設(shè)置為 true,再調(diào)用 doCancel(),doCancel() 是調(diào)用真正 RoundTripper (代表一個(gè) HTTP 請求事務(wù))的 CancelRequest,也就是取消請求,這個(gè)跟實(shí)現(xiàn)有關(guān)。
// 來自 src/net/http/client.go timer := time.NewTimer(time.Until(deadline)) var timedOut atomicBool go func() { select { case <-initialReqCancel: doCancel() timer.Stop() case <-timer.C: timedOut.setTrue() doCancel() case <-stopTimerCh: timer.Stop() } }()
Go 默認(rèn) RoundTripper CancelRequest 實(shí)現(xiàn)是關(guān)閉這個(gè)連接
// 位于 src/net/http/transport.go // CancelRequest cancels an in-flight request by closing its connection. // CancelRequest should only be called after RoundTrip has returned. func (t *Transport) CancelRequest(req *Request) { t.cancelRequest(cancelKey{req}, errRequestCanceled) }
3. 獲取連接
// 位于 src/net/http/transport.go for { select { case <-ctx.Done(): req.closeBody() return nil, ctx.Err() default: } // ... pconn, err := t.getConn(treq, cm) // ... }
代碼的開頭監(jiān)聽 ctx.Done,如果超時(shí)則直接返回,使用 for 循環(huán)主要是為了請求的重試。
后續(xù)的 getConn 是阻塞的,代碼比較長,挑重點(diǎn)說,先看看有沒有空閑連接,如果有則直接返回
// 位于 src/net/http/transport.go // Queue for idle connection. if delivered := t.queueForIdleConn(w); delivered { // ... return pc, nil }
如果沒有空閑連接,起個(gè)協(xié)程去異步建立,建立成功再通知主協(xié)程
// 位于 src/net/http/transport.go // Queue for permission to dial. t.queueForDial(w)
再接著是一個(gè) select 等待連接建立成功、超時(shí)或者主動(dòng)取消,這就實(shí)現(xiàn)了在連接過程中的超時(shí)
// 位于 src/net/http/transport.go // Wait for completion or cancellation. select { case <-w.ready: // ... return w.pc, w.err case <-req.Cancel: return nil, errRequestCanceledConn case <-req.Context().Done(): return nil, req.Context().Err() case err := <-cancelc: if err == errRequestCanceled { err = errRequestCanceledConn } return nil, err }
4. 讀寫數(shù)據(jù)
在上一條連接建立的時(shí)候,每個(gè)鏈接還偷偷起了兩個(gè)協(xié)程,一個(gè)負(fù)責(zé)往連接中寫入數(shù)據(jù),另一個(gè)負(fù)責(zé)讀數(shù)據(jù),他們都監(jiān)聽了相應(yīng)的 channel。
// 位于 src/net/http/transport.go go pconn.readLoop() go pconn.writeLoop()
其中 wirteLoop 監(jiān)聽來自主協(xié)程的數(shù)據(jù),并往連接中寫入
// 位于 src/net/http/transport.go func (pc *persistConn) writeLoop() { defer close(pc.writeLoopDone) for { select { case wr := <-pc.writech: startBytesWritten := pc.nwrite err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh)) // ... if err != nil { pc.close(err) return } case <-pc.closech: return } } }
同理,readLoop 讀取響應(yīng)數(shù)據(jù),并寫回主協(xié)程。讀與寫的過程中如果超時(shí)了,連接將被關(guān)閉,報(bào)錯(cuò)退出。
超時(shí)機(jī)制小結(jié)
Go 的這種請求超時(shí)機(jī)制,可隨時(shí)終止請求,可設(shè)置整個(gè)請求的超時(shí)時(shí)間。其實(shí)現(xiàn)主要依賴協(xié)程、channel、select 機(jī)制的配合??偨Y(jié)出套路是:
- 主協(xié)程生成 cancelCtx,傳遞給子協(xié)程,主協(xié)程與子協(xié)程之間用 channel 通信
- 主協(xié)程 select channel 和 cancelCtx.Done,子協(xié)程完成或取消則 return
- 循環(huán)任務(wù):子協(xié)程起一個(gè)循環(huán)處理,每次循環(huán)開始都 select cancelCtx.Done,如果完成或取消則退出
- 阻塞任務(wù):子協(xié)程 select 阻塞任務(wù)與 cancelCtx.Done,阻塞任務(wù)處理完或取消則退出
以循環(huán)任務(wù)為例
Java 能實(shí)現(xiàn)這種超時(shí)機(jī)制嗎
直接說結(jié)論:暫時(shí)不行。
首先 Java 的線程太重,像 Go 這樣一次請求開了這么多協(xié)程,換成線程性能會(huì)大打折扣。
其次 Go 的 channel 雖然和 Java 的阻塞隊(duì)列類似,但 Go 的 select 是多路復(fù)用機(jī)制,Java 暫時(shí)無法實(shí)現(xiàn),即無法監(jiān)聽多個(gè)隊(duì)列是否有數(shù)據(jù)到達(dá)。所以綜合來看 Java 暫時(shí)無法實(shí)現(xiàn)類似機(jī)制。
總結(jié)
本文介紹了 Go 另類且有趣的 HTTP 超時(shí)機(jī)制,并且分析了底層實(shí)現(xiàn)原理,歸納出了這種機(jī)制的套路,如果我們寫 Go 代碼,也可以如此模仿,讓代碼更 Go。
以上就是深入了解Go的HttpClient超時(shí)機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Go HttpClient超時(shí)機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go中Excelize處理excel表實(shí)現(xiàn)帶數(shù)據(jù)校驗(yàn)的文件導(dǎo)出
本文主要介紹了go中Excelize處理excel表實(shí)現(xiàn)帶數(shù)據(jù)校驗(yàn)的文件導(dǎo)出,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06從淺入深帶你掌握Golang數(shù)據(jù)結(jié)構(gòu)map
在?Go?語言中,map?是一種非常常見的數(shù)據(jù)類型,它可以用于快速地檢索數(shù)據(jù)。本篇文章將介紹?Go?語言中的?map,包括?map?的定義、初始化、操作和優(yōu)化,需要的可以參考一下2023-04-04