歡迎訪問我的個人網站獲取更佳閱讀排版 golang 網絡編程之如何正確關閉tcp連接以及管理它的生命周期 | yoko blog (https://pengrl.com/p/47401/)
本篇文章部分內容涉及到tcp協議以及socket編程的通用底層知識。討論的tcp連接對象皆為golang的net.conn對象。如果存在錯誤,請一定指正,謝謝。
先上結論
- Read方法返回EOF錯誤,表示本端感知到對端已經關閉連接(本端已接收到對端發送的FIN)。此后如果本端不調用Close方法,只釋放本端的連接對象,則連接處於非完全關閉狀態(CLOSE_WAIT)。即文件描述符發生泄漏。
- Write方法返回broken pipe錯誤,表示本端感知到對端已經關閉連接(本端已接收到對端發送的RST)。此后本端可不調用Close方法。連接處於完全關閉狀態。
- 由於golang里net.conn內部對文件描述符的所有io操作都有狀態保護,所以即使在對端或本端關閉了連接之后,依然可以任意次數調用Read、Write、Close方法。
個人認為正確、簡單、語義清晰、高效的做法:應該在Read或Write返回錯誤后調用Close。不論是主動關閉還是被動關閉,調用Close后,不應該再Read或Write,並盡快釋放net.conn對象。
部分demo測試與分析
我的測試環境: go version go1.11.4 darwin/amd64
第三方工具: netstat
和wireshark
驗證結論一
假設我們有兩個demo程序——server和client。
client主動連接上server后不做任何操作,直接關閉net.conn對象。用於模擬主動關閉端。偽代碼如下:
conn, _ := net.Dial("tcp", "127.0.0.1:8081")
conn.Close()
server在accept新連接后,在新連接的處理函數中調用Read方法,Read返回io.EOF后不調用Close方法,直接退出處理函數,釋放連接對象。偽代碼如下:
func handleConn(conn net.Conn) {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
log.Println(n, err)
//conn.Close()
}
啟動server后,再啟動client,server打印出0 EOF
。
用netstat查看連接情況:
# 在shell中輸入
$netstat -an | grep 8081
tcp4 0 0 127.0.0.1.8081 127.0.0.1.62871 CLOSE_WAIT
tcp4 0 0 127.0.0.1.62871 127.0.0.1.8081 FIN_WAIT_2
tcp46 0 0 *.8081 *.* LISTEN
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代碼。偽代碼如下:
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輸出如下:
2019/04/17 16:08:17 1024 <nil>
2019/04/17 16:08:18 0 write tcp 127.0.0.1:8081->127.0.0.1:64124: 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。
驗證結論三
場景一
對端關閉后,本端一直Read,則一直得到EOF錯誤。
這是由於系統調用Read會一直返回0。
場景二
對端關閉后,本端一直Write,則一直得到如下錯誤:
write tcp 127.0.0.1:8081->127.0.0.1:63520: 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操作都會返回錯誤。