golang的http客户端读写超时设置

net/http库中提供的Client中的Timeout字段是针对整个读写过程的。

也就是说如果下载或者上传一个很大的文件,Timeout字段设置为10秒,那么到10秒的时候不管你在做什么这个连接会报出Timeout

而一般而言,客户端其实需要的是每个连接的read,write timeout。这里需要设置Client中的Transport字段

Transport字段是一个接口

1
2
3
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}

它把连接池,连接的细节都封装在了里面。

所以读写超时的设置也要在这个字段中设置

1
2
3
4
5
6
7
8
type Transport struct {
Dial func(network, addr string) (net.Conn, error)
MaxIdleConns int
MaxIdleConnsPerHost int
IdleConnTimeout time.Duration
ResponseHeaderTimeout time.Duration
ExpectContinueTimeout time.Duration
}

Dial字段定义了连接的接口,如果不设置,会使用默认的net.Conn结构,也就是不带握手超时和读写超时的TCP连接

所以需要以装饰模式封装一个读写超时Conn结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type rwTimeoutConn struct {
*net.TCPConn
rwTimeout time.Duration
}

func (this *rwTimeoutConn) Read(b []byte) (int, error) {
err := this.TCPConn.SetDeadline(time.Now().Add(this.rwTimeout))
if err != nil {
return 0, err
}
return this.TCPConn.Read(b)
}

func (this *rwTimeoutConn) Write(b []byte) (int, error) {
err := this.TCPConn.SetDeadline(time.Now().Add(this.rwTimeout))
if err != nil {
return 0, err
}
return this.TCPConn.Write(b)
}

Dial := func(netw, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(netw, addr, ConnectTimeout)
if err != nil {
return nil, err
}
if RWTimeout > 0 {
return &rwTimeoutConn{
TCPConn: conn.(*net.TCPConn),
rwTimeout: RWTimeout,
}, nil
} else {
return conn, nil
}
}

这里ConnectTimeout是连接超时时间,RWTimeout是读写超时时间。这样就完成了Transport的握手以及读写超时时间设置。


我实现了一个简单的客户端goreq,是fork自别人的项目

https://github.com/tedcy/goreq

经过我的改造下支持以下功能:

1 Body字段填入结构体,设置Multipart = true,通过反射解析字段使用multipart/form-data方式进行请求

2 QueryString字段填入结构体,通过反射解析字段,使用application/x-www-form-urlencoded方式进行请求

3 设置ConnectTimeout和RWTimeout进行握手以及读写超时

第三点需要做到使用方对Transport设置无感知,就必须通过实现一个TransportManager来完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var DefaultTransportManager *TransportManager = &TransportManager{tss: make(map[string]*http.Transport)}

type TransportManager struct {
tss map[string]*http.Transport
rwlock sync.RWMutex
}

func (this *TransportManager) GetTransport(ConnectTimeout, RWTimeout, ResponseHeaderTimeout time.Duration) (ts *http.Transport){
var ok bool
timeoutName := fmt.Sprintf("%s-%s-%s",int(ConnectTimeout.Seconds()),int(RWTimeout.Seconds()),int(ResponseHeaderTimeout.Seconds()))

this.rwlock.RLock()
if ts, ok = this.tss[timeoutName];!ok {
this.rwlock.RUnlock()
this.rwlock.Lock()
ts = &http.Transport{}
...
this.tss[timeoutName] = ts
this.rwlock.Unlock()
}else {
this.rwlock.RUnlock()
}
return
}

这里为了性能考虑使用了刚学的一个双层锁定的技巧

在最外层只使用读锁,如果发现不存在此时再写锁来插入。