目录


当用Go写HTTP的服务器和客户端的时候,超时处理总是最易犯错和最微妙的地方之一。错误可能来自很多地方,一个错误可能等待很长时间没有结果,直到网络故障或者进程挂起。

HTTP是一个复杂的、多阶段(multi-stage)协议,所以没有一个放之四海而皆准的超时解决方案,比如一个流服务、一个JSON API和一个Comet服务对超时的需求都不相同, 往往默认值不是你想要的。

本文我将拆解需要超时设置的各个阶段,看看用什么不同的方式去处理它, 包括服务器端和客户端。

SetDeadline

首先,你需要了解Go实现超时的网络原语(primitive): Deadline (最后期限)。

net.ConnSet[Read|Write]Deadline(time.Time)
SetDeadlineRead/Write
SetDeadlinenet/http

江南雨的指正:
应该是由于“Deadline是一个绝对时间值”,不是真的超时机制,所以作者特别提醒,这个值不会自动重置的,需要每次手动设置。

服务器端超时设置

对于暴露在网上的服务器来说,为客户端连接设置超时至关重要,否则巨慢的或者隐失的客户端可能导致文件句柄无法释放,最终导致服务器出现下面的错误:

http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms  
http.ServerReadTimeoutand
srv := &http.Server{  
    ReadTimeout: 5 * time.Second,
    WriteTimeout: 10 * time.Second,
}
log.Println(srv.ListenAndServe())
ReadTimeoutAcceptSetReadDeadline
  ……
  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))
}
  ……
WriteTimeoutreadRequestSetWriteDeadline
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))
		}()
	}
  ……
}
SetWriteDeadlineAcceptWriteTimeout
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.TimeoutHandlerServeHTTP

http.ListenAndServe 的错误

net/httphttp.Serverhttp.ListenAndServehttp.ListenAndServeTLShttp.Serve
http.ServerReadTimeoutWriteTimeout

关于流

ServeHTTPnet.ConnWriteTimeoutnet.ConnWriteSetWriteDeadline
ResponseWriter.WriteResponseWriter.Close

编者按: 作者此处的说法是有问题的,可以通过Hijack获取net.Conn,既然可以可以获取net.Conn,我们就可以调用它的SetWriteDeadline方法。代码例子如下:

package main
import (
	"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.ClientTimeout
c := &http.Client{  
    Timeout: 15 * time.Second,
}
resp, err := c.Get("https://blog.filippo.io/")
http.GET

有一些更细粒度的超时控制:

net.Dialer.Timeouthttp.Transport.TLSHandshakeTimeouthttp.Transport.ResponseHeaderTimeouthttp.Transport.ExpectContinueTimeoutExpect: 100-continueDefaultTransport
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
http.Transport.IdleConnTimeout
http.Client.Timeoutredirecthttp.Transportredirect

Cancel 和 Context

net/httpRequest.CancelContext
Request.Cancel
Request.Canceltime.Timer
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)
        }
    }
}
net/http: request canceled
Request.Cancel
Request.WithContextcancel()
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