先上結論
- 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.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操作都會返回錯誤。