Go语言系列之如何复用TCP连接

1,066 阅读2分钟

问题引入

本文探讨Go语言图书《100 Go Mistakes》10.5 Not closing transient resources一节。

这一节主要讲,我们写代码时应该及时释放短暂(transient)的资源,以避免泄漏,包括磁盘和内存。

那什么是需要我们及时释放的资源呢?

作者说,那些实现了io.Closer接口的结构体,都需要我们注意。

Structs can generally implement the io.Closer interface to convey that a transient resource has to be closed.

接着作者举例 HTTP.Body

// The Client and Transport return Responses from servers once 
// the response headers have been received. The response body 
// is streamed on demand as the Body field is read. 
type Response struct { 
    Status string // e.g. "200 OK"  
...
    Body io.ReadCloser // ReadCloser当然也就实现了Closer接口嘞
... 
}

ResponseBody需要实现io.ReadCloser接口,当然也就包括io.Closer接口啦。

到这里,给我的启示就是,嗯,好吧,我会及时释放资源的,及时调用obj.Close()就行啦。

不过,没有这么简单,紧接着一段话引起了我的注意,我直接引用原文

Another essential thing to remember, there’s a different behavior depending on whether we close the body with or without having read from it: If we close the body without a read, the default HTTP transport may close the connection If we close the body following a read, the default HTTP transport won’t close the connection; hence, it may be reused

乖乖,好吧。我及时释放response.Body还不够,是否读取response.Body的内容还会影响TCP连接的复用。什么情况?作者的意思是说:

如果没有读取body内容,本次响应之后就会关闭TCP连接。否则,我们就可能复用之前的TCP连接。

多说无益,直接用wireshark抓包验证一下!

实验方法

我们实验的目的:验证『读取response.Body与否会影响TCP连接的复用』这一说法。

实验方法和步骤:开启抓包,然后请求同一url两次,根据客户端TCP端口号是否相同来判断是否复用了同一条TCP连接。

  • 如果两次HTTP Get请求,使用同一条TCP连接,那么我们会观察到就只有在第一次GET请求时才开启三次握手
  • 如果两次HTTP Get请求,没有使用同一条TCP连接,那么我们会观察到两次GET请求会分别开启三次握手。

代码准备

为此,我们准备了下面的代码,可以先浏览一遍。

package main

import (
	"fmt"
	"io"
	"net/http"
	"time"
)

func getStatus(url string) (int, error) {
	resp, err := http.Get(url)
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	return resp.StatusCode, nil
}

func main() {
	url := "https://www.baidu.com"
	// 连续进行两次Get请求
	for i := 0; i < 2; i++ {
		status, err := getStatus(url)
		fmt.Printf("status:%d, err(%+v)\n", status, err)
		time.Sleep(1 * time.Second)
	}
}

代码逻辑很简单:GET请求两次相同的url,中间相隔1s,然后获取每次响应的status

wireshark抓包准备

因为我们代码里固定了urlhttps://www.baidu.com,所以我们使用wireshark抓包时,过滤器表达式可以如下

host www.baidu.com

如下图

当我们运行代码之前,开启抓包即可!

那么就让我们进入真实的实验!

试验1:不读取resp.Body

不读取response.Body,其实我们上面的代码便是如此逻辑。开启抓包,直接运行。

我们如何判断开启了几次三次握手呢?写一条过滤包的条件即可。

tcp.flags.syn == 1 and tcp.dstport == 443

首先,tcp.flags.syn==1表示TCP设置了syn标志,表示握手请求。

其次,考虑到三次握手中的第一次握手、第二次握手都带有syn标志。所以我们新加了过滤条件tcp.dstport == 443,表示TCP报文的目的端口为443

两者条件一结合,我们能够唯一确定过滤出三次握手中的第一次握手。

让我们应用之

一下就清晰了很多,只有两个包啦。

1、两个包之间相差了1s多,这与代码中相隔1s请求相呼应。

2、两个包的Src Port并不相同。

综上,至少当我们没有读取response.Body的内容时,每次HTTP GET请求,都会进行三次握手,创建自己的TCP连接。

试验2:读取resp.Body

现在我们进行读取 response.Body的实验。代码也很简单,只增加一段代码——调用io.Copy来读取resp.Body

func getStatus(url string) (int, error) { 
    resp, err := http.Get(url) 
    if err != nil { 
        return 0, err 
    } 
    defer resp.Body.Close() 
    // 读取response.Body 
    io.Copy(io.Discard, resp.Body) 
    return resp.StatusCode, nil 
} 

开启抓包,运行代码。抓包之后,使用同样的过滤条件tcp.flags.syn == 1 and tcp.dstport == 443

我们发现,这次只进行了一次三次握手!

果然如此!

总结

  • 及时释放短暂的资源
  • HTTP响应body的读取影响TCP连接的复用,如果想要复用TCP连接以提高传输速率,就需要读取responsebody内容。

大家关于这个话题,还有什么疑问吗?

参考

  • <100 Go mistakes>