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 }
|
这里为了性能考虑使用了刚学的一个双层锁定的技巧
在最外层只使用读锁,如果发现不存在此时再写锁来插入。