本篇內(nèi)容主要講解“Golang自帶的HttpClient超時機制怎么實現(xiàn)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“Golang自帶的HttpClient超時機制怎么實現(xiàn)”吧!
網(wǎng)站建設(shè)哪家好,找成都創(chuàng)新互聯(lián)!專注于網(wǎng)頁設(shè)計、網(wǎng)站建設(shè)、微信開發(fā)、成都微信小程序、集團企業(yè)網(wǎng)站建設(shè)等服務(wù)項目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了思明免費建站歡迎大家使用!
在介紹 Go 的 HttpClient 超時機制之前,我們先看看 Java 是如何實現(xiàn)超時的。
寫一個 Java 原生的 HttpClient,設(shè)置連接超時、讀取超時時間分別對應(yīng)到底層的方法分別是:
再追溯到 JVM 源碼,發(fā)現(xiàn)是對系統(tǒng)調(diào)用的封裝,其實不光是 Java,大部分的編程語言都借助了操作系統(tǒng)提供的超時能力。
然而 Go 的 HttpClient 卻提供了另一種超時機制,挺有意思,我們來盤一盤。但在開始之前,我們先了解一下 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 簡單來說是一個可以攜帶超時時間、取消信號和其他數(shù)據(jù)的接口,Context 的方法會被多個協(xié)程同時調(diào)用。
Context 有點類似 Java 的ThreadLocal,可以在線程中傳遞數(shù)據(jù),但又不完全相同,它是顯示傳遞,ThreadLocal 是隱式傳遞,除了傳遞數(shù)據(jù)之外,Context 還能攜帶超時時間、取消信號。
Context 只是定義了接口,具體的實現(xiàn)在 Go 中提供了幾個:
Background :空的實現(xiàn),啥也沒做
TODO:還不知道用什么 Context,先用 TODO 代替,也是啥也沒做的空 Context
cancelCtx:可以取消的 Context
timerCtx:主動超時的 Context
針對 Context 的三個特性,可以通過 Go 提供的 Context 實現(xiàn)以及源碼中的例子來進一步了解下。
Context 三個特性例子
這部分的例子來源于 Go 的源碼,位于 src/context/example_test.go
使用 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
}
先起一個協(xié)程執(zhí)行一個死循環(huán),不停地往 channel 中寫數(shù)據(jù),同時監(jiān)聽 ctx.Done()
的事件
然后通過 這么看起來,可以簡單理解為在一個協(xié)程的循環(huán)中埋入結(jié)束標志,另一個協(xié)程去設(shè)置這個結(jié)束標志。// 來自 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
生成一個可取消的 Context,傳入gen
方法,直到gen
返回 5 時,調(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
有了 cancel 的鋪墊,超時就好理解了,cancel 是手動取消,超時是自動取消,只要起一個定時的協(xié)程,到時間后執(zhí)行 cancel 即可。
設(shè)置超時時間有2種方式:context.WithTimeout
與 context.WithDeadline
,WithTimeout 是設(shè)置一段時間后,WithDeadline 是設(shè)置一個截止時間點,WithTimeout 最終也會轉(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
}
基于 Context 可以設(shè)置任意代碼段執(zhí)行的超時機制,就可以設(shè)計一種脫離操作系統(tǒng)能力的請求超時能力。
超時機制簡介
看一下 Go 的 HttpClient 超時配置說明:
翻譯一下注釋: 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
}
Timeout
包括了連接、redirect、讀取數(shù)據(jù)的時間,定時器會在 Timeout 時間后打斷數(shù)據(jù)的讀取,設(shè)為0則沒有超時限制。
也就是說這個超時是一個請求的總體超時時間,而不必再分別去設(shè)置連接超時、讀取超時等等。
這對于使用者來說可能是一個更好的選擇,大部分場景,使用者不必關(guān)心到底是哪部分導(dǎo)致的超時,而只是想這個 HTTP 請求整體什么時候能返回。
超時機制底層原理
以一個最簡單的例子來闡述超時機制的底層原理。
這里我起了一個本地服務(wù),用 Go HttpClient 去請求,超時時間設(shè)置為 10 分鐘,建議使 Debug 時設(shè)置長一點,否則可能超時導(dǎo)致無法走完全流程。
client := http.Client{
Timeout: 10 * time.Minute,
}
resp, err := client.Get("http://127.0.0.1:81/hello")
// 來自 src/net/http/client.go
deadline = c.deadline()
登錄后復(fù)制// 來自 src/net/http/client.go
stopTimer, didTimeout := setRequestCancel(req, rt, deadline)
這里返回的 stopTimer 就是可以手動 cancel 的方法,didTimeout 是判斷是否超時的方法。這兩個可以理解為回調(diào)方法,調(diào)用 stopTimer() 可以手動 cancel,調(diào)用 didTimeout() 可以返回是否超時。
設(shè)置的主要代碼其實就是將請求的 Context 替換為 cancelCtx,后續(xù)所有的操作都將攜帶這個 cancelCtx:
同時,再起一個定時器,當超時時間到了之后,將 timedOut 設(shè)置為 true,再調(diào)用 doCancel(),doCancel() 是調(diào)用真正 RoundTripper (代表一個 HTTP 請求事務(wù))的 CancelRequest,也就是取消請求,這個跟實現(xiàn)有關(guān)。 Go 默認 RoundTripper CancelRequest 實現(xiàn)是關(guān)閉這個連接// 來自 src/net/http/client.go
var cancelCtx func()
if oldCtx := req.Context(); timeBeforeContextDeadline(deadline, oldCtx) {
req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
}
// 來自 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()
}
}()
// 位于 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)
}
代碼的開頭監(jiān)聽 ctx.Done,如果超時則直接返回,使用 for 循環(huán)主要是為了請求的重試。// 位于 src/net/http/transport.go
for {
select {
case <-ctx.Done():
req.closeBody()
return nil, ctx.Err()
default:
}
// ...
pconn, err := t.getConn(treq, cm)
// ...
}
后續(xù)的 getConn 是阻塞的,代碼比較長,挑重點說,先看看有沒有空閑連接,如果有則直接返回
如果沒有空閑連接,起個協(xié)程去異步建立,建立成功再通知主協(xié)程 再接著是一個 select 等待連接建立成功、超時或者主動取消,這就實現(xiàn)了在連接過程中的超時// 位于 src/net/http/transport.go
// Queue for idle connection.
if delivered := t.queueForIdleConn(w); delivered {
// ...
return pc, nil
}
// 位于 src/net/http/transport.go
// Queue for permission to dial.
t.queueForDial(w)
// 位于 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
}
在上一條連接建立的時候,每個鏈接還偷偷起了兩個協(xié)程,一個負責往連接中寫入數(shù)據(jù),另一個負責讀數(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é)程。讀與寫的過程中如果超時了,連接將被關(guān)閉,報錯退出。
超時機制小結(jié)
Go 的這種請求超時機制,可隨時終止請求,可設(shè)置整個請求的超時時間。其實現(xiàn)主要依賴協(xié)程、channel、select 機制的配合??偨Y(jié)出套路是:
主協(xié)程生成 cancelCtx,傳遞給子協(xié)程,主協(xié)程與子協(xié)程之間用 channel 通信
主協(xié)程 select channel 和 cancelCtx.Done,子協(xié)程完成或取消則 return
循環(huán)任務(wù):子協(xié)程起一個循環(huán)處理,每次循環(huán)開始都 select cancelCtx.Done,如果完成或取消則退出
阻塞任務(wù):子協(xié)程 select 阻塞任務(wù)與 cancelCtx.Done,阻塞任務(wù)處理完或取消則退出
以循環(huán)任務(wù)為例
Java 能實現(xiàn)這種超時機制嗎
直接說結(jié)論:暫時不行。
首先 Java 的線程太重,像 Go 這樣一次請求開了這么多協(xié)程,換成線程性能會大打折扣。
其次 Go 的 channel 雖然和 Java 的阻塞隊列類似,但 Go 的 select 是多路復(fù)用機制,Java 暫時無法實現(xiàn),即無法監(jiān)聽多個隊列是否有數(shù)據(jù)到達。所以綜合來看 Java 暫時無法實現(xiàn)類似機制。
到此,相信大家對“Golang自帶的HttpClient超時機制怎么實現(xiàn)”有了更深的了解,不妨來實際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!