net/http库中提供的Client中的Timeout字段是针对整个读写过程的。
也就是说如果下载或者上传一个很大的文件,Timeout字段设置为10秒,那么到10秒的时候不管你在做什么这个连接会报出Timeout
而一般而言,客户端其实需要的是每个连接的read,write timeout。这里需要设置Client中的Transport字段
Transport字段是一个接口
| 12
 3
 
 | type RoundTripper interface {RoundTrip(*Request) (*Response, error)
 }
 
 | 
它把连接池,连接的细节都封装在了里面。
所以读写超时的设置也要在这个字段中设置
| 12
 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结构体
| 12
 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来完成
| 12
 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
 }
 
 | 
这里为了性能考虑使用了刚学的一个双层锁定的技巧
在最外层只使用读锁,如果发现不存在此时再写锁来插入。