1. 三次握手、四次挥手过程中的状态变化

先结合下图回顾一下TCP建立连接的三次握手过程,关闭连接的四次挥手过程,以及在此过程中的状态变化。

02.png

主动发起连接请求端: CLOSE状态 –> 发送SYN标志位–> SYN_SEND状态 –> 接收对端发送的 ACK标志位、SYN标志位 –> SEND_SYN状态 –> 发送 ACK标志位 –> ESTABLISHED状态(数据通信态)

主动关闭连接请求端: ESTABLISHED状态(数据通信态) –> 发送 FIN标志位 –> FIN_WAIT_1状态 – 接收对段发送的ACK标志位 –> FIN_WAIT_2状态(半关闭)–> 接收对端发送的 FIN标志位 –> FIN_WAIT_2状态(半关闭)–> 回发ACK标志位 –> TIME_WAIT状态(只有主动关闭的连接方,会经历该状态)–> 等 2MSL时长 –> CLOSE状态

被动接收连接请求端: CLOSE状态 –>程序启动 –> LISTEN状态 –> 接收对端发送的 SYN标志位 –> LISTEN状态 –> 发送 ACK标志位和SYN标志位 –> SYN_RCVD状态 –> 接收对端发送的ACK标志位 –> ESTABLISHED状态(数据通信态)

被动关闭连接请求端: ESTABLISHED状态(数据通信态) –> 接收对端发送的 FIN标志位 –> ESTABLISHED状态(数据通信态)–> 发送ACK标志位 –> CLOSE_WAIT状态 (说明对端【主动关闭连接端】处于半关闭状态) –> 发送FIN标志位 –> LAST_ACK状态 –> 接收对端发送的ACK标志位 –> CLOSE状态

1
2
3
// 相关命令:
netstat -apn | grep client 查看客户端网络连接状态
netstat -apn | grep port 查看端口的网络连接状态

2. TCP状态转换图

01.png

说明:

  • 主动建立连接–>实线部分
  • 被动建立连接–>虚线部分

CLOSED:表示初始状态。

LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。

SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。

SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。

ESTABLISHED:表示连接已经建立。

FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:

FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。

FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。

FIN_WAIT_2:==主动关闭链接的一方,发出FIN收到ACK以后进入该状态==。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。

TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。

LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。

3. 半关闭

当TCP链接中A发送FIN请求关闭,B端回应ACK后(A端进入FIN_WAIT_2状态),B没有立即发送FIN给A时,A方处在半关闭状态,此时A可以接收B发送的数据,但是A已不能再向B发送数据。从程序的角度,可以使用API来控制实现半关闭状态。

1
2
#include <sys/socket.h>
int shutdown(int sockfd, int how);
  • sockfd: 需要关闭的socket的描述符
  • how: 允许为shutdown操作选择以下几种方式:
    • SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
    • SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
    • SHUT_RDWR(2): 关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。

注意:

  • 使用close中止一个连接,它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。

    比如说:如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。

  • shutdown不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。

    比如说:在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但如果一个进程close(sfd)将不会影响到其它进程。

4. 2MSL时长

4.1 为什么要2MSL时长?

让4次握手关闭流程更加可靠;4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。

做一个测试,首先启动server,然后启动client,用Ctrl-C终止server,马上再运行server,运行结果:

03.png

这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:

04.png

server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。

现在用Ctrl-C把client也终止掉,再观察现象:

05.png

client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。

4.2 端口复用

如果想要解决上述问题,就必须要设置端口复用,使用的函数原型如下:

1
2
// 这个函数是一个多功能函数, 可以设置套接字选项
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • 参数:
    • sockfd:用于监听的文件描述符
    • level:设置端口复用需要使用 SOL_SOCKET 宏
    • optname:要设置什么属性(下边的两个宏都可以设置端口复用)
      • SO_REUSEADDR
      • SO_REUSEPORT
    • optval:设置是去除端口复用属性还是设置端口复用属性,实际应该使用 int 型变量
      • 0:不设置
      • 1:设置
    • optlen:optval 指针指向的内存大小 sizeof (int)

==那我们该在代码哪边加入这个函数呢?==

在server代码的socket()和bind()调用之间插入如下代码:

1
2
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));