实战:用取消参数使 Go net/http 服务更灵活

关于超时 , 可以把开发者分为两类:一类是了解超时多么难以捉摸的人 , 另一类是正在感受超时如何难以捉摸的人 。
超时既难以捉摸 , 却又真实地存在于我们生活的由网络连接的世界中 。 在我写这篇文章的同时 , 隔壁两个同事正在用他们的智能手机打字 , 也许是在跟与他们相距万里的人聊天 。 网络使这一切变为可能 。
这里要说的是网络及其复杂性 , 作为写网络服务的我们 , 必须掌握如何高效地驾驭它们 , 并规避它们的缺陷 。
闲话少说 , 来看看超时和它们是如何影响我们的 net/http 服务的 。
服务超时 — 基本原理web 编程中 , 超时通常分为客户端和服务端超时两种 。 我之所以要研究这个主题 , 是因为我自己遇到了一个有意思的服务端超时的问题 。 这也是本文我们将要重点讨论服务侧超时的原因 。
先解释下基本术语:超时是一个时间间隔(或边界) , 用来标识在这个时间段内要完成特定的行为 。 如果在给定的时间范围内没有完成操作 , 就产生了超时 , 这个操作会被取消 。
从一个 net/http 的服务的初始化中 , 能看出一些超时的基础配置:
srv :=--tt-darkmode-color: #EF7060;">http.Server 类型的服务可以用四个不同的 timeout 来初始化:

  • ReadTimeout:读取包括请求体的整个请求的最大时长
  • WriteTimeout:写响应允许的最大时长
  • IdleTimetout:当开启了保持活动状态(keep-alive)时允许的最大空闲时间
  • ReadHeaderTimeout:允许读请求头的最大时长
对上述超时的图表展示:
实战:用取消参数使 Go net/http 服务更灵活文章插图
服务生命周期和超时
当心!不要以为这些就是你所需要的所有的超时了 。 除此之外还有很多超时 , 这些超时提供了更小的粒度控制 , 对于我们的持续运行的 HTTP 处理器不会生效 。
请听我解释 。
timeout 和 deadline如果我们查看 net/http 的源码 , 尤其是看到 `conn` 类型[1] 时 , 我们会发现conn 实际上使用了 net.Conn 连接 , net.Conn 表示底层的网络连接:
// Taken from: #L247// A conn represents the server-side of an HTTP connection.type conn struct {// server is the server on which the connection arrived.// Immutable; never nil.server *Server// * Snipped *// rwc is the underlying network connection.// This is never wrapped by other types and is the value given out// to CloseNotifier callers. It is usually of type *net.TCPConn or// *tls.Conn.rwc net.Conn// * Snipped *}换句话说 , 我们的 HTTP 请求实际上是基于 TCP 连接的 。 从类型上看 , TLS 连接是 *net.TCPConn 或 *tls.Conn。
serve 函数[2]处理每一个请求[3]时调用 readRequest 函数 。 readRequest使用我们设置的 timeout 值[4]来设置 TCP 连接的 deadline:
// Taken from: #L936// Read next request from connection.func (c *conn) readRequest(ctx context.Context) (w *response, err error) {// *Snipped*t0 := time.Now()if d := c.server.readHeaderTimeout(); d != 0 {hdrDeadline = t0.Add(d)}if d := c.server.ReadTimeout; d != 0 {wholeReqDeadline = t0.Add(d)}c.rwc.SetReadDeadline(hdrDeadline)if d := c.server.WriteTimeout; d != 0 {defer func() {c.rwc.SetWriteDeadline(time.Now().Add(d))}()}// *Snipped*}从上面的摘要中 , 我们可以知道:我们对服务设置的 timeout 值最终表现为 TCP 连接的 deadline 而不是 HTTP 超时 。
所以 , deadline 是什么?工作机制是什么?如果我们的请求耗时过长 , 它们会取消我们的连接吗?
一种简单地理解 deadline 的思路是 , 把它理解为对作用于连接上的特定的行为的发生限制的一个时间点 。 例如 , 如果我们设置了一个写的 deadline , 当过了这个 deadline 后 , 所有对这个连接的写操作都会被拒绝 。
尽管我们可以使用 deadline 来模拟超时操作 , 但我们还是不能控制处理器完成操作所需的耗时 。 deadline 作用于连接 , 因此我们的服务仅在处理器尝试访问连接的属性(如对 http.ResponseWriter 进行写操作)之后才会返回(错误)结果 。
为了实际验证上面的论述 , 我们来创建一个小的 handler , 这个 handler 完成操作所需的耗时相对于我们为服务设置的超时更长:
package mainimport ( "fmt" "io" "net/http" "time")func slowHandler(w http.ResponseWriter, req *http.Request) { time.Sleep(2 * time.Second) io.WriteString(w, "I am slow!\n")}func main() { srv := http.Server{Addr:":8888",WriteTimeout: 1 * time.Second,Handler:http.HandlerFunc(slowHandler), } if err := srv.ListenAndServe(); err != nil {fmt.Printf("Server failed: %s\n", err) }}上面的服务有一个 handler , 这个 handler 完成操作需要两秒 。 另一方面 , http.Server 的 WriteTimeout 属性设为 1 秒 。 基于服务的这些配置 , 我们猜测 handler 不能把响应写到连接 。