当在编写一个Go语言的HTTP服务端或者是客户端时,超时是最容易同时也是最敏感的错误,有很多选择,一个错误可以导致很长时间没有结果,知道网络出现故障,或者进程宕掉。 HTTP是一个复杂的多阶段的协议,所以超时没有一刀切的解决方案。想想一个流的端点与JSON API端点和comet端点。事实上,默认值往往不是你想要的。 在这篇文章中,我将采取不同的阶段,你可能需要申请一个超时,并在服务器和客户端不同的方式来实现。 |
山野痞夫
|
其它翻译版本(1) |
设置最后期限(超时)首先,你需要理解Go提供的最初级的网络超时实现:Deadlines(最后期限)。 实际开发中,你并不需要直接调用SetDeadline,而是在标准库net/http中使用更高层次的超时设置。但需要注意的是,所有基于Deadlines的超时都会被执行,所以不需要在每次收/发操作前,重置超时。(译注:tcp、udp、unix-socket也是如此,参见标准库net)。 服务器超时对于一个部署在Internet上的HTTP服务器来说,设置客户端链接超时,是至关重要的。否则,一个超慢或已消失的客户端,可能会泄漏文件描述符,并最终导致异常。如下所示: http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms |
caotj72
|
http.Server有两个设置超时的方法: ReadTimeout 和 andWriteTimeout`。你可以显式地设置它们: srv := &http.Server{ ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } 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中改正了。 |
sheepbao
|
其它翻译版本(1) |
http.ListenAndServe的问题 不幸的是, http.ListenAndServe, http.ListenAndServeTLS及http.Serveare等经由http.Server的便利函数不太适合用于对外发布网络服务。 关于流比较恼火的是没法从ServerHttp访问net.Conn包下的对象,所以一个服务器想要响应一个流就必须解除WriteTimeout设置(这就是为什么默认值是0的原因)。因为访问不到net.Conn包,就无法在每个Write操作之前调用SetWriteDeadline设置一个合理的闲置超时时间。 |
imqipan
|
同理,由于无法确认ResponseWriter.Close支持并发写操作,所以ResponseWriter.Write可能产生的阻塞,并且是无法被取消的。 (译者注:Go 1.6.2版本中 ,接口ResponseWriter定义中是没有Close方法的,需要在接口实现中自行实现。揣测是作者在开发中实现过该方法) 令人遗憾的是,这意味着流媒体服务器面对一个低速客户端时,将无法有效保障自身的效率、稳定。 我已经提交了一些建议,并期待有所反馈。 客户端超时客户端超时,取决于你的决策,可以很简单,也可以很复杂。但同样重要的是:要防止资源泄漏和阻塞。 最简单的使用超时的方式是http.Client。它涵盖整个交互过程,从发起连接到接收响应报文结束。 c := &http.Client{ Timeout: 15 * time.Second, } resp, err := c.Get("https://blog.filippo.io/") 与服务端情况类似,使用http.Get等包级易用函数创建客户端时,也无法设置超时。应用在开放网络环境中,存在很大的风险。 |
caotj72
|
还有其它一些方法,可以让你进行更精细的超时控制:
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, } } 据我了解,尚没有限制发送请求使用时间的机制。目前的解决方案是,在客户端方法返回后,通过time.Timer来个手工控制读取请求信息的时间(参见下面的“如何取消请求”)。 最后,在新的1.7版本中,提供了http.Transport.IdleConnTimeout。它用于控制一个闲置连接在连接池中的保留时间,而不考虑一个客户端请求被阻塞在哪个阶段。 注意,客户端将使用默认的重定向机制。由于http.Transport是一个底层的系统机制,没有重定向概念,因此http.Client.Timeout涵盖了用于重定向花费的时间,而更精细的超时控,可以根据请求的不同,进行定制。 |
caotj72
|
Cancel 和 Contextnet/http提供了两种用于撤销客户端请求的方法:Request.Cancel以及新的1.7版本中提供的Context。 Request.Cancel是一个可选channel。在Request.Timeout被触发时,Request.Cancel将被设置并关闭,进而促使请求中断(基本上“撤销”都采用相同的机制,在写此文时,我发现一个1.7中的bug,所有的撤销操作,都会当作一个超时错误返回)。 我们可以使用Request.Cancel和time.Timer,来构建一个超时更可控的,可用于流媒体的客户端。它可以在成功获响应报文体(Body)的部分数据后,重置deadline。 package main import ( "io" "io/ioutil" "log" "net/http" "time" ) func main() { c := make(chan struct{}) timer := time.AfterFunc(5*time.Second, func() { close(c) }) // Serve 256 bytes every second. req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil) if err != nil { log.Fatal(err) } req.Cancel = c log.Println("Sending request...") resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() log.Println("Reading body...") for { timer.Reset(2 * time.Second) // Try instead: timer.Reset(50 * time.Millisecond) _, err = io.CopyN(ioutil.Discard, resp.Body, 256) if err == io.EOF { break } else if err != nil { log.Fatal(err) } } } 在上面这个例子中,我们在请求阶段,设置了一个5秒钟的超时。但读取响应报文阶段,我们需要读8次,至少8秒钟的时间。每次读操作,设置2秒钟的超时。采用这样的机制,我们可以无限制的获取流媒体,而不用担心阻塞的风险。如果我们没有在2秒钟内读取到任何数据,io.CopyN将返回错误信息: net/http: request canceled。 在1.7版本标准库中的新增了context包。关于Contexts,我们有大量需要学习的东西。基于本文的主旨,你首先应该知道的是:Contexts将替代Request.Cancel,不再建议(反对)使用Request.Cancel。 |
caotj72
|
为了使用Contexts来撤销一个请求,我们需要创建一个新的Context以及它的基于context.WithCancel的cancel()函数,同时还有创建一个基于Request.WithContext的Request。当我们要撤销一个请求时,我们其实际是通过cancel()函数撤销相应的Context(取代原有的关闭Cancel channel的方式): ctx, cancel := context.WithCancel(context.TODO()) timer := time.AfterFunc(5*time.Second, func() { cancel() }) req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil) if err != nil { log.Fatal(err) } req = req.WithContext(ctx) 在上下文(我们提供给context.WithCancel的)已经被撤销的情况下,Contexts更具有优势。我们可以向整个管道发送命令。 就这些了。希望你对ReadDeadline理解比我更深刻。 |
caotj72
|
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
有疑问加站长微信联系(非本文作者)