go net/http: 当ReadHeaderTimeout > ReadTimeout时,ReadTimeout不会被遵守,

4si2a6ki  于 4个月前  发布在  Go
关注(0)|答案(4)|浏览(41)

你正在使用的Go版本是什么( go version )?

$ go version
go version go1.13.4 linux/amd64

这个问题在最新的版本中是否会重现?
应该会。

你正在使用什么操作系统和处理器架构( go env )?
go env 输出

$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOENV="/root/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build860032074=/tmp/go-build -gno-record-gcc-switches"

你做了什么?
以下简单的程序运行一个带有5秒ReadTimeout和10秒ReadHeaderTimeout的Hello World HTTP服务器。

package main

import (
	"fmt"
	"net/http"
	"time"
)

func main() {
	http.HandleFunc("/", HelloServer)
	server := &http.Server{
		Addr:              ":1234",
		ReadHeaderTimeout: 10 * time.Second,
		ReadTimeout:       5 * time.Second,
	}
	server.ListenAndServe()
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
}

启动服务器。现在,通过作为客户端连接来测试超时,如下所示,其中 nc 是netcat程序。客户端发起连接,但从未发送任何包含请求头的字节。

jamesjohnston-mac:website jamesjohnston$ time nc localhost 1234

real	0m10.019s
user	0m0.005s
sys	0m0.010s

你期望看到什么?
连接应该在5秒后超时。ReadTimeout的文档说明如下:

// ReadTimeout is the maximum duration for reading the entire
	// request, including the body.

由于头部是请求的一部分,我们还期望这个超时也适用于头部。

你看到了什么?
连接在10秒后超时,而不是5秒。
注意:如果我们改为进行HTTP POST请求并包含请求体,然后在5到10秒之间发送完整的请求(在发送所有头部和正文之前刷新网络连接),那么我们会发现服务器成功接收到头部,但在尝试读取正文时立即超时。我很难看到这在现实世界中有什么用处,尤其是考虑到ServeHTTP无法调整读取超时/截止时间-即将其延长到不会立即超时的程度。(参见 #16100 )

建议的操作项
我不知道http包的实际维护者希望实现什么行为。在我看来,解决这个矛盾有两个选择:

  • 更新文档以反映实际行为。例如,更新关于ReadTimeout的注解,使其看起来像这样:
// ReadTimeout is the maximum duration for reading the entire
	// request, including the body.  As an exception, if
	// ReadHeaderTimeout > ReadTimeout, then ReadHeaderTimeout will
	// apply for reading the header portion of the request, but then the
	// request body will immediately time out when attempting to read it if
	// the ReadTimeout deadline has already elapsed.
  • 更新代码以匹配当前记录的行为。例如,将go/src/net/http/server.go中的第946行更新为:

| | t0:=time.Now() |
看起来像这样:

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)
	}
	// New: Enforce hdrDeadline <= wholeReqDeadline
	// (Not shown: logic to deal with infinite ReadHeaderTimeout and/or ReadTimeout
	if wholeReqDeadline.Before(hdrDeadline) {
		hdrDeadline = wholeReqDeadline
	}
amrnrhlw

amrnrhlw1#

更改此行为可能会破坏应用程序。
当前行为是requestTimeout = readHeaderTimeout + readTimeout。
将其调整为与行为相匹配的文档会更安全。

s5a0g9ez

s5a0g9ez2#

当前行为是 requestTimeout = readHeaderTimeout + readTimeout
@fraenkel 我认为这也是不对的...实际上,我和你一样认为这是这种情况,并尝试在我的应用程序中使用 ReadHeaderTimeout > ReadTimeout。但事实证明并非如此,这种关系显示了一种有趣的行为。如果我们修改示例以读取:

func HelloServer(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "serving endpoint %v\n", r.URL.Path[1:])
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		fmt.Fprintf(w, "while reading body: %v", err)
	} else {
		fmt.Fprintf(w, "Hello, %s!", string(body))
	}
}

然后创建以下带有客户端有效负载的 shell 脚本:

#!/bin/bash

sleep 7
echo POST /myroute HTTP/1.1
echo Host: localhost
echo Content-Length: 5
echo
sleep 0.1 # Required in order to defeat buffering and flush headers in a separate packet
echo James

运行它:

$ time ./testreq.sh | nc localhost 1234
HTTP/1.1 200 OK
Date: Sun, 17 Nov 2019 18:51:36 GMT
Content-Length: 90
Content-Type: text/plain; charset=utf-8
Connection: close

serving endpoint myroute
while reading body: read tcp [::1]:1234->[::1]:63741: i/o timeout
real	0m7.130s
user	0m0.009s
sys	0m0.015s

服务器愉快地等待最多 ReadHeaderTimeout 的时间来读取头部,但在获取它们之后,如果 ReadTimeout 已过期,它将立即在尝试读取正文时超时请求。(因此,它不会影响没有正文的 GET 请求。)在现实生活中,这种行为似乎有些无用,因此任何人都不应该设置 ReadHeaderTimeout > ReadTimeout。
我在应用程序中不小心设置了 ReadHeaderTimeout > ReadTimeout,因为 ReadHeaderTimeout 文档说:

// ReadHeaderTimeout is the amount of time allowed to read
	// request headers. The connection's read deadline is reset
	// after reading the headers and the Handler can decide what
	// is considered too slow for the body.

问题是:读取截止时间被重置为什么?我假设它会被重置为基于何时停止读取头部的新截止时间。也就是说,当 ReadHeaderTimeout > ReadTimeout 时,会发生一些合理的事情。例如,将截止时间重置为 timeHeadersWereReceived + ReadTimeout 。但事实证明,它被重置为 timeConnectionWasOpened + ReadTimeout ,如果已经超过了 ReadTimeout 的时间,那么在我们从头部过渡到正文时,连接将立即超时。
我认为文档还可以澄清 read deadline 被重置为什么值,并明确指出这种有趣的行为作为警告。

qf9go6mv

qf9go6mv3#

对不起,我应该更清楚地表达。我认为当前的行为没有价值,因为它不符合我所期望的正确行为。
如果没有设置ReadHeaderTimeout,请求超时就是读取超时。如果设置了ReadHeaderTimeout,必须在该超时窗口内读取头信息,但请求超时是剩余的时间,即ReadHeaderTimeout + ReadTimeout之和。
就我个人而言,我更倾向于将ReadHeaderTimeout仅用于管理头部分,并允许ReadTimeout处理其余部分。但现在剩余的部分被保留了。
在我看来,上面的时间安排是正确的。头信息超时了,请求也超时了。对于GET请求来说,由于通常没有正文,这种分离的超时设置并没有太大意义。如果超时可以分开,那么就可以保证在时间X内读取到头信息,在时间Y内读取到正文,这将会很有用。
我想,如果我们看一下当前ReadHeaderTimeout的使用情况,就可以判断改变行为是否会影响人们,以及这样做是否更好。

相关问题