如何正确关闭TCP连接
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
如何正确关闭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操作都会返回错误。