http://1234n.com/?post/mwsw2r
Go语言http.Get()超时设置(更新)
上次写了一遍日志分享http.Get()设置超时的方案,后来自己过了一遍代码发现逻辑上有问题。
在Dail之后设置了Deadline,之后就没再重新设置。这对于不重用连接的http请求是没有问题的,但是Go的http库是支持keep-alive的,可以重用TCP/IP连接。这意味着一个连接过了超时时间后再被使用,就会出现超时错误,因为没有再重置超时时间。
拿上次的实验代码,在发送请求时加一个keep-alive头,然后每次发送请求前加个Sleep,就可以重新以上情况。
怎样做到每次使用一个连接发送和接收前就设置超时呢?我想了个办法是在Dial回调返回自己包装过的TimeoutConn,间接的调用真实的Conn,这样就可以再每次Read和Write之前设置超时时间了。
以下是修改后的实验代码:
//
// How to set timeout for http.Get() in golang
//
package main
import (
"io"
"io/ioutil"
"log"
"net"
"net/http"
"sync"
"time"
)
type TimeoutConn struct {
conn net.Conn
timeout time.Duration
}
func NewTimeoutConn(conn net.Conn, timeout time.Duration) *TimeoutConn {
return &TimeoutConn{
conn: conn,
timeout: timeout,
}
}
func (c *TimeoutConn) Read(b []byte) (n int, err error) {
c.SetReadDeadline(time.Now().Add(c.timeout))
return c.conn.Read(b)
}
func (c *TimeoutConn) Write(b []byte) (n int, err error) {
c.SetWriteDeadline(time.Now().Add(c.timeout))
return c.conn.Write(b)
}
func (c *TimeoutConn) Close() error {
return c.conn.Close()
}
func (c *TimeoutConn) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *TimeoutConn) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func (c *TimeoutConn) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *TimeoutConn) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t)
}
func (c *TimeoutConn) SetWriteDeadline(t time.Time) error {
return c.conn.SetWriteDeadline(t)
}
func main() {
client := &http.Client{
Transport: &http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
log.Printf("dial to %s://%s", netw, addr)
conn, err := net.DialTimeout(netw, addr, time.Second*2)
if err != nil {
return nil, err
}
return NewTimeoutConn(conn, time.Second*2), nil
},
ResponseHeaderTimeout: time.Second * 2,
},
}
addr := StartTestServer()
SendTestRequest(client, "1st", addr, "normal")
SendTestRequest(client, "2st", addr, "normal")
SendTestRequest(client, "3st", addr, "timeout")
SendTestRequest(client, "4st", addr, "normal")
time.Sleep(time.Second * 3)
SendTestRequest(client, "5st", addr, "normal")
}
func StartTestServer() string {
listener, err := net.Listen("tcp", ":0")
if err != nil {
log.Fatalf("failed to listen - %s", err.Error())
}
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
http.HandleFunc("/normal", func(w http.ResponseWriter, req *http.Request) {
time.Sleep(1000 * time.Millisecond)
io.WriteString(w, "ok")
})
http.HandleFunc("/timeout", func(w http.ResponseWriter, req *http.Request) {
time.Sleep(2500 * time.Millisecond)
io.WriteString(w, "ok")
})
wg.Done()
err = http.Serve(listener, nil)
if err != nil {
log.Fatalf("failed to start HTTP server - %s", err.Error())
}
}()
wg.Wait()
log.Printf("start http server at http://%s/", listener.Addr())
return listener.Addr().String()
}
func SendTestRequest(client *http.Client, id, addr, path string) {
req, err := http.NewRequest("GET", "http://"+addr+"/"+path, nil)
if err != nil {
log.Fatalf("new request failed - %s", err)
}
req.Header.Add("Connection", "keep-alive")
switch path {
case "normal":
if resp, err := client.Do(req); err != nil {
log.Fatalf("%s request failed - %s", id, err)
} else {
result, err2 := ioutil.ReadAll(resp.Body)
if err2 != nil {
log.Fatalf("%s response read failed - %s", id, err2)
}
resp.Body.Close()
log.Printf("%s request - %s", id, result)
}
case "timeout":
if _, err := client.Do(req); err == nil {
log.Fatalf("%s request not timeout", id)
} else {
log.Printf("%s request - %s", id, err)
}
}
}
https://my.oschina.net/astute/blog/295043
http://blog.csdn.net/lina_acm/article/details/52075566
当在编写一个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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
与服务端情况类似,使用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 |
02 |
03 |
04 |
05 |
06 |
07 |
08 |
09 |
10 |
11 |
据我了解,尚没有限制发送请求使用时间的机制。目前的解决方案是,在客户端方法返回后,通过time.Timer来个手工控制读取请求信息的时间(参见下面的“如何取消请求”)。
最后,在新的1.7版本中,提供了http.Transport.IdleConnTimeout。它用于控制一个闲置连接在连接池中的保留时间,而不考虑一个客户端请求被阻塞在哪个阶段。
注意,客户端将使用默认的重定向机制。由于http.Transport是一个底层的系统机制,没有重定向概念,因此http.Client.Timeout涵盖了用于重定向花费的时间,而更精细的超时控,可以根据请求的不同,进行定制。
Cancel 和 Context
net/http提供了两种用于撤销客户端请求的方法:Request.Cancel以及新的1.7版本中提供的Context。
Request.Cancel是一个可选channel。在Request.Timeout被触发时,Request.Cancel将被设置并关闭,进而促使请求中断(基本上“撤销”都采用相同的机制,在写此文时,我发现一个1.7中的bug,所有的撤销操作,都会当作一个超时错误返回)。
我们可以使用Request.Cancel和time.Timer,来构建一个超时更可控的,可用于流媒体的客户端。它可以在成功获响应报文体(Body)的部分数据后,重置deadline。
01 |
02 |
03 |
04 |
05 |
06 |
07 |
08 |
09 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
在上面这个例子中,我们在请求阶段,设置了一个5秒钟的超时。但读取响应报文阶段,我们需要读8次,至少8秒钟的时间。每次读操作,设置2秒钟的超时。采用这样的机制,我们可以无限制的获取流媒体,而不用担心阻塞的风险。如果我们没有在2秒钟内读取到任何数据,io.CopyN将返回错误信息: net/http: request canceled。
在1.7版本标准库中的新增了context包。关于Contexts,我们有大量需要学习的东西。基于本文的主旨,你首先应该知道的是:Contexts将替代Request.Cancel,不再建议(反对)使用Request.Cancel。
为了使用Contexts来撤销一个请求,我们需要创建一个新的Context以及它的基于context.WithCancel的cancel()函数,同时还有创建一个基于Request.WithContext的Request。当我们要撤销一个请求时,我们其实际是通过cancel()函数撤销相应的Context(取代原有的关闭Cancel channel的方式):
01 |
02 |
03 |
04 |
05 |
06 |
07 |
08 |
09 |
10 |
在上下文(我们提供给context.WithCancel的)已经被撤销的情况下,Contexts更具有优势。我们可以向整个管道发送命令。
就这些了。希望你对ReadDeadline理解比我更深刻。
——转自开源中国社区
Golang http 超时设置方法
c := http.Client{
Transport: &http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
deadline := time.Now().Add(25 * time.Second)
c, err := net.DialTimeout(netw, addr, time.Second*20)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
},
},
}
c.Get("http://www.qq.com")
//
// 上载请求
func NetUploadJson(addr string, buf interface{}) (*[]byte, *int, error) {
// 将需要上传的JSON转为Byte
v, _ := json.Marshal(buf)
// 上传JSON数据
req, e := http.NewRequest("POST", addr, bytes.NewReader(v))
if e != nil {
// 提交异常,返回错误
return nil, nil, e
}
// Body Type
req.Header.Set("Content-Type", "application/json")
// 完成后断开连接
req.Header.Set("Connection", "close")
// -------------------------------------------
// 设置 TimeOut
DefaultClient := http.Client{
Transport: &http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
deadline := time.Now().Add(30 * time.Second)
c, err := net.DialTimeout(netw, addr, time.Second*30)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
},
},
}
// -------------------------------------------
// 执行
resp, ee := DefaultClient.Do(req)
if ee != nil {
// 提交异常,返回错误
return nil, nil, ee
}
// 保证I/O正常关闭
defer resp.Body.Close()
// 判断返回状态
if resp.StatusCode == http.StatusOK {
// 读取返回的数据
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
// 读取异常,返回错误
return nil, nil, err
}
// 将收到的数据与状态返回
return &data, &resp.StatusCode, nil
} else if resp.StatusCode != http.StatusOK {
// 返回异常状态
return nil, &resp.StatusCode, nil
}
// 不会到这里
return nil, nil, nil
}
// 下载文件
func NetDownloadFile(addr string) (*[]byte, *int, *http.Header, error) {
// 上传JSON数据
req, e := http.NewRequest("GET", addr, nil)
if e != nil {
// 返回异常
return nil, nil, nil, e
}
// 完成后断开连接
req.Header.Set("Connection", "close")
// -------------------------------------------
// 设置 TimeOut
DefaultClient := http.Client{
Transport: &http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
deadline := time.Now().Add(30 * time.Second)
c, err := net.DialTimeout(netw, addr, time.Second*30)
if err != nil {
return nil, err
}
c.SetDeadline(deadline)
return c, nil
},
},
}
// -------------------------------------------
// 执行
resp, ee := DefaultClient.Do(req)
if ee != nil {
// 返回异常
return nil, nil, nil, ee
}
// 保证I/O正常关闭
defer resp.Body.Close()
// 判断请求状态
if resp.StatusCode == 200 {
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
// 读取错误,返回异常
return nil, nil, nil, err
}
// 成功,返回数据及状态
return &data, &resp.StatusCode, &resp.Header, nil
} else {
// 失败,返回状态
return nil, &resp.StatusCode, nil, nil
}
// 不会到这里
return nil, nil, nil, nil
}