How to force close TCP connection in Go

How to force close TCP connection in Go

[[425673]]

This article is reprinted from the WeChat public account "Golang Technology Sharing", the author is Jijiling Chopping Knife. Please contact the Golang Technology Sharing public account for reprinting this article.

In the article "Go Network Programming and TCP Packet Capture Practice", we wrote the Go version of the TCP server and client code, and used the tcpdump tool to capture and analyze the packets. In this example, the client code initiated a request to close the TCP connection by calling the Conn.Close() method, which is the default way to close the connection.

The default closing requires four handshake confirmation processes, which is a way of "negotiation", while TCP provides us with another "forced" closing mode.

How to force shutdown? How to implement it in Go code? This is what this article discusses.

Default off

I believe every programmer knows the four-wave process of TCP disconnection, which is the most common part of the interview essay. When we call the default Conn.Close() method in Go code, it is a typical four-wave process.

Take the client actively closing the connection as an example. After calling the Close function, it will send a FIN message to the server. If there is no data in the server's local socket receive buffer, the server's read will get an EOF error.

The party initiating the closing will experience the state changes of FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE. These states need to be updated with feedback from the party being closed.

Force Close

The default closing method, whether it is initiated by the client or the server, requires the other party's response to finally achieve the real closing of the connection. So is it possible to end the connection without caring whether the other party agrees when initiating the closing?

The answer is yes. The TCP protocol provides us with a RST flag. When one party of the connection thinks that the connection is abnormal, it can send a RST packet and immediately close the connection without waiting for the ACK confirmation from the closed party.

SetLinger() method

In Go, we can do this through the net.TCPConn.SetLinger() method.

  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 {}

The function comments are very clear, but readers are required to have the concept of socket buffer.

  • Socket Buffer

When the application layer code performs read and write operations through the socket, it actually passes through a layer of socket buffer, which is divided into a send buffer and a receive buffer.

Buffer information can be viewed by executing the netstat -nt command

  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

Among them, Recv-Q represents the receive buffer, and Send-Q represents the send buffer.

In the default closing mode, sec < 0, the operating system will process all the unprocessed data in the buffer and then close the connection.

When sec > 0, the operating system will run in the default shutdown mode. However, if the data in the buffer has not been processed after the defined time sec, the unfinished traffic in the buffer may be discarded in some operating systems.

When sec == 0, the operating system will directly discard the traffic data in the buffer, which is a forced shutdown.

Sample code and packet capture analysis

We will learn the usage of SetLinger() through sample code and use it to analyze the difference between forced closing and forced closing.

Server code

Example of actively closing the connection with the server

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

The server code is divided into four parts according to logic

Part 1: Port listening. We open a TCP connection listener on port 8000 via net.Listen("tcp", ":8000").

Part 2: Establish a connection. After successfully opening the listener, call the net.Listener.Accept() method to wait for the TCP connection. The Accept method will wait for the new connection to arrive in a blocking manner and return the connection as a net.Conn interface type.

Part 3: Data transmission. When the connection is established successfully, we will start a new goroutine to handle the reading and writing on the c connection. The data processing logic of the server in this article is that the client writes all the content of the TCP connection, and the server will write back the same content intact.

Part 4: Forced connection closing logic. Start a new goroutine, set the forced closing option through c.(*net.TCPConn).SetLinger(0), and close the connection after 10 seconds.

Client code

Example of passively closing the connection with the client

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

The client code is divided into three parts according to logic

Part 1: Establishing a connection. We connect a TCP connection via net.Dial("tcp", "localhost:8000") to the same localhost:8000 address that the server is listening on.

Part 2: Write data. When the connection is established successfully, write the data Hi, gopher? to the server through the c.Write() method.

Part 3: Reading data. Unless an error occurs, the client loops through the c.Read() method (remember, it is blocking) to read the content on the TCP connection.

Tcpdump packet capture results

Tcpdump is a very useful data packet capture tool. Its command options have been briefly introduced in the article "Go Network Programming and TCP Packet Capture Practice", so I will not repeat them here.

  • Enable tcpdump packet monitoring
  1. tcpdump -S -nn -vvv -i lo0 port 8000
  • Run the server code
  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  

After the server and client establish a connection, the data Hi, gopher? is read from the client. After 10 seconds, the server forcibly closes the TCP connection, and the server code blocked in c.Read returns an error: use of closed network connection.

  • Running the client code
  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: read : connection reset by peer

After the client and server establish a connection, they send data to the server, and the server returns the same data "Hi, gopher?" After 10 seconds, the server forcibly closes the TCP connection, so the client code blocked in c.Read captures the error: connection reset by peer.

  • tcpdump packet capture results
  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

We focus on the content Flags [], where [S] represents the SYN packet, which is used to establish a connection; [P] represents the PSH packet, indicating data transmission; [R] represents the RST packet, which is used to reset the connection; and [.] represents the corresponding ACK packet. For example, [S.] represents SYN-ACK.

After understanding the meaning of these flags, we can analyze the entire process of TCP communication forced to be closed by the server.

We compare the communication process of the client normally closing with the article "Go Network Programming and TCP Packet Capture Practice"

As you can see, after setting SetLinger(0), when the active closing party calls Close(), the system kernel will directly send a RST packet to the passive closing party. This process does not require a reply from the passive closing party, and the connection is closed. The active closing party will not have the status change of FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSE in the default closing mode.

Summarize

In this article, we introduce two methods of TCP: default closing and forced closing (in fact, there is also a compromise method: SetLinger(sec > 0)), both of which are derived from the TCP protocol design.

In most scenarios, we should choose to use the default closing method, because this ensures data integrity (data in the socket buffer will not be lost).

When closed in the default way, each connection will go through a series of connection state transitions, leaving it on the operating system for a period of time, especially when the server actively closes the connection (in most application scenarios, the client should actively initiate the closing operation), which consumes server resources.

If there are a large number of or malicious connections in a short period of time, we may need to use forced closure, because using forced closure can immediately close these connections, free up resources, and ensure the availability and performance of the server.

Of course, we can also choose a compromise approach and tolerate a period of cache data processing time before closing the operation.

Here is a question for readers to think about. If we change SetLinger(0) to SetLinger(1) in the example in this article, what will the packet capture result be?

Finally, have readers ever used the forced shutdown method in their projects?

<<:  Front-end: Uniapp encapsulation network request notes

>>:  80% of the network traffic returned by Internet applications comes from it?

Recommend

How can enterprises fully leverage the potential of private 5G networks?

It may take some time for 5G to become the most a...

Understanding Ethernet Switching Technology in One Article

Labs Guide Currently, most campus networks are ne...

The development of the Internet of Things depends on technological progress

The Internet of Things is an important part of fu...

CloudCone: $17.99/year KVM-1GB/50GB/1TB/Los Angeles MC Data Center

CloudCone sent an email at the beginning of the m...

Is 5G connectivity the future of IoT?

The three major US mobile operators AT&T, T-M...

What will 6G look like in the future?

[[389986]] At the recently concluded MWC Shanghai...

Four predictions for SD-WAN in 2018

2018 will be the year of WAN transformation, as r...

Special Research and Analysis of SMTC

Author: Wang Rui, unit: Hebei Mobile Labs Guide A...