序
这篇文章详细介绍了,net/http
包中对应HTTP
的各个阶段,如何使用timeout
来进行读/写超时控制以及服务端和客户端支持设置的timeout
类型。本质上,这些timeout
都是代码层面对各个函数设置的处理时间。好比,读取客户端读取请求头、读取响应体的时间,本质上都是响应函数的超时时间。git
做者强烈不建议,在工做中使用net/http
包上层封装的默认方法(没有明确设置timeout
),很容易出现系统文件套接字被耗尽等使人悲伤的状况。好比:github
// 相信工做中也不会出现这样的代码 func main() { http.ListenAndServe("127.0.0.1:3900", nil) }
在使用Go
开发HTTP Server
或client
的过程当中,指定timeout
很常见,但也很容易犯错。timeout
错误通常还不容易被发现,可能只有当系统出现请求超时、服务挂起时,错误才被严肃暴露出来。golang
HTTP
是一个复杂的多阶段协议,因此也不存在一个timeout
值适用于全部场景。想一下StreamingEndpoint
、JSON API
、 Comet
, 不少状况下,默认值根本不是咱们所须要的值。api
这篇博客中,我会对HTTP
请求的各个阶段进行拆分,列举可能须要设置的timeout
值。而后从客户端和服务端的角度,分析它们设置timeout
的不一样方式。缓存
SetDeadline
首先,你须要知道Go
所暴露出来的,用于实现timeout
的方法:Deadline
。网络
timeout
自己经过 net.Conn
包中的Set[Read|Write]Deadline(time.Time)
方法来控制。Deadline
是一个绝对的时间点,当链接的I/O
操做超过这个时间点而没有完成时,便会由于超时失败。app
Deadlines
不一样于timeouts
. 对一个链接而言,设置Deadline
以后,除非你从新调用SetDeadline
,不然这个Deadline
不会变化。前面也提了,Deadline
是一个绝对的时间点。所以,若是要经过SetDeadline
来设置timeout
,就不得不在每次执行Read
/Write
前从新调用它。tcp
你可能并不想直接调用SetDeadline
方法,而是选择 net/http
提供的更上层的方法。但你要时刻记住:全部timeout
操做都是经过设置Deadline
实现的。每次调用,它们并不会去重置的deadline
。ide
Server Timeouts
关于服务端超时,这篇帖子So you want to expose Go on the Internet
也介绍了不少信息,特别是关于HTTP/2
和Go 1.7 bugs
的部分.函数
对于服务端而言,指定timeout
相当重要。不然,一些请求很慢或消失的客户端极可能致使系统文件描述符
泄漏,最终服务端报错:
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
开始,到request body
被彻底读取结束(若是读取body
的话,不然是读取完header
头的时间)。内部是net/http
经过在Accept
后调用SetReadDeadline
实现的。
WriteTimeout
通常指从读取完header
头以后到写完response
的时间(又称ServerHTTP
的处理时间),内部经过在 readRequest
以后调用SetWriteDeadline
实现。
然而,若是是HTTPS
的话,SetWriteDeadline
方法在Accept
后就被调用,因此TLS handshake
也是WriteTimeout
的一部分。同时,这也意味着(仅仅HTTPS
)WriteTimeout
包括了读header
头以及握手的时间。
为了不不信任的client
端或者网络链接的影响,你应该同时设置这两个值,来保证链接不被client
长时间占用。
最后,介绍一下http.TimeoutHandler
,它并非一个Server
属性,它被用来Wrap http.Handler
,限制Handler
处理请求的时长。它主要依赖缓存的response
来工做,当超时发生时,响应503 Service Unavailable
的错误。它在1.6存在问题,在1.6.2进行了修复。
http.ListenAndServe
is doing it wrong顺带说一句,这也意味:使用一些内部封装http.Server
的包函数,好比http.ListenAndServe
, http.ListenAndServeTLS
以及http.Serve
是不正规的,尤为是直接面向外网提供服务的场合。
这种方法默认缺省配置timeout
值,也没有提供配置timeout
的功能。若是你使用它们,可能就会面临链接泄漏和文件描述符耗尽的风险。我也好几回犯过这样的错误。
相反的,建立一个http.Server
应该像文章开头例子中那样,明确设置ReadTimeout
和WriteTimeout
,并使用相应的方法来使server
更完善。
About streaming
Very annoyingly, there is no way of accessing the underlying net.Conn
from ServeHTTP
so a server that intends to stream a response is forced to unset the WriteTimeout
(which is also possibly why they are 0 by default). This is because without net.Conn
access, there is no way of calling SetWriteDeadline
before each Write
to implement a proper idle (not absolute) timeout.
Also, there's no way to cancel a blocked ResponseWriter.Write
since ResponseWriter.Close
(which you can access via an interface upgrade) is not documented to unblock a concurrent Write. So there's no way to build a timeout manually with a Timer, either.
Sadly, this means that streaming servers can't really defend themselves from a slow-reading client.
I submitted an issue with some proposals, and I welcome feedback there.
Client Timeouts
client
端的timeout
能够很简单,也能够很复杂,这彻底取决于你如何使用。但对于阻止内存泄漏或长时间链接占用的问题上,相对于Server
端来讲,它一样特别重要。
下面是使用http.Client
指定timeout
的最简单例子。timeout
覆盖了整个请求的时间:从Dial
(若是非链接重用)到读取response body
。
c := &http.Client{ Timeout: 15 * time.Second, } resp, err := c.Get("https://blog.filippo.io/")
像上面列举的那些server
端方法同样,client
端也封装了相似的方法,好比http.Get
。他内部用的就是一个没有设置超时时间的Client。
下面提供了不少类型的timeout
,可让你更精细的控制超时:
net.Dialer.Timeout
用于限制创建TCP
链接的时间,包括域名解析的时间在内(若是须要建立的话)http.Transport.TLSHandshakeTimeout
用于限制TLS
握手的时间http.Transport.ResponseHeaderTimeout
用于限制读取响应头的时间(不包括读取response body
的时间)http.Transport.ExpectContinueTimeout
用于限制从客户端在发送包含Expect: 100-continue请求头开始,到接收到响应去继续发送post data
的间隔时间。注意:在1.6
中 HTTP/2 不支持这个设置(DefaultTransport
从1.6.2起是一个例外 1.6.2).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
,用于限制链接池中空闲连持的存活时间。它不能用于控制阻塞阶段的客户端请求,
注:客户端默认执行请求重定向(302等)。能够为每一个请求指定细粒度的超时时间,其中http.Client.Timeout
包括了重定向在内的请求花费的所有时间。而http.Transport
是一个底层对象,没有跳转的概念。
Cancel and Context
net/http
提供了两种取消客户端请求的方法: Request.Cancel
以及在1.7
版本中引入的Context
.
Request.Cancel
是一个可选的channel
,若是设置了它,即可以经过关闭该channel
来终止请求,就跟请求超时了同样(它们的实现机制是相同的。在写这篇博客的时候,我还发现了一个 1.7的 bug
:全部被取消请求,返回的都是timeout
超时错误)。
type Request struct { // Cancel is an optional channel whose closure indicates that the client // request should be regarded as canceled. Not all implementations of // RoundTripper may support Cancel. // // For server requests, this field is not applicable. // // Deprecated: Use the Context and WithContext methods // instead. If a Request's Cancel field and context are both // set, it is undefined whether Cancel is respected. Cancel <-chan struct{} }
咱们可结合Request.Cancel
和time.Timer
对timeout
进行更细的控制。好比,在咱们每次从response body
中读取数据后,延长timeout
的时间。
package main import ( "io" "io/ioutil" "log" "net/http" "time" ) func main() { //定义一个timer:5s后取消该请求,即关闭该channel 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 //执行请求,请求的时间不该该超过5s 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
设置了5s
的超时,经过后续8个循环来读取response body
的内容,这个操做至少花费了8s
的时间。每次read
的操做均设置了2s
的超时。咱们能够持续这样读,不须要考虑任何阻塞的风险。若是在2s
内没有接受到数据,io.CopyN
将会返回net/http: request canceled
。
在1.7
的版本中context
被引入到了标注库,此处是一些介绍。接下来咱们用它来替换 Request.Cancel
,实现相同的功能。
使用context
来取消一个请求,咱们须要获取一个Context
类型,以及调用context.WithCancel
返回的cancel()
方法,并经过Request.WithContext
将context
绑定到一个请求上。当咱们想取消这个请求时,只须要调用cancel()
方法(代替上述关闭channel
的作法)
//ctx是context.TODO()的子节点 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)
Contexts
有不少优,好比一个parent
(传递给context.WithCancel
的对象)被取消,那么命令会沿着传递的路径一直向下传递,直到关闭全部子context
。
博客地址:neojos welcome you
阅读原文:The complete guide to Go net/http timeouts