Go:read一个已经被canceled的http.Request的应答

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,将得到此错误。

不要对一个已经被canceled的request的response做read。
如果read resp期间请求被cancel,理解其是TCP链路close引起错误。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值