你正在使用的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
}
4条答案
按热度按时间amrnrhlw1#
更改此行为可能会破坏应用程序。
当前行为是requestTimeout = readHeaderTimeout + readTimeout。
将其调整为与行为相匹配的文档会更安全。
s5a0g9ez2#
当前行为是 requestTimeout = readHeaderTimeout + readTimeout
@fraenkel 我认为这也是不对的...实际上,我和你一样认为这是这种情况,并尝试在我的应用程序中使用 ReadHeaderTimeout > ReadTimeout。但事实证明并非如此,这种关系显示了一种有趣的行为。如果我们修改示例以读取:
然后创建以下带有客户端有效负载的 shell 脚本:
运行它:
服务器愉快地等待最多 ReadHeaderTimeout 的时间来读取头部,但在获取它们之后,如果 ReadTimeout 已过期,它将立即在尝试读取正文时超时请求。(因此,它不会影响没有正文的 GET 请求。)在现实生活中,这种行为似乎有些无用,因此任何人都不应该设置 ReadHeaderTimeout > ReadTimeout。
我在应用程序中不小心设置了 ReadHeaderTimeout > ReadTimeout,因为 ReadHeaderTimeout 文档说:
问题是:读取截止时间被重置为什么?我假设它会被重置为基于何时停止读取头部的新截止时间。也就是说,当 ReadHeaderTimeout > ReadTimeout 时,会发生一些合理的事情。例如,将截止时间重置为
timeHeadersWereReceived + ReadTimeout
。但事实证明,它被重置为timeConnectionWasOpened + ReadTimeout
,如果已经超过了 ReadTimeout 的时间,那么在我们从头部过渡到正文时,连接将立即超时。我认为文档还可以澄清 read deadline 被重置为什么值,并明确指出这种有趣的行为作为警告。
qf9go6mv3#
对不起,我应该更清楚地表达。我认为当前的行为没有价值,因为它不符合我所期望的正确行为。
如果没有设置ReadHeaderTimeout,请求超时就是读取超时。如果设置了ReadHeaderTimeout,必须在该超时窗口内读取头信息,但请求超时是剩余的时间,即ReadHeaderTimeout + ReadTimeout之和。
就我个人而言,我更倾向于将ReadHeaderTimeout仅用于管理头部分,并允许ReadTimeout处理其余部分。但现在剩余的部分被保留了。
在我看来,上面的时间安排是正确的。头信息超时了,请求也超时了。对于GET请求来说,由于通常没有正文,这种分离的超时设置并没有太大意义。如果超时可以分开,那么就可以保证在时间X内读取到头信息,在时间Y内读取到正文,这将会很有用。
我想,如果我们看一下当前ReadHeaderTimeout的使用情况,就可以判断改变行为是否会影响人们,以及这样做是否更好。
nwo49xxi4#
@bradfitz @mikioh