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

原创内容,转载请注明出处

Posted by Weakyon Blog on August 31, 2017

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

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

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

Transport字段是一个接口

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

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

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

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结构体

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来完成

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
}

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

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

31 Aug 2017