背景

使用net/http同时发起多个简单请求时,偶尔会出现EOFconnect: connection reset by peer的情况。明明就是一个很简单的例子,为何会出现这种情况呢?

举例

这里我为了省事,直接套用网上其他人的客户端的例子:

req, err := http.NewRequest(method, url, body)
if err != nil {

	return nil, err

}

resp, err := http.DefaultClient.Do(req)
if err != nil {
	return nil, err
}
defer resp.Body.Close()

b, err := ioutil.ReadAll(resp.Body)
if err != nil {
	return nil, err
}

return b, nil

如此简单的几行代码,在我们套上大并发以后,各种异常情况接踵而至,最常见的就是下面的几个:

  • EOF
  • connection reset by peer

那具体又是什么原因导致出现上面几种情况呢?

探讨

HTTP也是针对TCP的一个封装,那我们先来简单回归一下?
图1
上面的图向我们展示了TCP连接建立、数据通信以及连接断开的几个步骤,go得益于goroutine和channel,与其他语言的实现方式不太一样,它是直接起了两个协程,一个用于读(readLoop),一个用于写(writeLoop)。我们来看看官方描述:

from:io/io.go

// EOF is the error returned by Read when no more input is available.
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either ErrUnexpectedEOF or some other error
// giving more detail.
var EOF = errors.New("EOF")

读写细节

conn.Read

  • socket无数据:read阻塞,直到有数据。

  • socket有部分数据:如果socket中有部分数据,且长度小于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回,而不是等待所有期望数据全部读取后再返回。

  • socket有足够数据:如果socket中有数据,且长度大于等于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回。这个情景是最符合我们对Read的期待的了:Read将用Socket中的数据将我们传入的slice填满后返回:n = 10, err = nil

  • 有数据,socket关闭:第一次Read成功读出了所有的数据,当第二次Read时,由于client端 socket关闭,Read返回EOF error;

  • 无数据,socket关闭:Read直接返回EOF error

    conn.Write

  • 成功写:Write调用返回的n与预期要写入的数据长度相等,且error = nil;

  • 写阻塞:当发送方将对方的接收缓冲区以及自身的发送缓冲区写满后,Write就会阻塞;

  • 写入部分数据:Write操作存在写入部分数据的情况,没有按照预期的写入所有数据,则需要循环写入。

    开方子

    通过上面的描述,我们大致已经明白了为何会出现EOF了,其实就是readLoop在进行读的时候,检测到socket被关闭了。在HTTP1.1,我们默认都是采用了Keep-Alive的,也就是会启动长连接,当server端断掉了该socket后,我们的EOF就出来了。所以尽量避免该情况发生的话,直接这样req.Close = true

来源:https://dpjeep.com/2019/06/10/golangzhi-http-eofxiang-jie/