9 1
http timeout 研究

今晚看了有关http connection的超时处理的文章, 针对目前我们直接调用以下代码的时候

resp,fetch_err := http.Get(url)

当处于网络延迟或不稳定的情况下,一般会出现长时间的等待, 这样体验就会下降.

于是在网上看了几篇资料, 学习了目前主流的Go Http超时处理办法 :

gist 1 by dmichael Go语言http.Get()超时设置 by 达达

其中第一篇文章原理是自定义Dial函数 :

func TimeoutDialer(config *Config) func(net, addr string) (c net.Conn, err error) {
    return func(netw, addr string) (net.Conn, error) {
        conn, err := net.DialTimeout(netw, addr, config.ConnectTimeout)
        if err != nil {
            return nil, err
        }
        conn.SetDeadline(time.Now().Add(config.ReadWriteTimeout))
        return conn, err
    }
}

…. (略)

return &http.Client{
        Transport:&http.Transport{
            Dial: TimeoutDialer(config),
        },
    }

通过对Client的封装,实现了连接和读写的超时.

其中达达的文章是解决了文章一针对长连接情况下的一些问题 , 为了达到模拟测试的环境, 具体方法是在发送请求时加一个keep-alive头,然后每次发送请求前加个Sleep,然后在Dial回调返回自己包装过的TimeoutConn,间接的调用真实的Conn,这样就可以再每次Read和Write之前设置超时时间了。

总结以上, 解决方法基本都可以掌握七七八八了 :


httpclient.go

package httptimeout

import (
    "net/http"
    "time"
    "fmt"
)

type TimeoutTransport struct {
    http.Transport
    RoundTripTimeout time.Duration
}

type respAndErr struct {
    resp *http.Response
    err error
}

type netTimeoutError struct {
    error
}

func (ne netTimeoutError) Timeout() bool { return true }

// If you don't set RoundTrip on TimeoutTransport, this will always timeout at 0
func (t *TimeoutTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    timeout := time.After(t.RoundTripTimeout)
    resp := make(chan respAndErr, 1)

    go func() {
        r, e := t.Transport.RoundTrip(req)
        resp <- respAndErr{
            resp: r,
            err: e,
        }
    }()

    select {
    case <-timeout:// A round trip timeout has occurred.
        t.Transport.CancelRequest(req)
        return nil, netTimeoutError{
            error: fmt.Errorf("timed out after %s", t.RoundTripTimeout),
        }
    case r := <-resp: // Success!
        return r.resp, r.err
    }
}

httpclient_test.go

package httptimeout

import (
    "io"
    "io/ioutil"
    "net"
    "net/http"
    "net/http/httptest"
    "testing"
    "time"
)

func TestHttpTimeout(t *testing.T) {
    http.HandleFunc("/normal", func(w http.ResponseWriter, req *http.Request) {
        // Empirically, timeouts less than these seem to be flaky
        time.Sleep(100 * time.Millisecond)
        io.WriteString(w, "ok")
    })
    http.HandleFunc("/timeout", func(w http.ResponseWriter, req *http.Request) {
        time.Sleep(250 * time.Millisecond)
        io.WriteString(w, "ok")
    })
    ts := httptest.NewServer(http.DefaultServeMux)
    defer ts.Close()

    numDials := 0

    client := &http.Client{
        Transport: &TimeoutTransport{
            Transport: http.Transport{
                Dial: func(netw, addr string) (net.Conn, error) {
                    t.Logf("dial to %s://%s", netw, addr)
                    numDials++ // For testing only.
                    return net.Dial(netw, addr) // Regular ass dial.
                },
            },
            RoundTripTimeout: time.Millisecond * 200,
        },
    }

    addr := ts.URL

    SendTestRequest(t, client, "1st", addr, "normal")
    if numDials != 1 {
        t.Fatalf("Should only have 1 dial at this point.")
    }
    SendTestRequest(t, client, "2st", addr, "normal")
    if numDials != 1 {
        t.Fatalf("Should only have 1 dial at this point.")
    }
    SendTestRequest(t, client, "3st", addr, "timeout")
    if numDials != 1 {
        t.Fatalf("Should only have 1 dial at this point.")
    }
    SendTestRequest(t, client, "4st", addr, "normal")
    if numDials != 2 {
        t.Fatalf("Should have our 2nd dial.")
    }

    time.Sleep(time.Millisecond * 700)

    SendTestRequest(t, client, "5st", addr, "normal")
    if numDials != 2 {
        t.Fatalf("Should still only have 2 dials.")
    }
}

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

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

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

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

代码相关 : http://golang.org/pkg/net/http/httptest/