Go:http request cancelled 服务端感知
1.背景
今天查问题的时候,偶然发现github上一个有意思的问题,记录下来。
原问题:https://github.com/golang/go/issues/23262
2.问题
首先,我们思考一个问题:
当HTTP请求被客户端cancel(例如,TCP链路断开),HTTP服务端能否感知?
通常来说(有一定“条件”),是可以感知的。
环境版本:
$ go version
go version go1.13.6 linux/amd64
服务实现:
package main
import (
"fmt"
"net/http"
"time"
)
func Handler(w http.ResponseWriter, req *http.Request) {
go func() {
<-req.Context().Done()
fmt.Println("done")
}()
time.Sleep(10 * time.Second)
}
func main() {
http.ListenAndServe(":9080", http.HandlerFunc(Handler))
}
编译运行后,使用curl发起测试:
curl -v --trace-time http://127.0.0.1:9080/
21:42:34.943320 * About to connect() to 127.0.0.1 port 9080 (#0)
21:42:34.943526 * Trying 127.0.0.1...
21:42:34.943711 * Connected to 127.0.0.1 (127.0.0.1) port 9080 (#0)
21:42:34.943882 > GET / HTTP/1.1
21:42:34.943882 > User-Agent: curl/7.29.0
21:42:34.943882 > Host: 127.0.0.1:9080
21:42:34.943882 > Accept: */*
21:42:34.943882 >
^C(立刻Ctrl+C)
此时,服务端立刻、实时输出(done):
$ go run main.go
done
当客户端(curl)断开链路(取消请求)时,服务端是可以实时感知的:Request.Context将Done。
3.服务端是否总能感知呢?
答案:并不是。
例如:使用curl做客户端发起POST请求,并携带请求正文数据:
curl -v --trace-time http://127.0.0.1:9080/ -X POST -d '123' -H 'Content-Type: application/text'
21:46:21.723542 * About to connect() to 127.0.0.1 port 9080 (#0)
21:46:21.723743 * Trying 127.0.0.1...
21:46:21.723939 * Connected to 127.0.0.1 (127.0.0.1) port 9080 (#0)
21:46:21.724147 > POST / HTTP/1.1
21:46:21.724147 > User-Agent: curl/7.29.0
21:46:21.724147 > Host: 127.0.0.1:9080
21:46:21.724147 > Accept: */*
21:46:21.724147 > Content-Type: application/text
21:46:21.724147 > Content-Length: 3
21:46:21.724147 >
21:46:21.724829 * upload completely sent off: 3 out of 3 bytes
^C
$ go run main.go
done(在运行main.go十秒之后输出结束,而不是在curl执行Ctrl+C后即时输出)
4.原因
Go需要应用层(即Handler实现者)先消费http.Request.Body完毕,才能感知到TCP链路上的RST、EOF。
例如:
客户端一口气发送了1GB的数据到服务端,然后立刻断开链路,内核发送RST包:
服务端需读完链路上堆积的数据(在Handler中读取Request.Body完毕),才能感知Request.Context已经Done:
参考:https://github.com/golang/go/issues/23262
Go doesn’t read the Request.Body by default.Your Handler has to do that.
Only once your Handler has reached the framed EOF (framed by Content-Length or chunking), then Go’s server takes over and does the long background read waiting for the next request or the connection to die.
We don’t do the automatic read by default, because that’d waste bandwidth (not applying any backpressure by filling up TCP buffers) if the request body is unwanted.
一定要应用层消费读取链路上的数据完毕(无论是Content-Length或chunk),Go底层才可感知。
理由是防止浪费带宽。
我认为这样做是不合理的:使得应用层感知请求被取消变得复杂、增加更多不必要开销。
5.消耗请求Body版
io.Copy(ioutil.Discard, req.Body)
func Handler(w http.ResponseWriter, req *http.Request) {
io.Copy(ioutil.Discard, req.Body)
go func() {
<-req.Context().Done()
fmt.Println("done")
}()
time.Sleep(10 * time.Second)
}
或者:req.Body.Close()
func Handler(w http.ResponseWriter, req *http.Request) {
req.Body.Close()
go func() {
<-req.Context().Done()
fmt.Println("done")
}()
time.Sleep(10 * time.Second)
}
无论请求是否携带数据,当HTTP请求被取消时,服务端总能立刻感知。
6.后续
可能Go会在后续的版本优化这一实现,不需要依赖应用层的任何操作就可以感知请求被cancelled。