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)
}
}
}