Go net/http 超時指導
當在編寫一個Go語言的HTTP服務端或者是客戶端時,超時是最容易同時也是最敏感的錯誤,有很多選擇,一個錯誤可以導致很長時間沒有結果,知道網路出現故障,或者程式宕掉。
HTTP是一個複雜的多階段的協議,所以超時沒有一刀切的解決方案。想想一個流的端點與JSON API端點和comet端點。事實上,預設值往往不是你想要的。
在這篇文章中,我將採取不同的階段,你可能需要申請一個超時,並在伺服器和客戶端不同的方式來實現。
設定最後期限(超時)
首先,你需要理解Go提供的最初級的網路超時實現:Deadlines(最後期限)。
在Go標準庫net.Conn中實現了Deadlines,通過 set[Read|Write]Deadline(time.Time)方法進行設定。Deadlines是一個絕對時間,一旦到時,將停止所有I/O操作,併產生一個超時錯誤。(譯註:time.Time的精度是納秒)
Deadlines本身是不會超時的。一旦被設定,將一直生效(直到再一次調SetDeadline),它並不關心在此期間連結是否存在以及如何使用。因此,你需要在每次進行讀/寫操作前,使用SetDeadline設定一個超時時長。
實際開發中,你並不需要直接呼叫SetDeadline,而是在標準庫net/http中使用更高層次的超時設定。但需要注意的是,所有基於Deadlines的超時都會被執行,所以不需要在每次收/發操作前,重置超時。(譯註:tcp、udp、unix-socket也是如此,參見標準庫net)。
伺服器超時
對於一個部署在Internet上的HTTP伺服器來說,設定客戶端連結超時,是至關重要的。否則,一個超慢或已消失的客戶端,可能會洩漏檔案描述符,並最終導致異常。如下所示:
http:Accepterror:accepttcp[::]:80:accept4:toomanyopenfiles;retryingin5ms
http.Server有兩個設定超時的方法:ReadTimeout和andWriteTimeout`。你可以顯式地設定它們:
1 |
2 |
3 |
4 |
5 |
ReadTimeout的時間計算是從連線被接受(accept)到request body完全被讀取(如果你不讀取body,那麼時間截止到讀完header為止)。它的內部實現是在Accept立即呼叫SetReadDeadline方法-程式碼。
WriteTimeout的時間計算正常是從request header的讀取結束開始,到 response write結束為止 (也就是 ServeHTTP 方法的宣告週期), 它是通過在readRequest方法結束的時候呼叫SetWriteDeadline實現的-程式碼。
但是,當連線是HTTPS的時候,SetWriteDeadline會在Accept之後立即呼叫-程式碼,所以它的時間計算也包括 TLS握手時的寫的時間。 討厭的是, 這就意味著(也只有這種情況)WriteTimeout設定的時間也包含讀取Headerd到讀取body第一個位元組這段時間。
當你處理不可信的客戶端和網路的時候,你應該同時設定讀寫超時,這樣客戶端就不會因為讀慢或者寫慢長久的持有這個連線了。
最後,還有一個http.TimeoutHandler方法。 它並不是Server引數,而是一個Handler包裝函式,可以限制ServeHTTP呼叫。它快取response, 如果deadline超過了則傳送504 Gateway Timeout錯誤。 注意這個功能在1.6 中有問題,在1.6.2中改正了。
http.ListenAndServe的問題
不幸的是, http.ListenAndServe,http.ListenAndServeTLS及http.Serveare等經由http.Server的便利函式不太適合用於對外釋出網路服務。
因為這些函式預設關閉了超時設定,也無法手動設定。使用這些函式,將很快洩露連線,然後耗盡檔案描述符。對於這點,我至少犯了6次以上這樣的錯誤。
對此,你應該使用http.server!在建立http.server例項的時候,呼叫相應的方法指定ReadTimeout(讀取超時時間)和WriteTimeout(寫超時時間),在以下會有一些案例。
關於流
比較惱火的是沒法從ServerHttp訪問net.Conn包下的物件,所以一個伺服器想要響應一個流就必須解除WriteTimeout設定(這就是為什麼預設值是0的原因)。因為訪問不到net.Conn包,就無法在每個Write操作之前呼叫SetWriteDeadline設定一個合理的閒置超時時間。
同理,由於無法確認ResponseWriter.Close支援併發寫操作,所以ResponseWriter.Write可能產生的阻塞,並且是無法被取消的。
(譯者注:Go 1.6.2版本中 ,介面ResponseWriter定義中是沒有Close方法的,需要在介面實現中自行實現。揣測是作者在開發中實現過該方法)
令人遺憾的是,這意味著流媒體伺服器面對一個低速客戶端時,將無法有效保障自身的效率、穩定。
我已經提交了一些建議,並期待有所反饋。
客戶端超時
客戶端超時,取決於你的決策,可以很簡單,也可以很複雜。但同樣重要的是:要防止資源洩漏和阻塞。
最簡單的使用超時的方式是http.Client。它涵蓋整個互動過程,從發起連線到接收響應報文結束。
1 |
2 |
3 |
4 |
與服務端情況類似,使用http.Get等包級易用函式建立客戶端時,也無法設定超時。應用在開放網路環境中,存在很大的風險。
還有其它一些方法,可以讓你進行更精細的超時控制:
-
net.Dialer.Timeout 限制建立一個TCP連線使用的時間(如果需要一個新的連結)
-
http.Transport.TLSHandshakeTimeout 限制TLS握手使用的時間
-
http.Transport.ResponseHeaderTimeout 限制讀取響應報文頭使用的時間
-
http.Transport.ExpectContinueTimeout 限制客戶端在傳送一個包含:100-continue的http報文頭後,等待收到一個go-ahead響應報文所用的時間。在1.6中,此設定對HTTP/2無效。(在1.6.2中提供了一個特定的封裝DefaultTransport)
01 |
02 |
03 |
04 |
05 |
06 |
07 |
08 |
09 |
10 |
11 |
據我瞭解,尚沒有限制傳送請求使用時間的機制。目前的解決方案是,在客戶端方法返回後,通過time.Timer來個手工控制讀取請求資訊的時間(參見下面的“如何取消請求”)。
最後,在新的1.7版本中,提供了http.Transport.IdleConnTimeout。它用於控制一個閒置連線在連線池中的保留時間,而不考慮一個客戶端請求被阻塞在哪個階段。
注意,客戶端將使用預設的重定向機制。由於http.Transport是一個底層的系統機制,沒有重定向概念,因此http.Client.Timeout涵蓋了用於重定向花費的時間,而更精細的超時控,可以根據請求的不同,進行定製。
Cancel 和 Context
net/http提供了兩種用於撤銷客戶端請求的方法:Request.Cancel以及新的1.7版本中提供的Context。
Request.Cancel是一個可選channel。在Request.Timeout被觸發時,Request.Cancel將被設定並關閉,進而促使請求中斷(基本上“撤銷”都採用相同的機制,在寫此文時,我發現一個1.7中的bug,所有的撤銷操作,都會當作一個超時錯誤返回)。
我們可以使用Request.Cancel和time.Timer,來構建一個超時更可控的,可用於流媒體的客戶端。它可以在成功獲響應報文體(Body)的部分資料後,重置deadline。
01 |
02 |
03 |
04 |
05 |
06 |
07 |
08 |
09 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
在上面這個例子中,我們在請求階段,設定了一個5秒鐘的超時。但讀取響應報文階段,我們需要讀8次,至少8秒鐘的時間。每次讀操作,設定2秒鐘的超時。採用這樣的機制,我們可以無限制的獲取流媒體,而不用擔心阻塞的風險。如果我們沒有在2秒鐘內讀取到任何資料,io.CopyN將返回錯誤資訊: net/http: request canceled。
在1.7版本標準庫中的新增了context包。關於Contexts,我們有大量需要學習的東西。基於本文的主旨,你首先應該知道的是:Contexts將替代Request.Cancel,不再建議(反對)使用Request.Cancel。
為了使用Contexts來撤銷一個請求,我們需要建立一個新的Context以及它的基於context.WithCancel的cancel()函式,同時還有建立一個基於Request.WithContext的Request。當我們要撤銷一個請求時,我們其實際是通過cancel()函式撤銷相應的Context(取代原有的關閉Cancel channel的方式):
01 |
02 |
03 |
04 |
05 |
06 |
07 |
08 |
09 |
10 |
在上下文(我們提供給context.WithCancel的)已經被撤銷的情況下,Contexts更具有優勢。我們可以向整個管道傳送命令。
就這些了。希望你對ReadDeadline理解比我更深刻。
——轉自開源中國社群
相關文章
- Go net/http 超時機制完全手冊GoHTTP
- Go net/http 標準庫思維導圖GoHTTP
- 導致HTTP代理超時的五種原因HTTP
- 導致HTTP超時的兩種常見原因HTTP
- 記go中一次http超時引發的事故GoHTTP
- go get 超時Go
- .NET 應用架構指導應用架構
- Go 裡的超時控制Go
- Go使用net/http庫傳送GET請求GoHTTP
- Go Web學習 -標準庫 net/http 使用GoWebHTTP
- 解決 go get 超時問題Go
- GO httpGoHTTP
- 【譯】Go 語言原始碼貢獻官方指導文件Go原始碼
- Go併發呼叫的超時處理Go
- 清華尹成帶你實戰GO案例(23)Go 超時Go
- GO 指標Go指標
- Go語言什麼時候該使用指標 與 指標使用分析Go指標
- Go 解決國內go get安裝包超時問題Go
- Angular 如何通過 HTTP Interceptor 實現 HTTP 請求的超時監控AngularHTTP
- 超詳細的TCP、Sokcket和SuperSocket與TCP入門指導TCP
- Go Gin安裝解決國內go get 方式安裝超時Go
- Go的http clientGoHTTPclient
- go http請求GoHTTP
- HTTP呼叫超時咋辦?重複請求又如何?HTTP
- 一文搞懂如何實現 Go 超時控制Go
- Git使用指導Git
- 導致代理超時的三種常見原因
- go實現http代理GoHTTP
- HTTP代理504閘道器超時錯誤如何修復?HTTP
- http代理504閘道器超時錯誤原因介紹HTTP
- 如何修復http代理504閘道器超時錯誤HTTP
- go-zero的全鏈路追蹤與超時Go
- 畫時序圖工具TimingDesigner 9.2 安裝指導時序圖
- Flutter ModalBottomSheet 指導Flutter
- wxPython使用指導Python
- nginx 入門指導Nginx
- redis叢集指導Redis
- 安裝docker指導Docker