Go語言http.Get()超時設置

http://1234n.com/?post/mwsw2r

Go語言http.Get()超時設置(更新)

Go by 達達 at 2014-02-26

上次寫了一遍日誌分享http.Get()設置超時的方案,後來自己過了一遍代碼發現邏輯上有問題。

在Dail之後設置了Deadline,之後就沒再重新設置。這對於不重用連接的http請求是沒有問題的,但是Go的http庫是支持keep-alive的,可以重用TCP/IP連接。這意味着一個連接過了超時時間後再被使用,就會出現超時錯誤,因爲沒有再重置超時時間。

拿上次的實驗代碼,在發送請求時加一個keep-alive頭,然後每次發送請求前加個Sleep,就可以重新以上情況。

怎樣做到每次使用一個連接發送和接收前就設置超時呢?我想了個辦法是在Dial回調返回自己包裝過的TimeoutConn,間接的調用真實的Conn,這樣就可以再每次Read和Write之前設置超時時間了。

以下是修改後的實驗代碼:

//
// How to set timeout for http.Get() in golang
//
package main

import (
    "io"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "sync"
    "time"
)

type TimeoutConn struct {
    conn    net.Conn
    timeout time.Duration
}

func NewTimeoutConn(conn net.Conn, timeout time.Duration) *TimeoutConn {
    return &TimeoutConn{
        conn:    conn,
        timeout: timeout,
    }
}

func (c *TimeoutConn) Read(b []byte) (n int, err error) {
    c.SetReadDeadline(time.Now().Add(c.timeout))
    return c.conn.Read(b)
}

func (c *TimeoutConn) Write(b []byte) (n int, err error) {
    c.SetWriteDeadline(time.Now().Add(c.timeout))
    return c.conn.Write(b)
}

func (c *TimeoutConn) Close() error {
    return c.conn.Close()
}

func (c *TimeoutConn) LocalAddr() net.Addr {
    return c.conn.LocalAddr()
}

func (c *TimeoutConn) RemoteAddr() net.Addr {
    return c.conn.RemoteAddr()
}

func (c *TimeoutConn) SetDeadline(t time.Time) error {
    return c.conn.SetDeadline(t)
}

func (c *TimeoutConn) SetReadDeadline(t time.Time) error {
    return c.conn.SetReadDeadline(t)
}

func (c *TimeoutConn) SetWriteDeadline(t time.Time) error {
    return c.conn.SetWriteDeadline(t)
}

func main() {
    client := &http.Client{
        Transport: &http.Transport{
            Dial: func(netw, addr string) (net.Conn, error) {
                log.Printf("dial to %s://%s", netw, addr)

                conn, err := net.DialTimeout(netw, addr, time.Second*2)

                if err != nil {
                    return nil, err
                }

                return NewTimeoutConn(conn, time.Second*2), nil
            },
            ResponseHeaderTimeout: time.Second * 2,
        },
    }

    addr := StartTestServer()

    SendTestRequest(client, "1st", addr, "normal")
    SendTestRequest(client, "2st", addr, "normal")
    SendTestRequest(client, "3st", addr, "timeout")
    SendTestRequest(client, "4st", addr, "normal")

    time.Sleep(time.Second * 3)

    SendTestRequest(client, "5st", addr, "normal")
}

func StartTestServer() string {
    listener, err := net.Listen("tcp", ":0")

    if err != nil {
        log.Fatalf("failed to listen - %s", err.Error())
    }

    wg := new(sync.WaitGroup)
    wg.Add(1)

    go func() {
        http.HandleFunc("/normal", func(w http.ResponseWriter, req *http.Request) {
            time.Sleep(1000 * time.Millisecond)
            io.WriteString(w, "ok")
        })

        http.HandleFunc("/timeout", func(w http.ResponseWriter, req *http.Request) {
            time.Sleep(2500 * time.Millisecond)
            io.WriteString(w, "ok")
        })

        wg.Done()

        err = http.Serve(listener, nil)

        if err != nil {
            log.Fatalf("failed to start HTTP server - %s", err.Error())
        }
    }()

    wg.Wait()

    log.Printf("start http server at http://%s/", listener.Addr())

    return listener.Addr().String()
}

func SendTestRequest(client *http.Client, id, addr, path string) {
    req, err := http.NewRequest("GET", "http://"+addr+"/"+path, nil)

    if err != nil {
        log.Fatalf("new request failed - %s", err)
    }

    req.Header.Add("Connection", "keep-alive")

    switch path {
    case "normal":
        if resp, err := client.Do(req); err != nil {
            log.Fatalf("%s request failed - %s", id, err)
        } else {
            result, err2 := ioutil.ReadAll(resp.Body)
            if err2 != nil {
                log.Fatalf("%s response read failed - %s", id, err2)
            }
            resp.Body.Close()
            log.Printf("%s request - %s", id, result)
        }
    case "timeout":
        if _, err := client.Do(req); err == nil {
            log.Fatalf("%s request not timeout", id)
        } else {
            log.Printf("%s request - %s", id, err)
        }
    }
}

https://my.oschina.net/astute/blog/295043

剛纔看了下,golang http 庫客戶端有5個超時設置,一個是 Client 裏面的  Timeout,一個是 Client 使用的 Transport 的 ResponseHeaderTimeout,還有三個是 Transport 內部的 Dialer 的超時設置,Timeout,KeepAlive,Deadline。

通過查看 golang 代碼發現,在執行 http 請求之前, 如果 Client.Timeout > 0,會使用 time.AfterFunc 定義一個回掉函數,超時後調用,此函數會取消正在進行中的請求。
Dialer 的 Timeout 和 Deadline 是連接超時時間,建立連接過程中使用
發送請求,接收響應 分別由兩個協程處理。發送請求後,Transport 裏的超時時間 ResponseHeaderTimeout 開始計時,因此它指的是等待響應的超時時間。

http://blog.csdn.net/lina_acm/article/details/52075566

 

Go net/http 超時指導

 202人閱讀 評論(0) 收藏 舉報
 分類:

目錄(?)[+]

當在編寫一個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)。

服務器超時

HTTP server phases

對於一個部署在Internet上的HTTP服務器來說,設置客戶端鏈接超時,是至關重要的。否則,一個超慢或已消失的客戶端,可能會泄漏文件描述符,並最終導致異常。如下所示:

http:Accepterror:accepttcp[::]:80:accept4:toomanyopenfiles;retryingin5ms

http.Server有兩個設置超時的方法:ReadTimeout和andWriteTimeout`。你可以顯式地設置它們:

 

 

1 srv:=&http.Server{

2 ReadTimeout:5*time.Second,

3 WriteTimeout:10*time.Second,

4 }

5 log.Println(srv.ListenAndServe())

 

 

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 phases

客戶端超時,取決於你的決策,可以很簡單,也可以很複雜。但同樣重要的是:要防止資源泄漏和阻塞。

最簡單的使用超時的方式是http.Client。它涵蓋整個交互過程,從發起連接到接收響應報文結束。

 

 

1 c:=&http.Client{

2 Timeout:15*time.Second,

3 }

4 resp,err:=c.Get("https://blog.filippo.io/")

 

 

與服務端情況類似,使用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 c:=&http.Client{

02 Transport:&Transport{

03 Dial:(&net.Dialer{

04 Timeout:30*time.Second,

05 KeepAlive:30*time.Second,

06 }).Dial,

07 TLSHandshakeTimeout:10*time.Second,

08 ResponseHeaderTimeout:10*time.Second,

09 ExpectContinueTimeout:1*time.Second,

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 packagemain

02  

03 import(

04 "io"

05 "io/ioutil"

06 "log"

07 "net/http"

08 "time"

09 )

10  

11 funcmain(){

12 c:=make(chanstruct{})

13 timer:=time.AfterFunc(5*time.Second,func(){

14 close(c)

15 })

16  

17 //Serve256byteseverysecond.

18 req,err:=http.NewRequest("GET","http://httpbin.org/range/2048?duration=8&chunk_size=256",nil)

19 iferr!=nil{

20 log.Fatal(err)

21 }

22 req.Cancel=c

23  

24 log.Println("Sendingrequest...")

25 resp,err:=http.DefaultClient.Do(req)

26 iferr!=nil{

27 log.Fatal(err)

28 }

29 deferresp.Body.Close()

30  

31 log.Println("Readingbody...")

32 for{

33 timer.Reset(2*time.Second)

34 //Tryinstead:timer.Reset(50*time.Millisecond)

35 _,err=io.CopyN(ioutil.Discard,resp.Body,256)

36 iferr==io.EOF{

37 break

38 }elseiferr!=nil{

39 log.Fatal(err)

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 ctx,cancel:=context.WithCancel(context.TODO())

02 timer:=time.AfterFunc(5*time.Second,func(){

03 cancel()

04 })

05  

06 req,err:=http.NewRequest("GET","http://httpbin.org/range/2048?duration=8&chunk_size=256",nil)

07 iferr!=nil{

08 log.Fatal(err)

09 }

10 req=req.WithContext(ctx)

 

 

在上下文(我們提供給context.WithCancel的)已經被撤銷的情況下,Contexts更具有優勢。我們可以向整個管道發送命令。

就這些了。希望你對ReadDeadline理解比我更深刻。

——轉自開源中國社區


http://studygolang.com/articles/2451

Golang http 超時設置方法

 2014-07-03   nulijiabei
 閱讀 4929 次   0 條評論   收藏
c := http.Client{
    Transport: &http.Transport{
        Dial: func(netw, addr string) (net.Conn, error) {
        deadline := time.Now().Add(25 * time.Second)
        c, err := net.DialTimeout(netw, addr, time.Second*20)
        if err != nil {
            return nil, err
        }
        c.SetDeadline(deadline)
        return c, nil
        },
    },
    }

c.Get("http://www.qq.com")

//////////////////////////////////////////////

// 上載請求
func NetUploadJson(addr string, buf interface{}) (*[]byte, *int, error) {
    // 將需要上傳的JSON轉爲Byte
    v, _ := json.Marshal(buf)
    // 上傳JSON數據
    req, e := http.NewRequest("POST", addr, bytes.NewReader(v))
    if e != nil {
    // 提交異常,返回錯誤
    return nil, nil, e
    }
    // Body Type
    req.Header.Set("Content-Type", "application/json")
    // 完成後斷開連接
    req.Header.Set("Connection", "close")
    // -------------------------------------------
    // 設置 TimeOut
    DefaultClient := http.Client{
    Transport: &http.Transport{
        Dial: func(netw, addr string) (net.Conn, error) {
        deadline := time.Now().Add(30 * time.Second)
        c, err := net.DialTimeout(netw, addr, time.Second*30)
        if err != nil {
            return nil, err
        }
        c.SetDeadline(deadline)
        return c, nil
        },
    },
    }
    // -------------------------------------------
    // 執行
    resp, ee := DefaultClient.Do(req)
    if ee != nil {
    // 提交異常,返回錯誤
    return nil, nil, ee
    }
    // 保證I/O正常關閉
    defer resp.Body.Close()
    // 判斷返回狀態
    if resp.StatusCode == http.StatusOK {
    // 讀取返回的數據
    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        // 讀取異常,返回錯誤
        return nil, nil, err
    }
    // 將收到的數據與狀態返回
    return &data, &resp.StatusCode, nil
    } else if resp.StatusCode != http.StatusOK {
    // 返回異常狀態
    return nil, &resp.StatusCode, nil
    }
    // 不會到這裏
    return nil, nil, nil
}

// 下載文件
func NetDownloadFile(addr string) (*[]byte, *int, *http.Header, error) {
    // 上傳JSON數據
    req, e := http.NewRequest("GET", addr, nil)
    if e != nil {
    // 返回異常
    return nil, nil, nil, e
    }
    // 完成後斷開連接
    req.Header.Set("Connection", "close")
    // -------------------------------------------
    // 設置 TimeOut
    DefaultClient := http.Client{
    Transport: &http.Transport{
        Dial: func(netw, addr string) (net.Conn, error) {
        deadline := time.Now().Add(30 * time.Second)
        c, err := net.DialTimeout(netw, addr, time.Second*30)
        if err != nil {
            return nil, err
        }
        c.SetDeadline(deadline)
        return c, nil
        },
    },
    }
    // -------------------------------------------
    // 執行
    resp, ee := DefaultClient.Do(req)
    if ee != nil {
    // 返回異常
    return nil, nil, nil, ee
    }
    // 保證I/O正常關閉
    defer resp.Body.Close()
    // 判斷請求狀態
    if resp.StatusCode == 200 {
    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        // 讀取錯誤,返回異常
        return nil, nil, nil, err
    }
    // 成功,返回數據及狀態
    return &data, &resp.StatusCode, &resp.Header, nil
    } else {
    // 失敗,返回狀態
    return nil, &resp.StatusCode, nil, nil
    }
    // 不會到這裏
    return nil, nil, nil, nil
}

本文來自:努力加貝

感謝作者:nulijiabei

查看原文:Golang http 超時設置方法



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章