這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
上次寫了一遍日誌分享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 mainimport ( "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) } }}