文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Go 中如何强制关闭 TCP 连接

2024-12-02 20:55

关注

本文转载自微信公众号「Golang技术分享」,作者机器铃砍菜刀。转载本文请联系Golang技术分享公众号。

在《Go 网络编程和 TCP 抓包实操》一文中,我们编写了 Go 版本的 TCP 服务器与客户端代码,并通过 tcpdump 工具进行抓包获取分析。在该例中,客户端代码通过调用 Conn.Close() 方法发起了关闭 TCP 连接的请求,这是一种默认的关闭连接方式。

默认关闭需要四次挥手的确认过程,这是一种”商量“的方式,而 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() 方法来实现。

  1. // SetLinger sets the behavior of Close on a connection which still 
  2. // has data waiting to be sent or to be acknowledged. 
  3. // 
  4. // If sec < 0 (the default), the operating system finishes sending the 
  5. // data in the background. 
  6. // 
  7. // If sec == 0, the operating system discards any unsent or 
  8. // unacknowledged data. 
  9. // 
  10. // If sec > 0, the data is sent in the background as with sec < 0. On 
  11. // some operating systems after sec seconds have elapsed any remaining 
  12. // unsent data may be discarded. 
  13. func (c *TCPConn) SetLinger(sec int) error {} 

函数的注释已经非常清晰,但是需要读者有 socket 缓冲区的概念。

当应用层代码通过 socket 进行读与写的操作时,实质上经过了一层 socket 缓冲区,它分为发送缓冲区和接受缓冲区。

缓冲区信息可通过执行 netstat -nt 命令查看

  1. $ netstat -nt 
  2. Active Internet connections 
  3. Proto Recv-Q Send-Q  Local Address          Foreign Address        (state) 
  4. 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() 的使用,并以此来分析强制关闭的区别。

服务端代码

以服务端为主动关闭连接方示例

  1. package main 
  2.  
  3. import ( 
  4.  "log" 
  5.  "net" 
  6.  "time" 
  7.  
  8. func main() { 
  9.  // Part 1: create a listener 
  10.  l, err := net.Listen("tcp"":8000"
  11.  if err != nil { 
  12.   log.Fatalf("Error listener returned: %s", err) 
  13.  } 
  14.  defer l.Close() 
  15.  
  16.  for { 
  17.   // Part 2: accept new connection 
  18.   c, err := l.Accept() 
  19.   if err != nil { 
  20.    log.Fatalf("Error to accept new connection: %s", err) 
  21.   } 
  22.  
  23.   // Part 3: create a goroutine that reads and write back data 
  24.   go func() { 
  25.    log.Printf("TCP session open"
  26.    defer c.Close() 
  27.  
  28.    for { 
  29.     d := make([]byte, 100) 
  30.  
  31.     // Read from TCP buffer 
  32.     _, err := c.Read(d) 
  33.     if err != nil { 
  34.      log.Printf("Error reading TCP session: %s", err) 
  35.      break 
  36.     } 
  37.     log.Printf("reading data from client: %s\n", string(d)) 
  38.  
  39.     // write back data to TCP client 
  40.     _, err = c.Write(d) 
  41.     if err != nil { 
  42.      log.Printf("Error writing TCP session: %s", err) 
  43.      break 
  44.     } 
  45.    } 
  46.   }() 
  47.  
  48.   // Part 4: create a goroutine that closes TCP session after 10 seconds 
  49.   go func() { 
  50.    // SetLinger(0) to force close the connection 
  51.    err := c.(*net.TCPConn).SetLinger(0) 
  52.    if err != nil { 
  53.     log.Printf("Error when setting linger: %s", err) 
  54.    } 
  55.  
  56.    <-time.After(time.Duration(10) * time.Second
  57.    defer c.Close() 
  58.   }() 
  59.  } 

服务端代码根据逻辑分为四个部分

第一部分:端口监听。我们通过 net.Listen("tcp", ":8000")开启在端口 8000 的 TCP 连接监听。

第二部分:建立连接。在开启监听成功之后,调用 net.Listener.Accept()方法等待 TCP 连接。Accept 方法将以阻塞式地等待新的连接到达,并将该连接作为 net.Conn 接口类型返回。

第三部分:数据传输。当连接建立成功后,我们将启动一个新的 goroutine 来处理 c 连接上的读取和写入。本文服务器的数据处理逻辑是,客户端写入该 TCP 连接的所有内容,服务器将原封不动地写回相同的内容。

第四部分:强制关闭连接逻辑。启动一个新的 goroutine,通过 c.(*net.TCPConn).SetLinger(0) 设置强制关闭选项,并于 10 s 后关闭连接。

客户端代码

以客户端为被动关闭连接方示例

  1. package main 
  2.  
  3. import ( 
  4.  "log" 
  5.  "net" 
  6.  
  7. func main() { 
  8.  // Part 1: open a TCP session to server 
  9.  c, err := net.Dial("tcp""localhost:8000"
  10.  if err != nil { 
  11.   log.Fatalf("Error to open TCP connection: %s", err) 
  12.  } 
  13.  defer c.Close() 
  14.  
  15.  // Part2: write some data to server 
  16.  log.Printf("TCP session open"
  17.  b := []byte("Hi, gopher?"
  18.  _, err = c.Write(b) 
  19.  if err != nil { 
  20.   log.Fatalf("Error writing TCP session: %s", err) 
  21.  } 
  22.  
  23.  // Part3: read any responses until get an error 
  24.  for { 
  25.   d := make([]byte, 100) 
  26.   _, err := c.Read(d) 
  27.   if err != nil { 
  28.    log.Fatalf("Error reading TCP session: %s", err) 
  29.   } 
  30.   log.Printf("reading data from server: %s\n", string(d)) 
  31.  } 

客户端代码根据逻辑分为三个部分

第一部分:建立连接。我们通过 net.Dial("tcp", "localhost:8000")连接一个 TCP 连接到服务器正在监听的同一个 localhost:8000 地址。

第二部分:写入数据。当连接建立成功后,通过 c.Write() 方法写入数据 Hi, gopher? 给服务器。

第三部分:读取数据。除非发生 error,否则客户端通过 c.Read() 方法(记住,是阻塞式的)循环读取 TCP 连接上的内容。

tcpdump 抓包结果

tcpdump 是一个非常好用的数据抓包工具,在《Go 网络编程和 TCP 抓包实操》一文中已经简单介绍了它的命令选项,这里就不再赘述。

  1. tcpdump -S -nn -vvv -i lo0 port 8000 
  1. $ go run main.go 
  2. 2021/09/25 20:21:44 TCP session open 
  3. 2021/09/25 20:21:44 reading data from client: Hi, gopher? 
  4. 2021/09/25 20:21:54 Error reading TCP session: read tcp 127.0.0.1:8000->127.0.0.1:59394: use of closed network connection 

服务器和客户端建立连接之后,从客户端读取到数据 Hi, gopher? 。在 10s 后,服务端强制关闭了 TCP 连接,阻塞在 c.Read 的服务端代码返回了错误: use of closed network connection。

  1. $ go run main.go 
  2. 2021/09/25 20:21:44 TCP session open 
  3. 2021/09/25 20:21:44 reading data from server: Hi, gopher? 
  4. 2021/09/25 20:21:54 Error reading TCP session: read tcp 127.0.0.1:59394->127.0.0.1:8000: readconnection reset by peer 

客户端和服务器建立连接之后,发送数据给服务端,服务端返回相同的数据 Hi, gopher? 回来。在 10s 后,由于服务器强制关闭了 TCP 连接,因此阻塞在 c.Read 的客户端代码捕获到了错误:connection reset by peer。

  1. $ tcpdump -S -nn -vvv -i lo0 port 8000 
  2. tcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes 
  3. 20:21:44.682942 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!) 
  4.     127.0.0.1.59394 > 127.0.0.1.8000: Flags [S], cksum 0xfe34 (incorrect -> 0xfa62), seq 3783365585, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 725769370 ecr 0,sackOK,eol], length 0 
  5. 20:21:44.683042 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!) 
  6.     127.0.0.1.8000 > 127.0.0.1.59394: Flags [S.], cksum 0xfe34 (incorrect -> 0x23d3), seq 1050611715, ack 3783365586, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 725769370 ecr 725769370,sackOK,eol], length 0 
  7. 20:21:44.683050 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!) 
  8.     127.0.0.1.59394 > 127.0.0.1.8000: Flags [.], cksum 0xfe28 (incorrect -> 0x84dc), seq 3783365586, ack 1050611716, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 0 
  9. 20:21:44.683055 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!) 
  10.     127.0.0.1.8000 > 127.0.0.1.59394: Flags [.], cksum 0xfe28 (incorrect -> 0x84dc), seq 1050611716, ack 3783365586, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 0 
  11. 20:21:44.683302 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 63, bad cksum 0 (->3cb7)!) 
  12.     127.0.0.1.59394 > 127.0.0.1.8000: Flags [P.], cksum 0xfe33 (incorrect -> 0x93f5), seq 3783365586:3783365597, ack 1050611716, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 11 
  13. 20:21:44.683311 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!) 
  14.     127.0.0.1.8000 > 127.0.0.1.59394: Flags [.], cksum 0xfe28 (incorrect -> 0x84d1), seq 1050611716, ack 3783365597, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 0 
  15. 20:21:44.683499 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 152, bad cksum 0 (->3c5e)!) 
  16.     127.0.0.1.8000 > 127.0.0.1.59394: Flags [P.], cksum 0xfe8c (incorrect -> 0x9391), seq 1050611716:1050611816, ack 3783365597, win 6379, options [nop,nop,TS val 725769370 ecr 725769370], length 100 
  17. 20:21:44.683511 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!) 
  18.     127.0.0.1.59394 > 127.0.0.1.8000: Flags [.], cksum 0xfe28 (incorrect -> 0x846e), seq 3783365597, ack 1050611816, win 6378, options [nop,nop,TS val 725769370 ecr 725769370], length 0 
  19. 20:21:54.688350 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40, bad cksum 0 (->3cce)!) 
  20.     127.0.0.1.8000 > 127.0.0.1.59394: Flags [R.], cksum 0xfe1c (incorrect -> 0xcd39), seq 1050611816, ack 3783365597, win 6379, length 0 

我们重点关注内容 Flags [],其中 [S] 代表 SYN 包,用于建立连接;[P] 代表 PSH 包,表示有数据传输;[R]代表 RST 包,用于重置连接;[.] 代表对应的 ACK 包。例如 [S.] 代表 SYN-ACK。

搞懂了这几个 Flags 的含义,那我们就可以分析出本次服务端强制关闭的 TCP 通信全过程。

我们和《Go 网络编程和 TCP 抓包实操》一文中,客户端正常关闭的通信过程进行比较

可以看到,当通过设定 SetLinger(0) 之后,主动关闭方调用 Close() 时,系统内核会直接发送 RST 包给被动关闭方。这个过程并不需要被动关闭方的回复,就已关闭了连接。主动关闭方也就没有了默认关闭模式下 FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE 的状态改变。

总结

本文我们介绍了 TCP 默认关闭与强制关闭两种方式(其实还有种折中的方式:SetLinger(sec > 0)),它们都源于 TCP 的协议设计。

在大多数的场景中,我们都应该选择使用默认关闭方式,因为这样才能确保数据的完整性(不会丢失 socket 缓冲区里的数据)。

当使用默认方式关闭时,每个连接都会经历一系列的连接状态转变,让其在操作系统上停留一段时间。尤其是服务器要主动关闭连接时(大多数应用场景,都应该是由客户端主动发起关闭操作),这会消耗服务器的资源。

如果短时间内有大量的或者恶意的连接涌入,我们或许需要采用强制关闭方式。因为使用强制关闭,能立即关闭这些连接,释放资源,保证服务器的可用与性能。

当然,我们还可以选择折中的方式,容忍一段时间的缓存区数据处理时间,再进行关闭操作。

这里给读者朋友留一个思考题。如果在本文示例中,我们将 SetLinger(0) 改为 SetLinger(1) ,抓包结果又会是如何?

最后,读者朋友们在项目中,有使用过强制关闭方式吗?

 

来源:Golang技术分享内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯