go语言net/http超时时间详解

首先了解一个概念Deadline
net.Conn为Deadline提供了多个方法,如Set[Read|Write]Deadline(time.Time)。Deadline是一个绝对时间值,当到达这个时间的时候,对应的读或写操作会失败,返回超时(timeout)错误。

这里需要了解两点:

  • Deadline不是超时(timeout),这个值不会自动重置,需要每次手动设置。
  • 所有的超时的实现都是基于Deadline的

服务器超时设置

http.ListenAndServe的错误

顺便说一下,net/http包下的http.ListenAndServe、http.ListenAndServeTLS和http.Serve并不适合实现互联网服务器。这些函数默认不启用超时时间,并且你没办法设置启用超时处理。取而代之,你应该创建一个http.Server实例,设置ReadTimeout和WriteTimeout。

20180902153588106761054.png

使用http.Server设置服务器超时

http.Server有两个设置超时的方法: ReadTimeoutWriteTimeout

1
2
3
4
5
srv:=&http.Server{
ReadTimeout:5*time.Second,
WriteTimeout:10*time.Second
}
log.Println(srv.ListenAndServe)

ReadTimeout的时间计算是从连接被accept到request body完全被读取(如果你不读取body,时间截止到读取完header为止)。它的内部实现是在Accept立即调用SetReadDeadline方法

1
2
3
4
5
6
if d:=c.server.ReadTimout;d!=0{
c.rwc.SetReadDeadline(time.Now().Add(d))
}
if d:=c.server.WriteTimout;d!=0{
c.rwc.SetWriteDeadline(time.Now().Add(d))
}

WriteTimeout的时间计算正常是从request header的读取结束开始,到response write结束为止,它是通过在readRequest方法结束后调用SetWriteDeadline实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {
if c.hijacked() {
return nil, ErrHijacked
}
if d := c.server.ReadTimeout; d != 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
}
if d := c.server.WriteTimeout; d != 0 {
defer func() {
c.rwc.SetWriteDeadline(time.Now().Add(d))
}()
}
……
}

但是当连接时HTTPS时,SetWriteDeadline会在Accept之后立即调用,所以它的时间计算也包括TLS握手时写的时间。讨厌的是,这就意味着WriteTimeout设置的时间也包含读取Header到读取body第一个字节这段时间。

1
2
3
4
5
6
7
8
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
if d := c.server.ReadTimeout; d != 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
}
if d := c.server.WriteTimeout; d != 0 {
c.rwc.SetWriteDeadline(time.Now().Add(d))
}
……

当你处理不可信的客户端和网络时,你应该同时设置读写超时,这样客户端就不会因为读慢或者写慢长久的持有这个连接了。

客户端超时设置

20180902153588744814641.png
client设置超时时间主要是为了防止程序卡住。最简单的方式是使用http.Client的Timeout字段,它的时间计算包括从连接(dial)到读完response body

1
2
3
c:=&http.Client{
Timeout:15*time.Second,
}

http.Get无法设置超时时间,一般不直接使用

更细粒度的超时控制
net.Dialer.Timeout 限制建立TCP连接的时间
http.Transport.TLSHandshakeTimeout 限制TLS握手的时间
htp.Transport.ResponseHeaderTimeout 限制读取response header的时间
http.Transport.ExpectContinueTimeout 限制client在发送包含Expect:100-continue的header到收到继续发送body的response之间的时间等待。

1
2
3
4
5
6
7
8
9
10
11
c:=&http.Client{
Transport:&Transport{
Dial:(&net.Dialer{
Timeout:30*time.Second,
KeepAlive:30*time.Second
}).Dial,
TLSHandshakeTimeout:10*time.Second,
ResponseHeaderTimeout:10*time.Second,
ExpectContinueTimeout:1*time.Second,
}
}

注意,没有办法限制发送request的时间。
读取response body的时间花费可以手工的通过一个time.Timer来实现,读取发生在调用Client.Do之后

最后在Go1.7中,增加了一个http.Transport.IdleConnTimeout,它不控制client request的阻塞阶段,但是可以控制连接池中一个连接可以idle多长时间。

注意Client缺省的可以执行redirect.http.Client.Timeout包含所有的redirect,而细粒度的超时控制参数只针对单词请求有效,因为http.Transport是一个底层的类型,没有redirect的概念

Cancel和Context

net/http提供了两种方式取消一个client的请求: Request.Cancel和Go1.7新加的Context

Request.Cancel是一个可选的channel,当设置这个值并且close它的时候,request就会终止,就好像超时了一样

如果选用Context的话取消请求很简单,我们只需得到一个新的Context和它的cancel函数,这是通过context.WithCancel方法得到的,然后创建一个request兵使用Requst.WithContext绑定它。当我们想取消这个请求是,我们调`用cancel()去掉这个Context:

1
2
3
4
5
6
7
8
9
ctx,cancel:=context.WithCancel(context.TODO())
timer:=time.AfterFunc(5*time.Second,func(){
cancel()
})
req,err:=http.NewRequest("GET","http://xxx.com",nil)
if err!=nil{
..
}
req=req.WithContext(ctx)

Context好处还在于如果parent context被取消的时候(在context.WithCancel调用的时候传递进来的),命令可以进行传递,子context也收到取消通知。