在我之前写的Wireshark抓包:理解TCP三次握手和四次挥手过程中,通过抓包分析了TCP传输的三次握手和四次挥手的过程。在这一节中,将分析在Linux中的三次握手和四次挥手的状态和过程,另外还有一个在我们编程过程中值得注意的SIGPIPE
信号的处理。
文章目录
- 1 TCP连接的11种状态
- 2 实验:查看TCP状态变化
- 3 read/recv返回0的作用
- 4 SIGPIPE信号
1 TCP连接的11种状态
在TCP建立连接的过程中将经历一系列状态,包括:LISTEN
(监听)、SYN-SENT
(同步已发送)、SYN-RECEIVED
(同步已接收)、ESTABLISHED
(已建立)、FIN-WAIT-1
(等待第一个关闭)、FIN-WAIT-2
(等待第二个关闭)、CLOSE-WAIT
(等待关闭)、CLOSING
(关闭中)、LAST-ACK
(最后的确认)、TIME-WAIT
(等待时间)、以及虚构的状态CLOSED
(关闭)。
CLOSED
是虚构的,因为它表示当没有传输控制块(TCB)时的状态(没有连接时的状态)
在TCP标准中:
- 三次握手:首先发送
SYN
标志的客户端被称为主动打开者,而另一侧的服务器称为被动打开者 - 四次挥手:首先发送
FIN
标志的客户端或服务器被称为主动关闭者,而另一端则被称为被动关闭者
如下图所示:客户端通过发送SYN
标志成为SYN_SENT
状态的过程(主动打开),同时服务器成为LISTEN
状态的过程(被动打开)。
现在来看一下我们熟悉的三次握手和四次挥手的握手流程:
- LISTEN(监听)
- 表示服务器在接收到来自客户端的
SYN
标志后可以创建新连接的状态。 - 在Linux中,通过
bind()
和listen()
系统调用,服务器进入LISTEN状态。
- 表示服务器在接收到来自客户端的
- SYN_SENT(SYN已发送)
- 表示关闭状态的客户端发送
SYN
标志并转换的状态。 - 在Linux中,客户端可以通过
connect()
系统调用进入SYN_SENT
状态。 - 根据
/proc/sys/net/ipv4/tcp_syn_retries
值以最大RTO
(重传超时)间隔发送SYN
标志。
- 表示关闭状态的客户端发送
- SYN_RECEIVED(SYN已接收)
- 表示处于
LISTEN
状态的服务器在接收到客户端的SYN
标志后,响应SYN+ACK
标志并转换的状态。 - 在Linux中,通过
accept()
系统调用,服务器进入SYN_RECEIVED
状态。 - 根据
/proc/sys/net/ipv4/tcp_synack_retries
值以最大RTO
(重传超时)间隔发送SYN
标志。
- 表示处于
- ESTABLISHED(已建立)
- 表示在3次握手后建立连接的状态,服务器和客户端可以交换数据。
- 在Linux中,通过
send()
和recv()
系统调用可以进行数据交换。 - 可以通过在套接字中设置
SO_KEEPALIVE
选项来定期检查TCP连接的有效性。
- FIN_WAIT_1(等待第一次关闭)
- 表示在已建立的状态中,主动关闭者结束后转换的状态。主动关闭者在调用
close()
或其进程终止后,由于套接字关闭,主动关闭者发送FIN
标志并进入FIN_WAIT_1
状态。
- 表示在已建立的状态中,主动关闭者结束后转换的状态。主动关闭者在调用
- FIN_WAIT_2(等待第二次关闭)
- 表示
FIN_WAIT_1
状态的主动关闭者接收到被动关闭者的ACK
,或者直到由Linux内核设置的FIN_WAIT_2
的超时时间过去为止。
- 表示
- TIME_WAIT(等待时间)
- 表示
FIN_WAIT_2
状态的主动关闭者接收到被动关闭者的FIN标志后的状态。TIME_WAIT
状态应持续2MSL
(2 * 最大分段寿命),即直到网络上的所有相关数据包(分段)完全被清除为止,以确保对后续新连接的影响最小化。
- 表示
- CLOSING(关闭中)
- 表示发生同时关闭,
FIN_WAIT_1
状态的主动关闭者接收到FIN
标志时的状态。
- 表示发生同时关闭,
- CLOSE_WAIT(等待关闭)
- 表示被动关闭者从主动关闭者接收到
FIN
标志并转换的状态。在Linux环境中,CLOSE_WAIT
状态没有超时,只有在被动关闭者的套接字关闭时才会结束。
- 表示被动关闭者从主动关闭者接收到
- LAST_ACK(最后确认)
- 表示
CLOSE_WAIT
状态的被动关闭者发送FIN
标志给主动关闭者后,在收到相应的ACK之前保持的状态。
- 表示
理论介绍完了,还是得实际写代码看看Linux中这些状态的切换,来看看实际执行过程中会遇到什么问题。
2 实验:查看TCP状态变化
- 这里实验使用这篇利用fork实现服务端与多个客户端建立连接最后贴出来的代码
1、运行服务端程序
可以看到此时服务端处于LISTEN
状态。
2、运行客户端程序
现在客户端和服务端都处于ESTABLISHED
建立连接状态。
SYN_SENT
和SYN_RCVD
状态切换地过快,这里没体现出来
3、关闭客户端程序
可以看到客户端进入了TIME_WAIT
状态。在等待2MSL时间后,客户端的网络状态将关闭(CLOSED
)。
3 read/recv返回0的作用
注意:在一端关闭后,另一端需要关闭自己的程序,否则主动关闭的那一端将无法进入TIME_WAIT
状态,而是保持在FIN_WAIT2
状态。
那如何判断对端关闭了呢? 当一端关闭后,另一端的read
或recv
函数将无限非阻塞返回0。
现在修改一下服务端的读取代码:
这里把break注释掉,也就是在客户端关闭时,服务端不退出fork
出来的子进程。现在重复一下前面的实验过程:运行服务端和客户端,然后客户端断开连接。如下图所示,客户端果然处于FIN_WAIT2
状态。
所以无论是客户端还是服务端都需要实时的read
或recv
来判断对端是否断开,进行资源的回收处理,否则对端的状态将无法处于TIME_WAIT
。
4 SIGPIPE信号
我们知道,当一方关闭连接时,它会发送一个FIN
标志给对方。这是TCP的一种半关闭状态,表示发送方(主动关闭的一方)不再发送数据,但仍然可以接收数据。所以当服务端收到客户端发送的FIN
后,它可以继续向客户端发送数据,但这些数据会被送入TCP的发送缓冲区,并不会立即发送。
如果客户端收到服务端的FIN
并关闭了连接,而服务端仍然试图向客户端发送数据,这时如果客户端已经关闭了接收端,写入操作就会导致Linux内核发送一个SIGPIPE
信号被发送给服务端进程。
SIGPIPE
:用于通知进程它试图向一个已经关闭的管道(或socket
)写数据。
默认情况下,如果进程忽略或者不捕获SIGPIPE信号,进程会被终止。因此,为了避免进程因为SIGPIPE信号而终止,可以在程序中捕获SIGPIPE信号,或者在写入之前使用一些手段来检查连接状态。
现在再来修改一下服务端程序:
这里我们也把break注释掉,这样理论上这个recv
函数会无限非阻塞地返回0,也就是服务端用于处理客户端的子进程会一直输出peer closed
,同时客户端还会处于FIN_WAIT2
状态。就是前面read/recv返回0的作用中的实验结果。
但现在,在服务端的recv
返回0时,表示服务端已经收到了FIN
信号,此时服务端使用send
往客户端发送一个消息。现在会产生一个SIGPIPE
信号,将服务端用于处理客户端的子进程杀掉。如下图所示,可以看到进程确实被杀掉了。
为了验证一下确实产生了SIGPIPE
信号,我们自己来注册这个信号。程序做出如下修改:
结果如下:
不注册的话,内核默认会将程序杀掉,注册了这个信号的话,内核就不会杀掉进程,就由我们自己处理了。在这里由于服务端的socket_read
中没有break掉,内核也没有杀掉这个子进程,所以这里recv
将无限非阻塞返回0,加上在这个分支中一直调用send
发送数据,所以服务端在这里无限输出peer closed
,并无限收到SIGPIPE
信号。
所以为了防止服务端程序在客户端被关闭后,由于程序之间没有及时的同步,导致服务端继续往客户端写数据,最后异常地被SIGPIPE
信号关闭,我们可以忽略掉SIGPIPE
信号以防止服务端被误关闭。
signal(SIGPIPE, SIG_IGN);