Socket 文件描述符符的关闭看上去挺简单的,但是实际上还是有不少地方需要注意的。
1. Shutdown or Rest
之前遇到过一种情况,在关闭链路的时候有时候是通过四次挥手来关闭的,有时候则是直接通过 Abort 关闭的,这个问题我还找了好久。后来才发现如果接收队列里面还有数据,直接调用 close
就会发送 RST
强行关闭链路
def start_server(host=SERVER_HOST, port=SERVER_PORT):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen()
print("TCP Server Listen on: ", SERVER_HOST + ":" + str(SERVER_PORT))
while True:
client_socket, client_addr = server_socket.accept()
print("Connection from ", client_addr)
# just close socket
client_socket.close()
但是如果接收队列里面没有数据了,再调用 close
就会发送 FIN
通过正常的四次挥手关闭链路
def start_server(host=SERVER_HOST, port=SERVER_PORT):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen()
print("TCP Server Listen on: ", SERVER_HOST + ":" + str(SERVER_PORT))
while True:
client_socket, client_addr = server_socket.accept()
print("Connection from ", client_addr)
# if close after recv, server will send Fin
client_socket.recv(1024)
client_socket.close()
至于为什么接收缓冲区存在数据的时候调用 close
会直接发送 RST 我觉得理由应该是这样子的:发送 FIN 表示我准备要关闭链路了,你那边还有什么没有发送的可以一起发过来,我还可以处理。但是现在明显接收缓冲区存在数据但是没有处理,发送 FIN 也没了意义,所以就直接 RST 了。
2. Close Vs Shutdown
Linux 的 POSIX API 提供了两个 API 用于关闭 Socket,一个是 close
一个是 shutdown
。
2.1 Close[1]
其中 close
是完全关闭 Socket,不允许读也不允许写了,会直接关闭链接。
2.2 Shutdown[2]
The shutdown() call causes all or part of a full-duplex connection on the socket associated with sockfd to be shut down. If how is SHUT_RD, further receptions will be disallowed. If how is SHUT_WR, further transmissions will be disallowed. If how is SHUT_RDWR, further receptions and transmissions will be disallowed.
shutdown
相对于 close
则更加灵活,可以只关闭读、只关闭写或者两者都关闭。如果是只关闭读(SHUT_RD),是不会发送 FIN
给对端的,关闭写(SHUT_WR)则会发送 FIN
表示本端不会在继续发送数据了,但是对端还可以继续发送。
3. TIME_WAIT
上面这张图来自于《Unix Network Programming》,可以看到主动断开 TCP 链接的肯定会进入一个叫做 TIME_WAIT
阶段,这个阶段有 2MSL 长,大概会有一分钟甚至更长。由于这个时间非常的长就会导致一些问题,比如服务端程序不小心挂掉了,此时服务端程序可能会被重新拉起,但是由于 TIME_WAIT 的存在,直接 bind
会失败。这样就会导致服务端一直无法被拉起,影响业务,当然这个问题可以通过 SO_REUSEPORT
或者 SO_REUSEADDR
来解决。
但是为什么 TCP 要留一个这么长的 TIME_WAIT
时间呢,原因有两个
- 四次挥手的之后一个
ACK
丢失了,如果没有TIME_WAIT
链接就直接进入close
状态,也无法重传了,不符合 TCP 保证消息可靠性的要求。 - 防止前世今生,如果没有
TIME_WAIT
后面的程序直接绑定上了,但是有些数据包在路由里面还没过来,新绑定的程序就收到本应该发给上一个程序的包导致错误。如果经过TIME_WAIT
这么长时间,路由里面的包要么已经传到了,要么就过了 TTL(Time To Live)被丢了。