在《Go 网络编程和 TCP 抓包实操》一文中,我们编写了 Go 版本的 TCP 服务器与客户端代码,并通过 tcpdump 工具进行抓包获取分析。在该例中,客户端代码通过调用 Conn.Close() 方法发起了关闭 TCP 连接的请求,这是一种默认的关闭连接方式。
ASP站长网默认关闭需要四次挥手的确认过程,这是一种”商量“的方式,而 TCP 为我们提供了另外一种”强制“的关闭模式。
如何强制性关闭?具体在 Go 代码中应当怎样实现?这就是本文探讨的内容。
默认关闭
相信每个程序员都知道 TCP 断开连接的四次挥手过程,这是面试八股文中的股中股。我们在 Go 代码中调用默认的 Conn.Close() 方法,它就是典型的四次挥手。
以客户端主动关闭连接为例,当它调用 Close 函数后,就会向服务端发送 FIN 报文,如果服务器的本端 socket 接收缓存区里已经没有数据,那服务端的 read 将会得到一个 EOF 错误。
发起关闭方会经历 FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE 的状态变化,这些状态需要得到被关闭方的反馈而更新。
强制关闭
默认的关闭方式,不管是客户端还是服务端主动发起关闭,都要经过对方的应答,才能最终实现真正的关闭连接。那能不能在发起关闭时,不关心对方是否同意,就结束掉连接呢?
答案是肯定的。TCP 协议为我们提供了一个 RST 的标志位,当连接的一方认为该连接异常时,可以通过发送 RST 包并立即关闭该连接,而不用等待被关闭方的 ACK 确认。
SetLinger() 方法
在 Go 中,我们可以通过 net.TCPConn.SetLinger() 方法来实现。
// SetLinger sets the behavior of Close on a connection which still
// has data waiting to be sent or to be acknowledged.
//
// If sec < 0 (the default), the operating system finishes sending the
// data in the background.
//
// If sec == 0, the operating system discards any unsent or
// unacknowledged data.
//
// If sec > 0, the data is sent in the background as with sec < 0. On
// some operating systems after sec seconds have elapsed any remaining
// unsent data may be discarded.
func (c *TCPConn) SetLinger(sec int) error {}
函数的注释已经非常清晰,但是需要读者有 socket 缓冲区的概念。
socket 缓冲区
当应用层代码通过 socket 进行读与写的操作时,实质上经过了一层 socket 缓冲区,它分为发送缓冲区和接受缓冲区。
缓冲区信息可通过执行 netstat -nt 命令查看
$ netstat -nt
Active Internet connections
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 127.0.0.1.57721 127.0.0.1.49448 ESTABLISHED
其中,Recv-Q 代表的就是接收缓冲区,Send-Q 代表的是发送缓冲区。
默认关闭方式中,即 sec < 0 。操作系统会将缓冲区里未处理完的数据都完成处理,再关闭掉连接。
当 sec > 0 时,操作系统会以与默认关闭方式运行。但是当超过定义的时间 sec 后,如果还没处理完缓存区的数据,在某些操作系统下,缓冲区中未完成的流量可能就会被丢弃。
而 sec == 0 时,操作系统会直接丢弃掉缓冲区里的流量数据,这就是强制性关闭。
示例代码与抓包分析
我们通过示例代码来学习 SetLinger() 的使用,并以此来分析强制关闭的区别。
服务端代码
以服务端为主动关闭连接方示例
package main
import (
"log"
"net"
"time"
)
func main() {
// Part 1: create a listener
l, err := net.Listen("tcp", ":8000")
if err != nil {
log.Fatalf("Error listener returned: %s", err)
}
defer l.Close()
for {
// Part 2: accept new connection
c, err := l.Accept()
if err != nil {
log.Fatalf("Error to accept new connection: %s", err)
}
// Part 3: create a goroutine that reads and write back data
go func() {
log.Printf("TCP session open")
defer c.Close()
for {
d := make([]byte, 100)
// Read from TCP buffer
_, err := c.Read(d)
if err != nil {
log.Printf("Error reading TCP session: %s", err)
break
}
log.Printf("reading data from client: %s\n", string(d))
// write back data to TCP client
_, err = c.Write(d)
if err != nil {
log.Printf("Error writing TCP session: %s", err)
break
}
}
}()
// Part 4: create a goroutine that closes TCP session after 10 seconds
go func() {
// SetLinger(0) to force close the connection
err := c.(*net.TCPConn).SetLinger(0)
if err != nil {
log.Printf("Error when setting linger: %s", err)
}
<-time.After(time.Duration(10) * time.Second)
defer c.Close()
}()
}
}