Go:http request cancelled 服务端感知

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。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值