[译]Go net/http 超时机制完全手册
2017-12-08 16:53
489 查看
目录 [−]
SetDeadline服务器端超时设置
http.ListenAndServe
的错误
关于流
客户端超时设置
Cancel
和 Context
英文原始出处: The
complete guide to Go net/http timeouts, 作者: Filippo
Valsorda
当用Go写HTTP的服务器和客户端的时候,超时处理总是最易犯错和最微妙的地方之一。错误可能来自很多地方,一个错误可能等待很长时间没有结果,直到网络故障或者进程挂起。
HTTP是一个复杂的、多阶段(multi-stage)协议,所以没有一个放之四海而皆准的超时解决方案,比如一个流服务、一个JSON API和一个Comet服务对超时的需求都不相同, 往往默认值不是你想要的。
本文我将拆解需要超时设置的各个阶段,看看用什么不同的方式去处理它, 包括服务器端和客户端。
SetDeadline
首先,你需要了解Go实现超时的网络原语(primitive): Deadline (最后期限)。net.Conn为Deadline提供了多个方法
Set[Read|Write]Deadline(time.Time)。Deadline是一个绝对时间值,当到达这个时间的时候,所有的
I/O 操作都会失败,返回超时(timeout)错误。
Deadline不是超时(timeout)。一旦设置它们永久生效(或者直到下一次调用SetDeadline), 不管此时连接是否被使用和怎么用。所以如果想使用
SetDeadline建立超时机制,你不得不每次在
Read/Write操作之前调用它。
你可能不想自己调用
SetDeadline,
而是让
net/http代替你调用,所以你可以调用更高级的timeout方法。但是请记住,所有的超时的实现都是基于Deadline,
所以它们不会每次接收或者发送重新设置这个值(so they do NOT reset every time data is sent or received)。
江南雨的指正:
应该是由于“Deadline是一个绝对时间值”,不是真的超时机制,所以作者特别提醒,这个值不会自动重置的,需要每次手动设置。
服务器端超时设置
对于暴露在网上的服务器来说,为客户端连接设置超时至关重要,否则巨慢的或者隐失的客户端可能导致文件句柄无法释放,最终导致服务器出现下面的错误:
http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms
http.Server有两个设置超时的方法:
ReadTimeout和
andWriteTimeout`。你可以显示地设置它们:
12345 | srv := &http.Server{ ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second,}log.Println(srv.ListenAndServe()) |
ReadTimeout的时间计算是从连接被接受(accept)到request
body完全被读取(如果你不读取body,那么时间截止到读完header为止)。它的内部实现是在
Accept立即调用
SetReadDeadline方法(代码行)。
12345678 | …… 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))} …… |
WriteTimeout的时间计算正常是从request
header的读取结束开始,到 response write结束为止 (也就是 ServeHTTP 方法的声明周期), 它是通过在
readRequest方法结束的时候调用
SetWriteDeadline实现的(代码行)。
12345678910111213141516 | 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)) }() } ……} |
SetWriteDeadline会在
Accept之后立即调用(代码),所以它的时间计算也包括
TLS握手时的写的时间。 讨厌的是, 这就意味着(也只有这种情况)
WriteTimeout设置的时间也包含读取Headerd到读取body第一个字节这段时间。
12345678 | 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)) } …… |
最后,还有一个
http.TimeoutHandler方法。
它并不是Server参数,而是一个Handler包装函数,可以限制
ServeHTTP调用。它缓存response,
如果deadline超过了则发送 504 Gateway Timeout 错误。 注意这个功能在
1.6 中有问题,在1.6.2中改正了。
http.ListenAndServe 的错误
顺便提一句,net/http包下的封装的绕过
http.Server的函数
http.ListenAndServe,
http.ListenAndServeTLS和
http.Serve并不适合实现互联网的服务器。这些函数让超时设置默认不启用,并且你没有办法设置启用超时处理。所以如果你使用它们,你会很快发现连接泄漏,太多的文件句柄。我犯过这种错误至少五六次。
取而代之,你应该创建一个
http.Server示例,设置
ReadTimeout和
WriteTimeout,像上面的例子中一样使用相应的方法。
关于流
令人心塞的是, 没有办法从ServeHTTP中访问底层的
net.Conn,所以提供流服务强制不去设置
WriteTimeout(这也可能是为什么这些值的默认值总为0)。如果无法访问
net.Conn就不能在每次
Write的时候调用
SetWriteDeadline来实现一个正确的idle
timeout。
而且,也没有办法取消一个阻塞的
ResponseWriter.Write,因为
ResponseWriter.Close没有文档指出它可以取消一个阻塞并发写。也没有办法使用Timer创建以俄国手工的timeout
杯具就是流服务器不能对于慢读的客户端进行防护。我提交的了一个[bug](https://github.com/golang/go/issues/16100),欢迎大家反馈。
编者按: 作者此处的说法是有问题的,可以通过Hijack获取net.Conn,既然可以可以获取net.Conn,我们就可以调用它的SetWriteDeadline方法。代码例子如下:
123456789101112131415161718192021222324252627282930313233343536 | package mainimport ( "fmt" "log" "net/http")func main() { http.HandleFunc("/hijack", func(w http.ResponseWriter, r *http.Request) { hj, ok := w.(http.Hijacker) if !ok { http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError) return } conn, bufrw, err := hj.Hijack() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Don't forget to close the connection: defer conn.Close() conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) bufrw.WriteString("Now we're speaking raw TCP. Say hi: ") bufrw.Flush() s, err := bufrw.ReadString('\n') if err != nil { log.Printf("error reading string: %v", err) return } fmt.Fprintf(bufrw, "You said: %q\nBye.\n", s) bufrw.Flush() })} |
客户端超时设置
Client端的超时设置说复杂也复杂,说简单也简单,看你怎么用了,最重要的就是不要有资源泄漏的情况或者程序被卡住。
最简单的方式就是使用
http.Client的
Timeout字段。
它的时间计算包括从连接(Dial)到读完response body。
1234 | c := &http.Client{ Timeout: 15 * time.Second,}resp, err := c.Get("https://blog.filippo.io/") |
http.GET使用Client的时候也没有超时设置,所以在互联网上使用也很危险。
有一些更细粒度的超时控制:
net.Dialer.Timeout限制建立TCP连接的时间
http.Transport.TLSHandshakeTimeout限制
TLS握手的时间
http.Transport.ResponseHeaderTimeout限制读取response
header的时间
http.Transport.ExpectContinueTimeout限制client在发送包含
Expect: 100-continue的header到收到继续发送body的response之间的时间等待。注意在1.6中设置这个值会禁用HTTP/2(
DefaultTransport自1.6.2起是个特例)
1234567891011 | 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来实现,
读取发生在调用Client.Do之后(详见下一节)。
最后将一点,在Go 1.7中,增加了一个
http.Transport.IdleConnTimeout,
它不控制client request的阻塞阶段,但是可以控制连接池中一个连接可以idle多长时间。
注意一个Client缺省的可以执行 redirect。
http.Client.Timeout包含所有的
redirect,而细粒度的超时控制参数只针对单次请求有效,
因为
http.Transport是一个底层的类型,没有
redirect的概念。
Cancel 和 Context
net/http提供了两种方式取消一个client的请求:
Request.Cancel以及Go
1.7新加的
Context。
Request.Cancel是一个可选的channel,
当设置这个值并且close它的时候,request就会终止,就好像超时了一样(实际它们的实现是一样的,在写本文的时候我还发现一个1.7 的 一个bug,
所有的cancel操作返回的错误还是timeout error )。
我们可以使用
Request.Cancel和
time.Timer来构建一个细粒度的超时控制,允许读取流数据的时候推迟deadline:
123456789101112131415161718192021222324252627282930313233343536373839404142 | package mainimport ( "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) } }} |
net/http: request canceled错误。
在1.7中, context包升级了,进入到标准库中。Context有很多值得学习的功能,但是对于本文介绍的内容来讲,你只需直到它可以用来替换和扔掉
Request.Cancel。
用Context取消请求很简单,我们只需得到一个新的Context和它的cancel()函数,这是通过context.WithCancel方法得到的,然后创建一个request并使用
Request.WithContext绑定它。当我们想取消这个请求是,我们调用
cancel()取消这个Context:
12345678910 | 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调用的时候传递进来的),子context也会取消,
命令会进行传递。
好了,这就是本文要讲的全部,希望我没有超过你的阅读deadline。
相关文章推荐
- [译]Go net/http 超时机制完全手册
- Go net/http 超时机制完全手册
- Go net/http 超时指导
- Go net/http 超时指导
- 异步和同步http请求超时机制
- go-代码收集-net/http客户端长连接
- 分享:Go net/http 包 第三部分 翻译完毕
- HttpClient连接池的连接保持、超时和失效机制
- go http.Get请求 http.Post请求 http.PostForm请求 Client 超时设置
- Delphi 完全时尚手册之 Visual Style 篇 (界面不错) 转自http://blog.csdn.net/iseekcode/article/details/4733229
- (HttpClient超时机制)timeout调度算法探讨
- Android·HTTP超时机制
- 网站HTTP升级HTTPS完全配置手册
- Go语言http.Get()超时设置
- 【转】HTTP协议Content-Type不完全手册
- Go利用net/http包搭建Web服务器
- Delphi 完全时尚手册之 Visual Style 篇 (界面不错) 转自http://blog.csdn.net/iseekcode/article/details/4733229
- GO语言练习:channel select 超时机制
- Android·HTTP超时机制
- Go实战--net/http中JSON的使用(The way to go)