Go net http超时应用场景全面详解
作者:Go语言教程
一、前言
在Go中编写HTTP服务器或客户端时,超时是最容易出错、最微妙的事情之一:有很多选择,错误在很长一段时间内都不会产生任何后果,直到网络出现故障,进程挂起。
二、超时时间
2.1 SetDeadline
首先,您需要了解Go为实现超时而公开的网络原语:Deadlines。
由net.Conn使用Set[Read|Write]Deadline(time.time)方法公开,Deadlines是一个绝对时间,当达到该时间时,所有I/O操作都会失败并出现超时错误。
Deadlines不是超时。一旦设置,它们将永远有效(或直到下一次调用SetDeadline),无论在此期间是否以及如何使用连接。因此,要使用SetDeadline构建超时,您必须在每次读/写操作之前调用它。
您可能不想自己调用SetDeadline,而是让net/http使用其更高级别的超时为您调用它。但是,请记住所有超时都是根据Deadlines实现的,因此它们不会在每次发送或接收数据时重置。
2.2 Server Timeouts
因此你想在互联网上公开Go的帖子发现更多关于服务器超时的信息,特别是关于HTTP/2和Go 1.7错误的信息:
对于暴露在Internet上的HTTP服务器来说,强制客户端连接超时是至关重要的。否则,速度非常慢或正在消失的客户端可能会泄漏文件描述符,并最终导致以下情况:
http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms
在http.Server中公开了两个超时:ReadTimeout和WriteTimeout。您可以通过显式使用服务器来设置它们:
srv := &http.Server{ ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } log.Println(srv.ListenAndServe())
ReadTimeout涵盖了从接受连接到完全读取请求正文的时间(如果您确实读取了正文,否则到标头末尾)。它是通过在Accept之后立即调用SetReadDeadline在net/http中实现的。
WriteTimeout通常通过在readRequest结束时调用SetWriteDeadline来覆盖从请求标头读取结束到响应写入结束的时间(也称为ServeHTTP的生存期)。
但是,当连接是HTTPS时,在Accept之后会立即调用SetWriteDeadline,以便它也覆盖作为TLS握手一部分写入的数据包。令人烦恼的是,这意味着(仅在这种情况下)WriteTimeout最终包括头读取和第一个字节等待。
当你处理不受信任的客户端和/或网络时,你应该设置这两个超时,这样客户端就不会因为写或读速度慢而中断连接。
最后是http.TimeoutHandler。它不是Server参数,而是一个限制ServeHTTP调用最长持续时间的Handler包装器。它的工作方式是缓冲响应,如果超过最后期限,则发送504网关超时。请注意,它在1.6中被提出,在1.6.2中。
三、http.ListenAndServe 做的是错误的
顺便说一句,这意味着绕过http.Server的包级便利功能,如http.ListenAndServe、http.Listen AndServeTLS和http.Serve,不适合公共Internet服务器。
这些函数将Tmeouts保留为默认的off值,无法启用它们,因此如果使用它们,很快就会出现连接泄漏和文件描述符用完的情况。我至少犯过六次这个错误。
相反,使用ReadTimeout和WriteTimeout创建一个http.Server实例,并使用其相应的方法,就像上面几段中的示例一样。
3.1 streaming
非常令人恼火的是,没有办法从ServeHTTP访问底层的net.Conn,因此打算流式传输响应的服务器被迫取消设置WriteTimeout(这也可能是它们默认为0的原因)。这是因为如果没有net.Conne访问,就无法在每次写入之前调用SetWriteDeadline来实现适当的空闲(而不是绝对)超时。
此外,无法取消被阻止的ResponseWriter.Write,因为没有记录ResponseWriter.Close(您可以通过接口升级访问)来取消阻止并发写入。因此,也没有办法用Timer手动构建超时。
可悲的是,这意味着流媒体服务器无法真正保护自己免受慢速阅读客户端的攻击。
3.2 Client Timeouts
客户端超时可能更简单,也可能更复杂,这取决于您使用的超时,但对于防止资源泄漏或陷入困境同样重要。
最容易使用的是http.Client的Timeout字段。它涵盖了整个交换,从Dial(如果不重用连接)到读取主体。
c := &http.Client{ Timeout: 15 * time.Second, } resp, err := c.Get("https://blog.filippo.io/")
与上面的服务器端案例一样,包级别的函数(如http.Get请求客户端在没有超时的情况下使用,因此在开放式互联网上使用是危险的。
为了进行更精细的控制,您可以设置许多其他更具体的超时
- net.Dialer.Timeout限制建立TCP连接所花费的时间(如果需要新的连接)。
- http.Transport.TLS握手超时限制执行TLS握手所花费的时间
- http.Transport.ResponseHeaderTimeout限制读取响应标头所花费的时间。
- http.Transport.ExpectContinueTimeout限制客户端在发送包含Expect:100 continue的请求标头和接收发送正文的批准之间等待的时间。
c := &http.Client{ Transport: &http.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手动控制,因为它发生在Client方法返回之后(有关如何取消请求,请参阅下文)。
最后,1.7中新增了http.Transport.IdleConnTimeout。它不控制客户端请求的阻塞阶段,而是控制空闲连接在连接池中保持的时间。
请注意,客户端默认情况下会遵循重定向。http.Client.Timeout包括重定向后花费的所有时间,而细粒度超时是针对每个请求的,因为http.Transport是一个没有重定向概念的较低级别系统。
3.3 Cancel and Context
nethttp提供了两种取消客户端请求的方法:request.cancel和1.7中新增的Context。
Request.Cancel是一个可选通道,当设置并关闭时,会导致请求中止,就像达到Request.Timeout一样。(它们实际上是通过相同的机制实现的,在写这篇文章时,我在1.7中发现了一个错误,所有取消都会作为超时错误返回。)
我们可以使用Request.Cancel和time.Timer来构建一个更精细的超时,允许流式传输,每次我们成功从Body读取一些数据时都会将截止日期向后推:
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) } } }
在上面的例子中,我们在请求的Do阶段设置了5秒的超时,但随后读取正文,每次超时2秒。可以永远这样流媒体,而不会有陷入困境的风险。如果在超过2秒的时间内没有收到正文数据,那么io.CopyN将返回net/http:requestcancelled。
在1.7中,上下文包升级为标准库。关于上下文有很多需要学习的地方,但就目的而言,您应该知道它们会取代和弃用Request.Cancel。
要使用Contexts取消请求,我们只需获取一个新的Context及其带有Context.WithCancel的cancel()函数,并使用request.WithContext创建一个绑定到它的request。当我们想取消请求时,我们通过调用cancel来取消Context:
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)
以上就是Go net http超时应用场景全面详解的详细内容,更多关于Go net http超时的资料请关注脚本之家其它相关文章!