目录

SetDeadline

服务器端超时设置

客户端超时设置

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`。你可以显示地设置它们:

srv := &http.Server{  ReadTimeout: 5 * time.Second,WriteTimeout: 10 * time.Second,
}
log.Println(srv.ListenAndServe())

ReadTimeout的时间计算是从连接被接受(accept)到request body完全被读取(如果你不读取body,那么时间截止到读完header为止)。它的内部实现是在Accept立即调用SetReadDeadline方法(代码行)。

  ……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实现的(代码行)。

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))}()}……
}

但是,当连接是HTTPS的时候,SetWriteDeadline会在Accept之后立即调用(代码),所以它的时间计算也包括 TLS握手时的写的时间。 讨厌的是, 这就意味着(也只有这种情况) WriteTimeout设置的时间也包含读取Headerd到读取body第一个字节这段时间。

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.ListenAndServehttp.ListenAndServeTLS 和 http.Serve并不适合实现互联网的服务器。这些函数让超时设置默认不启用,并且你没有办法设置启用超时处理。所以如果你使用它们,你会很快发现连接泄漏,太多的文件句柄。我犯过这种错误至少五六次。

取而代之,你应该创建一个http.Server示例,设置ReadTimeoutWriteTimeout,像上面的例子中一样使用相应的方法。

关于流

令人心塞的是, 没有办法从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方法。代码例子如下:

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.Client的 Timeout字段。 它的时间计算包括从连接(Dial)到读完response body。

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起是个特例)
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,}
}

如我所讲,没有办法限制发送request的时间。读取response body (原文是读取request body,按照理解应该是读取response可以手工控制)的时间花费可以手工的通过一个time.Timer来实现, 读取发生在调用Client.Do之后(详见下一节)。

最后将一点,在Go 1.7中,增加了一个http.Transport.IdleConnTimeout, 它不控制client request的阻塞阶段,但是可以控制连接池中一个连接可以idle多长时间。

注意一个Client缺省的可以执行 redirecthttp.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.Canceltime.Timer来构建一个细粒度的超时控制,允许读取流数据的时候推迟deadline:

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 = clog.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秒的超时,但是我们至少花费8秒执行8次才能读完所欲的body,每一次设置2秒的超时。我们可以为流 API这样处理避免程序死在那里。 如果超过两秒我们没有从服务器读取到数据, io.CopyN会返回net/http: request canceled错误。

在1.7中, context包升级了,进入到标准库中。Context有很多值得学习的功能,但是对于本文介绍的内容来讲,你只需直到它可以用来替换和扔掉Request.Cancel

用Context取消请求很简单,我们只需得到一个新的Context和它的cancel()函数,这是通过context.WithCancel方法得到的,然后创建一个request并使用Request.WithContext绑定它。当我们想取消这个请求是,我们调用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)

Context好处还在于如果parent context被取消的时候(在context.WithCancel调用的时候传递进来的),子context也会取消, 命令会进行传递。

golang net/http 超时机制完全手册相关推荐

  1. golang mysql 超时_Go语言channel超时机制

    Go语言没有提供直接的超时处理机制,所谓超时可以理解为当我们上网浏览一些网站时,如果一段时间之后不作操作,就需要重新登录. 那么我们应该如何实现这一功能呢,这时就可以使用 select 来设置超时. ...

  2. 关于Hystrix超时机制和线程状态的测试观察和个人理解

    作者:未完成交响曲,资深Java工程师!目前在某一线互联网公司任职,架构师社区合伙人! 我们在使用Hystrix时,大部分情况下都是直接基于SpringCloud的相关注解来完成请求调用的.我们有个项 ...

  3. .Net Cancellable Task - APM异步超时机制扩展

    概述 .NET基于委托的APM(Asynchronous Programming Model)模式通过BeginInvoke, EndInvoke, AsyncCallback,IAsyncResul ...

  4. (golang)HTTP基本认证机制及使用gocolly登录爬取

    内网有个网页用了HTTP基本认证机制,想用gocolly爬取,不知道怎么登录,只好研究HTTP基本认证机制 参考这里:https://www.jb51.net/article/89070.htm 下面 ...

  5. 超时机制,断路器模式简介

    使用Hystrix保护应用,它是一种豪猪,他身上有很多的刺,所以他能保护自己,我们知道老外的项目,他的项目名称往往取得比较有格调,比较的生动形象,所以他可以保护这样的一个组件,起名叫Hystrix,我 ...

  6. mysql会话超时机制_mysql超时机制

    mysql每次建立一个socket连接(connect)时,这个socket都会占用一定内存.即使你关闭(close)连接时,并不是真正的关闭,而是处于睡眠(sleep)状态. 当你下次再进行连接时, ...

  7. java netty swap高_Netty 超时机制及心跳程序实现

    本文介绍了 Netty 超时机制的原理,以及如何在连接闲置时发送一个心跳来维持连接. Netty 超时机制的介绍 Netty 的超时类型 IdleState 主要分为: ALL_IDLE : 一段时间 ...

  8. Linux使用I/O复用函数的超时机制的定时器

    I/O复用超时机制 利用socket的timeout的参数,进行超时设定,这期间也可以处理其他事情.在主循环中,一定要每次都更新超时参数!!! 这是个代码实例: #include <stdio. ...

  9. golang编译之vendor机制

    原文地址:golang编译之vendor机制 Go 1.5引入了vendor 机制,但是需要手动设置环境变量 GO15VENDOREXPERIMENT= 1,Go编译器才能启用.从Go1.6起,,默认 ...

最新文章

  1. 践行科技向善,腾讯Light 把光引向厦门
  2. 图表 Echarts 3.0 简单使用
  3. Android开发中adb启动失败adb连接异常的解决办法
  4. 初识contiki(2.7版本)
  5. zabbix如何添加图表显示内容_Zabbix图表出现中文乱码解决方案
  6. C++ 标准库类型 map
  7. mysql存储过程实例_mysql存储过程案例讲解
  8. 2d的公式_旋转之二 - 三维空间中的旋转:罗德里格旋转公式
  9. unity3d 大地图接壤_架空世界地图制作指南(六)气候带
  10. 数据结构实验之排序一:一趟快排
  11. vb.net 同时给多个属性赋值_系统小技巧:充分用好文件的时间属性
  12. python冒泡循环示例_Python for循环示例
  13. Linux 中的 DTrace :BPF 进入 4.9 内核
  14. Python WXPY实现微信监控报警功能的代码
  15. 专访任玉刚:从菜鸟到资深工程师的进阶之路
  16. your cuda software stack is old.we fall back to the NIVIDIA driver for some compilation. Update your
  17. html 引入 BootCDN 上的库
  18. 华为 USG 双机热备
  19. 计算机专业应届研究生面试自我介绍,计算机专业研究生面试自我介绍
  20. Verilog-移位操作(算术右移与逻辑右移)

热门文章

  1. python常用的字串格式化选项
  2. c# 18位身份证源代码
  3. 求最大元最小元极大元极小元_极大元、最大元.ppt
  4. etcd 启动分析_grpc-go基于etcd实现服务发现机制
  5. 设计模式 — 创建型模式 — 原型模式
  6. Cloudify — Blueprints
  7. 互联网协议 — TLS — 安全四要素
  8. 分布式任务队列 Celery — 实践
  9. FOC驱动器和无刷电调的区别
  10. 数据分析之全国热门景点分析