網站首頁 編程語言 正文
Java HttpClient 超時底層原理
在介紹 Go 的 HttpClient 超時機制之前,我們先看看 Java 是如何實現超時的。
寫一個 Java 原生的 HttpClient,設置連接超時、讀取超時時間分別對應到底層的方法分別是:
再追溯到 JVM 源碼,發現是對系統調用的封裝,其實不光是 Java,大部分的編程語言都借助了操作系統提供的超時能力。
然而 Go 的 HttpClient 卻提供了另一種超時機制,挺有意思,我們來盤一盤。但在開始之前,我們先了解一下 Go 的 Context。
Go Context 簡介
Context 是什么
根據 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 簡單來說是一個可以攜帶超時時間、取消信號和其他數據的接口,Context 的方法會被多個協程同時調用。
Context 有點類似 Java 的ThreadLocal,可以在線程中傳遞數據,但又不完全相同,它是顯示傳遞,ThreadLocal 是隱式傳遞,除了傳遞數據之外,Context 還能攜帶超時時間、取消信號。
Context 只是定義了接口,具體的實現在 Go 中提供了幾個:
- Background :空的實現,啥也沒做
- TODO:還不知道用什么 Context,先用 TODO 代替,也是啥也沒做的空 Context
- cancelCtx:可以取消的 Context
- timerCtx:主動超時的 Context
針對 Context 的三個特性,可以通過 Go 提供的 Context 實現以及源碼中的例子來進一步了解下。
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 }
取消
先起一個協程執行一個死循環,不停地往 channel 中寫數據,同時監聽?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
?生成一個可取消的 Context,傳入?gen
?方法,直到?gen
?返回 5 時,調用?cancel
?取消?gen
?方法的執行。
// 來自 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 是手動取消,超時是自動取消,只要起一個定時的協程,到時間后執行 cancel 即可。
設置超時時間有2種方式:context.WithTimeout
?與?context.WithDeadline
,WithTimeout 是設置一段時間后,WithDeadline 是設置一個截止時間點,WithTimeout 最終也會轉換為 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 的另一種超時機制
基于 Context 可以設置任意代碼段執行的超時機制,就可以設計一種脫離操作系統能力的請求超時能力。
超時機制簡介
看一下 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、讀取數據的時間,定時器會在 Timeout 時間后打斷數據的讀取,設為0則沒有超時限制。
也就是說這個超時是一個請求的總體超時時間,而不必再分別去設置連接超時、讀取超時等等。
這對于使用者來說可能是一個更好的選擇,大部分場景,使用者不必關心到底是哪部分導致的超時,而只是想這個 HTTP 請求整體什么時候能返回。
超時機制底層原理
以一個最簡單的例子來闡述超時機制的底層原理。
這里我起了一個本地服務,用 Go HttpClient 去請求,超時時間設置為 10 分鐘,建議使 Debug 時設置長一點,否則可能超時導致無法走完全流程。
client := http.Client{ Timeout: 10 * time.Minute, } resp, err := client.Get("http://127.0.0.1:81/hello")
1. 根據 timeout 計算出超時的時間點
// 來自 src/net/http/client.go deadline = c.deadline()
2. 設置請求的 cancel
// 來自 src/net/http/client.go stopTimer, didTimeout := setRequestCancel(req, rt, deadline)
這里返回的 stopTimer 就是可以手動 cancel 的方法,didTimeout 是判斷是否超時的方法。這兩個可以理解為回調方法,調用 stopTimer() 可以手動 cancel,調用 didTimeout() 可以返回是否超時。
設置的主要代碼其實就是將請求的 Context 替換為 cancelCtx,后續所有的操作都將攜帶這個 cancelCtx:
// 來自 src/net/http/client.go var cancelCtx func() if oldCtx := req.Context(); timeBeforeContextDeadline(deadline, oldCtx) { req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline) }
同時,再起一個定時器,當超時時間到了之后,將 timedOut 設置為 true,再調用 doCancel(),doCancel() 是調用真正 RoundTripper (代表一個 HTTP 請求事務)的 CancelRequest,也就是取消請求,這個跟實現有關。
// 來自 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 默認 RoundTripper CancelRequest 實現是關閉這個連接
// 位于 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) // ... }
代碼的開頭監聽 ctx.Done,如果超時則直接返回,使用 for 循環主要是為了請求的重試。
后續的 getConn 是阻塞的,代碼比較長,挑重點說,先看看有沒有空閑連接,如果有則直接返回
// 位于 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)
再接著是一個 select 等待連接建立成功、超時或者主動取消,這就實現了在連接過程中的超時
// 位于 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. 讀寫數據
在上一條連接建立的時候,每個鏈接還偷偷起了兩個協程,一個負責往連接中寫入數據,另一個負責讀數據,他們都監聽了相應的 channel。
// 位于 src/net/http/transport.go go pconn.readLoop() go pconn.writeLoop()
其中 wirteLoop 監聽來自主協程的數據,并往連接中寫入
// 位于 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 讀取響應數據,并寫回主協程。讀與寫的過程中如果超時了,連接將被關閉,報錯退出。
超時機制小結
Go 的這種請求超時機制,可隨時終止請求,可設置整個請求的超時時間。其實現主要依賴協程、channel、select 機制的配合。總結出套路是:
- 主協程生成 cancelCtx,傳遞給子協程,主協程與子協程之間用 channel 通信
- 主協程 select channel 和 cancelCtx.Done,子協程完成或取消則 return
- 循環任務:子協程起一個循環處理,每次循環開始都 select cancelCtx.Done,如果完成或取消則退出
- 阻塞任務:子協程 select 阻塞任務與 cancelCtx.Done,阻塞任務處理完或取消則退出
以循環任務為例
Java 能實現這種超時機制嗎
直接說結論:暫時不行。
首先 Java 的線程太重,像 Go 這樣一次請求開了這么多協程,換成線程性能會大打折扣。
其次 Go 的 channel 雖然和 Java 的阻塞隊列類似,但 Go 的 select 是多路復用機制,Java 暫時無法實現,即無法監聽多個隊列是否有數據到達。所以綜合來看 Java 暫時無法實現類似機制。
總結
本文介紹了 Go 另類且有趣的 HTTP 超時機制,并且分析了底層實現原理,歸納出了這種機制的套路,如果我們寫 Go 代碼,也可以如此模仿,讓代碼更 Go。
原文鏈接:https://www.cnblogs.com/zhuochongdashi/p/16893627.html
相關推薦
- 2022-12-01 C++中單鏈表操作的示例代碼_C 語言
- 2022-04-26 Swift踩坑實戰之一個字符引發的Crash_Swift
- 2022-04-30 Python中類變量和實例變量的區別_python
- 2022-05-01 C#實現提取Word中插入的多媒體文件(視頻,音頻)_C#教程
- 2022-10-14 C++ STL - list 模擬實現+解析迭代器
- 2022-05-02 winform關閉窗體FormClosing事件用法介紹_C#教程
- 2022-05-08 ASP.NET?MVC視圖尋址_實用技巧
- 2022-09-25 git提交代碼版本沖突問題
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支