Go net/http 超时指导

2016-07-31 07:48:14
2puT
原创 1393

当在编写一个Go语言的HTTP服务端或者是客户端时,超时是最容易同时也是最敏感的错误,有很多选择,一个错误可以导致很长时间没有结果,知道网络出现故障,或者进程宕掉。

HTTP是一个复杂的多阶段的协议,所以超时没有一刀切的解决方案。想想一个流的端点与JSON API端点和comet端点。事实上,默认值往往不是你想要的。

在这篇文章中,我将采取不同的阶段,你可能需要申请一个超时,并在服务器和客户端不同的方式来实现。

设置最后期限(超时)

首先,你需要理解Go提供的最初级的网络超时实现:Deadlines(最后期限)。

在Go标准库net.Conn中实现了Deadlines,通过 set[Read|Write]Deadline(time.Time)方法进行设置。Deadlines是一个绝对时间,一旦到时,将停止所有I/O操作,并产生一个超时错误。(译注:time.Time的精度是纳秒)

Deadlines本身是不会超时的。一旦被设置,将一直生效(直到再一次调SetDeadline),它并不关心在此期间链接是否存在以及如何使用。因此,你需要在每次进行读/写操作前,使用SetDeadline设定一个超时时长。

实际开发中,你并不需要直接调用SetDeadline,而是在标准库net/http中使用更高层次的超时设置。但需要注意的是,所有基于Deadlines的超时都会被执行,所以不需要在每次收/发操作前,重置超时。(译注:tcp、udp、unix-socket也是如此,参见标准库net)。

服务器超时

对于一个部署在Internet上的HTTP服务器来说,设置客户端链接超时,是至关重要的。否则,一个超慢或已消失的客户端,可能会泄漏文件描述符,并最终导致异常。如下所示:

http:Accepterror:accepttcp[::]:80:accept4:toomanyopenfiles;retryingin5ms

http.Server有两个设置超时的方法:ReadTimeout和andWriteTimeout`。你可以显式地设置它们:




1
srv:=&http.Server{


2
ReadTimeout:5*time.Second,


3
WriteTimeout:10*time.Second,


4
}


5
log.Println(srv.ListenAndServe())



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

WriteTimeout的时间计算正常是从request header的读取结束开始,到 response write结束为止 (也就是 ServeHTTP 方法的声明周期), 它是通过在readRequest方法结束的时候调用SetWriteDeadline实现的-代码。

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

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

最后,还有一个http.TimeoutHandler方法。 它并不是Server参数,而是一个Handler包装函数,可以限制ServeHTTP调用。它缓存response, 如果deadline超过了则发送504 Gateway Timeout错误。 注意这个功能在1.6 中有问题,在1.6.2中改正了。

http.ListenAndServe的问题

不幸的是, http.ListenAndServe,http.ListenAndServeTLS及http.Serveare等经由http.Server的便利函数不太适合用于对外发布网络服务。
因为这些函数默认关闭了超时设置,也无法手动设置。使用这些函数,将很快泄露连接,然后耗尽文件描述符。对于这点,我至少犯了6次以上这样的错误。
对此,你应该使用http.server!在创建http.server实例的时候,调用相应的方法指定ReadTimeout(读取超时时间)和WriteTimeout(写超时时间),在以下会有一些案例。

关于流

比较恼火的是没法从ServerHttp访问net.Conn包下的对象,所以一个服务器想要响应一个流就必须解除WriteTimeout设置(这就是为什么默认值是0的原因)。因为访问不到net.Conn包,就无法在每个Write操作之前调用SetWriteDeadline设置一个合理的闲置超时时间。

同理,由于无法确认ResponseWriter.Close支持并发写操作,所以ResponseWriter.Write可能产生的阻塞,并且是无法被取消的。

(译者注:Go 1.6.2版本中 ,接口ResponseWriter定义中是没有Close方法的,需要在接口实现中自行实现。揣测是作者在开发中实现过该方法)

令人遗憾的是,这意味着流媒体服务器面对一个低速客户端时,将无法有效保障自身的效率、稳定。

我已经提交了一些建议,并期待有所反馈。

客户端超时

客户端超时,取决于你的决策,可以很简单,也可以很复杂。但同样重要的是:要防止资源泄漏和阻塞。

最简单的使用超时的方式是http.Client。它涵盖整个交互过程,从发起连接到接收响应报文结束。




1
c:=&http.Client{


2
Timeout:15*time.Second,


3
}


4
resp,err:=c.Get("https://blog.filippo.io/")



与服务端情况类似,使用http.Get等包级易用函数创建客户端时,也无法设置超时。应用在开放网络环境中,存在很大的风险。

还有其它一些方法,可以让你进行更精细的超时控制:

  • net.Dialer.Timeout 限制创建一个TCP连接使用的时间(如果需要一个新的链接)
  • http.Transport.TLSHandshakeTimeout 限制TLS握手使用的时间
  • http.Transport.ResponseHeaderTimeout 限制读取响应报文头使用的时间
  • http.Transport.ExpectContinueTimeout 限制客户端在发送一个包含:100-continue的http报文头后,等待收到一个go-ahead响应报文所用的时间。在1.6中,此设置对HTTP/2无效。(在1.6.2中提供了一个特定的封装DefaultTransport)




01
c:=&http.Client{


02
Transport:&Transport{


03
Dial&net.Dialer{


04
Timeout:30*time.Second,


05
KeepAlive:30*time.Second,


06
}).Dial,


07
TLSHandshakeTimeout:10*time.Second,


08
ResponseHeaderTimeout:10*time.Second,


09
ExpectContinueTimeout:1*time.Second,


10
}


如何在 Github 上发现优秀的开源项目?
发表评论
评论通过审核后显示。