這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
當使用Go開發HTTP伺服器或用戶端時,逾時造成的錯誤,常常是簡單而又微妙的:很多因素都可能產生逾時。一個錯誤可以很長一段時間沒有結果,直到網路故障,進程被掛起。
HTTP是一個複雜的多層協議,所以在逾時這個問題上,並沒有一個通用的解決方案。想一想:流媒體終端、JSON API、Comet終端。事實上,預設值往往不是你想要的。(譯註:沒理解Comet endpoint是什麼意思。原文給出的連結是維基百科上天文意義的彗星。譯者懷疑是支援BT協議的BitComet)
在這篇文章中,我將分別介紹,在那些階段,你可能需要設定一個逾時。而且在伺服器和用戶端上,也將採用不同的方式來處理逾時。(譯者:本文主要是基於Go標準庫進行介紹的。Go標準庫在逾時定義上提供了很高的靈活性。譯者在剛開始用Go開發時,很是被折騰了一把)
caotj72 翻譯於 11個月前 2人頂 頂 翻譯得不錯哦! 其它翻譯版本(1)
正在載入...
設定期限(逾時)
首先,你需要理解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: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms
caotj72 翻譯於 11個月前 1人頂 頂 翻譯得不錯哦!
http.Server提供了兩個逾時實現ReadTimeout和WriteTimeout。你可以使用顯式定義方式來設定它們:
srv := &http.Server{ ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second,}log.Println(srv.ListenAndServe())
ReadTimeout涵蓋的時間範圍是:從受理一個連結請求開始,到讀取一個完整請求報文後結束(HTTP協議的請求報文,可能只有報文頭,例如GET,所以,也可以是讀取請求報文頭後)。是在net/http的Accept方法中,通過調用SetReadline來設定的。
WriteTimeout涵蓋的時間範圍是:從讀取請求報文頭後開始,到返迴響應報文後結束(也可以稱為:ServeHTTP生命週期)。在readRequest方法結束前,通過SetWriteDeadline來設定。
然而,在使用HTTPS串連時,WriteTimeout是在Accept方法中,調用SetWriteDeadline來設定的。因為,它還需要涵蓋TLS握手所用的時間。這意味著(僅在此情況下),在使用HTTPS時,WriteTimeout實際上包括了請求報文的擷取/等待時間。
當你處理不可信的用戶端以及網路時,應該將兩種逾時都設定上。以此來避免,一個用戶端,因超慢的讀/寫操作,長時間佔用一個連結資源。
最後是http.TimeoutHandler。它不是一個Server參數,但一個Handler封裝,會用它來限制ServeHTTP調用的時間長度。當達到逾時條件時,將緩衝響應資料,並發送一個504 Gateway Timeout 。注意,1.6版本存在問題,1.6.2中被修複。
caotj72 翻譯於 11個月前 1人頂 頂 翻譯得不錯哦! 其它翻譯版本(1)
正在載入...
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設定一個合理的閑置逾時時間。
imqipan 翻譯於 11個月前 0人頂 頂 翻譯得不錯哦!
同理,由於無法確認ResponseWriter.Close支援並發寫操作,所以ResponseWriter.Write可能產生的阻塞,並且是無法被取消的。
(譯者註:Go 1.6.2版本中 ,介面ResponseWriter定義中是沒有Close方法的,需要在介面實現中自行實現。揣測是作者在開發中實現過該方法)
令人遺憾的是,這意味著流媒體伺服器面對一個低速用戶端時,將無法有效保障自身的效率、穩定。
我已經提交了一些建議,並期待有所反饋。
用戶端逾時
用戶端逾時,取決於你的決策,可以很簡單,也可以很複雜。但同樣重要的是:要防止資源泄漏和阻塞。
最簡單的使用逾時的方式是http.Client。它涵蓋整個互動過程,從發起串連到接收響應報文結束。
c := &http.Client{ Timeout: 15 * time.Second,}resp, err := c.Get("https://blog.filippo.io/")
與服務端情況類似,使用http.Get等包級易用函數建立用戶端時,也無法設定逾時。應用在開放網路環境中,存在很大的風險。
caotj72 翻譯於 11個月前 0人頂 頂 翻譯得不錯哦!
還有其它一些方法,可以讓你進行更精細的逾時控制:
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)
c := &http.Client{ Transport: &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, }}
據我瞭解,尚沒有限制發送請求使用時間的機制。目前的解決方案是,在用戶端方法返回後,通過time.Timer來個手工控制讀取請求資訊的時間(參見下面的“如何取消請求”)。
最後,在新的1.7版本中,提供了http.Transport.IdleConnTimeout。它用於控制一個閑置串連在串連池中的保留時間,而不考慮一個用戶端請求被阻塞在哪個階段。
注意,用戶端將使用預設的重新導向機制。由於http.Transport是一個底層的系統機制,沒有重新導向概念,因此http.Client.Timeout涵蓋了用於重新導向花費的時間,而更精細的逾時控,可以根據請求的不同,進行定製。
caotj72 翻譯於 11個月前 0人頂 頂 翻譯得不錯哦!
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。
package mainimport ( "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) } }}
在上面這個例子中,我們在要求階段,設定了一個5秒鐘的逾時。但讀取響應報文階段,我們需要讀8次,至少8秒鐘的時間。每次讀操作,設定2秒鐘的逾時。採用這樣的機制,我們可以無限制的擷取流媒體,而不用擔心阻塞的風險。如果我們沒有在2秒鐘內讀取到任何資料,io.CopyN將返回錯誤資訊: net/http: request canceled。
在1.7版本標準庫中的新增了context包。關於Contexts,我們有大量需要學習的東西。基於本文的主旨,你首先應該知道的是:Contexts將替代Request.Cancel,不再建議(反對)使用Request.Cancel。
caotj72 翻譯於 11個月前 0人頂 頂 翻譯得不錯哦!
為了使用Contexts來撤銷一個請求,我們需要建立一個新的Context以及它的基於context.WithCancel的cancel()函數,同時還有建立一個基於Request.WithContext的Request。當我們要撤銷一個請求時,我們其實際是通過cancel()函數撤銷相應的Context(取代原有的關閉Cancel channel的方式):
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)
在上下文(我們提供給context.WithCancel的)已經被撤銷的情況下,Contexts更具有優勢。我們可以向整個管道發送命令。
就這些了。希望你對ReadDeadline理解比我更深刻。
caotj72 翻譯於 11個月前 0人頂 頂 翻譯得不錯哦!