Go:read一个已经被canceled的http.Request的应答
1.复现
最近发现项目在处理chunk类型的http应答时,出现读数据异常报错,代码示例如下:
server
package main
import (
"bytes"
"net/http"
)
func main() {
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
// 不设置Content-Length,应答默认超过2KB时使用chunk模式
writer.Write(bytes.Repeat([]byte{'.'}, 5<<10))
})
http.ListenAndServe(":9080", nil)
}
client
package main
import (
"context"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
)
func main() {
log.Println("> create cancel-context")
ctx, cancel := context.WithCancel(context.TODO())
log.Println("> create http request(with context)")
req, err := http.NewRequestWithContext(
ctx,
"POST",
"http://127.0.0.1:9080",
strings.NewReader("{}"),
)
go func() {
time.Sleep(time.Second*1)
log.Println("> cancel context")
cancel()
}()
log.Println("> send http request")
resp, err := http.DefaultClient.Do(req)
log.Println("> recv http response")
if err != nil {
panic(err)
}
log.Println("> wait...")
time.Sleep(time.Second*2)
defer resp.Body.Close()
log.Println("> try to read http response")
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
log.Println("> read done: ", len(data))
}
client主要逻辑如下:
1.创建协程,等待1秒后主动cancel http request
2.发送请求,等待应答;
3.收到应答,休眠2秒;
4.休眠结束,读取响应;(此时,http request已经被canceled)
client执行结果:
2021/05/02 16:43:52 > create cancel-context
2021/05/02 16:43:52 > create http request(with context)
2021/05/02 16:43:52 > send http request
2021/05/02 16:43:52 > recv http response
2021/05/02 16:43:52 > wait...
2021/05/02 16:43:53 > cancel context
2021/05/02 16:43:54 > try to read http response
panic: context canceled
goroutine 1 [running]:
main.main()
C:/Users/EB/Desktop/chunk/client/main.go:46 +0x4eb
Process finished with exit code 2
client在读取应答时,出现报错:context canceled。
但是,当将server侧应答体长度缩小,例如1KB时:
func main() {
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
// 不设置Content-Length,应答默认超过2KB时使用chunk模式
writer.Write(bytes.Repeat([]byte{'.'}, 1<<10))
})
http.ListenAndServe(":9080", nil)
}
再次多次执行client,均无报错:
2021/05/02 16:47:30 > create cancel-context
2021/05/02 16:47:30 > create http request(with context)
2021/05/02 16:47:30 > send http request
2021/05/02 16:47:30 > recv http response
2021/05/02 16:47:30 > wait...
2021/05/02 16:47:31 > cancel context
2021/05/02 16:47:32 > try to read http response
2021/05/02 16:47:32 > read done: 1024
Process finished with exit code 0
是否报错,与应答数据长度有关?
抓包发现,基于chunk的应答,都会出现这个报错,难道是chunk的应答,在请求被取消后不能再读取数据?
2.原因
一般来说,chunk的数据都会比较大(多),初步猜测原因是:
client的HTTP链路的TCP接收缓冲区收到大量的数据(满),当client取消http.Request时对未读完的链路造成影响?
我们打断点到ioutil.ReadAll:
使用DEBUG模式运行client:
逐步调试,进入到bytes.Buffer的ReadFrom函数:
其中ReadFrom的参数就是http response的Body。
在ReadFrom中for循环调用http response Body的Read接口获取应答正文。
其中,http response Body实际类型是:*net/http.bodyEOFSignal
依次单步调试,最终定位到bufio.go中Reader的Read函数,读取Reader.rd(http.persistConn):
最终,在conn.Read读取TCP接收缓冲区数据时报错:
期间,在bodyEOFSignal.Read调用persitConn中,*net.OpError被转换为context canceled错误。
到此为止我们可知:
因request被canceled,导致TCP链路persistConn的状态发生变化断开,此时再Read将错误:use of closed network connection。
但是为什么服务端返回少量数据时不会有问题呢?
当非chunk模式时,将使用io.LimitedReader,此时在读到数据结束时:
我理解的数据结束是:
1.Content-Length时,已经读到指定的Content-Length字节数据;
2.chunk时,已经读到末尾0标识没有更多数据;
将直接返回不再读:
3.终
当server返回响应数据时,client的TCP接收缓冲区接收,然后解析。
当server返回响应数据比较多时,将client的TCP接收缓冲区堆满,此时需不断循环读取TCP链路上的数据然后存留在应用层。
如果在http request被cancel前,所有的应答数据都已经被收全到client的接收缓冲区中,则read一个已经被canceled的http request的response,没有影响;反之,request已经被canceled,但是还有server的响应在server的发送缓冲区、网络介质中,当client应用层cancel请求时,关闭TCP链路,client尝试从已关闭的链路中读取数据,返回错误。
所以:针对响应很长(包含应答头长度),在长时间的read期间,请求被canceled,将得到此错误。