如何正確關閉TCP連接


先上結論

  1. Read方法返回EOF錯誤,表示本端感知到對端已經關閉連接(本端已接收到對端發送的FIN)。此后如果本端不調用Close方法,只釋放本端的連接對象,則連接處於非完全關閉狀態(CLOSE_WAIT)。即文件描述符發生泄漏。
  2. Write方法返回broken pipe錯誤,表示本端感知到對端已經關閉連接(本端已接收到對端發送的RST)。此后本端可不調用Close方法。連接處於完全關閉狀態。
  3. 由於golang里net.conn內部對文件描述符的所有io操作都有狀態保護,所以即使在對端或本端關閉了連接之后,依然可以任意次數調用Read、Write、Close方法。

個人認為正確、簡單、語義清晰、高效的做法:應該在Read或Write返回錯誤后調用Close。不論是主動關閉還是被動關閉,調用Close后,不應該再Read或Write,並盡快釋放net.conn對象(也可以理解為在關閉連接之前一定要確保對端不會再發數據過來,一定要處理完對端的數據后才能關閉)。

部分demo測試與分析

我的測試環境: go version go1.13.1 darwin/amd64

第三方工具: netstat和wireshark

驗證結論一

假設我們有兩個demo程序——server和client。

client主動連接上server后不做任何操作,直接關閉net.conn對象。用於模擬主動關閉端。代碼如下:

package main

import (
	"log"
	"net"
)

func main(){
	conn, err := net.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		log.Println("dial error:", err)
		return
	}
	defer conn.Close()
}

server在accept新連接后,在新連接的處理函數中調用Read方法,Read返回io.EOF后不調用Close方法,直接退出處理函數,釋放連接對象。代碼如下:

package main

import (
	"log"
	"net"
)

func main() {
	listen, err := net.Listen("tcp", "127.0.0.1:8888")
	if err != nil {
		panic(err)
	}
	defer listen.Close()

	for {
		conn, err := listen.Accept()
		if err != nil {
			panic(err)
		}
		buf := make([]byte, 1024)
		n, err := conn.Read(buf)
		log.Println(n, err)
		//conn.Close()
	}
}

啟動server后,再啟動client,server打印出0 EOF。

用netstat查看連接情況:

$netstat -an | grep 8888
  TCP    127.0.0.1:2593         127.0.0.1:8888         FIN_WAIT_2
  TCP    127.0.0.1:8888         0.0.0.0:0              LISTENING
  TCP    127.0.0.1:8888         127.0.0.1:2593         CLOSE_WAIT

client處於FIN_WAIT_2狀態,說明client發送了FIN,並收到了對應的ACK。

server處於CLOSE_WAIT狀態,說明server收到了FIN,並發送了對應的ACK。

用wireshark抓包:

再測試一遍,發現client發送了FIN,server回復了對應的ACK。但是server並沒有發送FIN。與netstat顯示的狀態相符合。

修改server代碼,在Read返回EOF后,調用conn.Close()

重新測試,再使用netstat和wireshark分析,發現server也發送了FIN,兩端都正常關閉。

驗證結論二

修改server代碼。偽代碼如下:

package main

import (
	"log"
	"net"
	"time"
)

func main() {
	listen, err := net.Listen("tcp", "127.0.0.1:8888")
	if err != nil {
		panic(err)
	}
	defer listen.Close()

	for {
		conn, err := listen.Accept()
		if err != nil {
			panic(err)
		}
		buf := make([]byte, 1024)
		time.Sleep(5 * time.Second)
		n, err := conn.Write(buf)
		log.Println(n, err)
		time.Sleep(5 * time.Second)
		n, err = conn.Write(buf)
		log.Println(n, err)
	}
}

server輸出如下:

2021/09/15 21:11:24 1024 <nil>
2021/09/15 21:11:29 0 write tcp 127.0.0.1:8081->127.0.0.1:14856: write: broken pipe

server的第一次Sleep 5秒是為了確保在第一次Write之前client已關閉連接,實際測試不加這個時間也可以。

用netstat觀察:

我們發現在5秒內,server處於CLOSE_WAIT狀態,client處於FIN_WAIT_2狀態。

5秒之后,兩端都進入完全關閉狀態。

用wireshark抓包:

發現5秒后,server向client發送第一次1024字節數據后,client向server回復了RST包。

10秒后,server並不會再發送第二次的1024字節數據。

server的第二次Sleep 5秒是為了確保在第一次Write之后,server接收到了RST包。如果去掉第二次的Sleep,可能出現server連續發送兩次數據給client,client回復兩次RST給server。

Server端收到RST包后,也不用再回復ACK了,直接關閉連接。

如果是服務端收到請求立馬close掉,客戶端sleep 2次往conn里write數據,第一次可以寫成功,第二次也會報"broken pipe"的錯誤。

驗證結論三

場景一

對端關閉后,本端一直Read,則一直得到EOF錯誤。

這是由於系統調用Read會一直返回0。

場景二

對端關閉后,本端一直Write,則一直得到如下錯誤:

write tcp 127.0.0.1:8081->127.0.0.1:14856: write: broken pipe

這是由於系統調用Write會一直返回EPIPE。

場景三

本端關閉后,本端繼續調用Read或Write或Close,則一直得到如下錯誤:

127.0.0.1:63482->127.0.0.1:8081: use of closed network connection
127.0.0.1:63448->127.0.0.1:8081: use of closed network connection

這是由fd_mutex.go中的mutexClosed標志決定的,當文件描述符被關閉后,該標志會被設置,之后所有io操作都會返回錯誤。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM