如果我们查看 net/http 的源码,尤其是看到 `conn` 类型[1] 时,我们会发现conn 实际上使用了 net.Conn 连接,net.Conn 表示底层的网络连接:
// Taken from: https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#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: https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#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 main
import (
"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 不能把响应写到连接。
我们可以用 go run server.go 来启动服务。使用 curl localhost:8888 来发送一个请求:
$ time curl localhost:8888
curl: (52) Empty reply from server
curl localhost:8888 0.01s user 0.01s system 0% CPU 2.021 total
这个请求需要两秒来完成处理,服务返回的响应是空的。虽然我们的服务知道在 1 秒之后我们写不了响应了,但 handler 还是多耗了 100% 的时间(2 秒)来完成处理。
虽然这是个类似超时的处理,但它更大的作用是在到达超时时间时,阻止服务进行更多的操作,结束请求。在我们上面的例子中,handler 在完成之前一直在处理请求,即使已经超出响应写超时时间(1 秒)100%(耗时 2 秒)。
最根本的问题是,对于处理器来说,我们应该怎么设置超时时间才更有效?