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()

image-20240927114236975

但是如果接收队列里面没有数据了,再调用 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()

image-20240927114339627

至于为什么接收缓冲区存在数据的时候调用 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 表示本端不会在继续发送数据了,但是对端还可以继续发送。不过我还没遇到过需要用到 shutdown 的场景。

3. TIME_WAIT

上面这张图来自于《Unix Network Programming》,可以看到主动断开 TCP 链接的肯定会进入一个叫做 TIME_WAIT 阶段,这个阶段有 2MSL 长,大概会有一分钟甚至更长。由于这个时间非常的长就会导致一些问题,比如服务端程序不小心挂掉了,此时服务端程序可能会被重新拉起,但是由于 TIME_WAIT 的存在,直接 bind 会失败。这样就会导致服务端一直无法被拉起,影响业务,当然这个问题可以通过 SO_REUSEPORT 或者 SO_REUSEADDR 来解决。

但是为什么 TCP 要留一个这么长的 TIME_WAIT 时间呢,原因有两个

  1. 四次挥手的之后一个 ACK 丢失了,如果没有 TIME_WAIT 链接就直接进入 close 状态,也无法重传了,不符合 TCP 保证消息可靠性的要求。
  2. 防止前世今生,如果没有 TIME_WAIT 后面的程序直接绑定上了,但是有些数据包在路由里面还没过来,新绑定的程序就收到本应该发给上一个程序的包导致错误。如果经过 TIME_WAIT 这么长时间,路由里面的包要么已经传到了,要么就过了 TTL(Time To Live)被丢了。

  1. https://man7.org/linux/man-pages/man2/close.2.html ↩︎

  2. https://man7.org/linux/man-pages/man2/shutdown.2.html ↩︎