目录
- [1.TCP连接建立 -- 三次握手全过程](#1.TCP连接建立 -- 三次握手全过程)
- 2.为什么是三次握手?不是两次、四次?
- 3.三次握手失败会发生什么?
- 4.TCP连接断开
- 5.四次挥手失败,会发生什么?
- 6.TIME_WAIT状态
- 7.CLOSED_WAIT状态
1.TCP连接建立 -- 三次握手全过程
- 一开始,客户端和服务器都处于close 状态,先是服务端主动监听某个端口,处于Listen 状态
- client_isn:客户端初始化序列号
- server_isn:服务端初始化序列号
1.三次握手的第一个报文(客户端发起)
- 客户端会随机初始化序列号**(client_isn),将此序号置于TCP首部序号字段中,同时把 SYN标志位置为 1**,表示SYN报文
- 接着把第一个SYN 报文发生给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN_SENT 状态
2.三次握手的第二个报文(服务端发起)
- 服务端收到客户端的SYN报文后,首先服务端也随机初始化自己的序号**(server_isn)**,将此序号填入TCP首部的序号字段中
- 其次把TCP首部的确认应答号字段填入client_isn + 1 , 接着把SYN 和ACK 标志位置为1
- 最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD 状态
3.三次握手的第三个报文(客户端发起)
- 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文TCP首部ACK 标志位置为 1 ,其次确认应答号字段填入server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于ESTABLISHED状态
- 服务器收到客户端的应答报文后,也进入ESTABLISHED状态
- 一旦完成三次握手,双方都处于ESTABLISHED状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了
2.为什么是三次握手?不是两次、四次?
- 主要有以下三个原因:
- 三次握手才可以阻止重复历史连接的初始化
- 三次握手才可以同步双方的初始化序列号
- 三次握手才可以有效避免SYN攻击和避免资源浪费
1.三次握手才可以阻止重复历史连接的初始化
-
假设有这样一种场景,客户端给服务端发送了一个SYN报文(seq=100),但是这个报文由于网络波动阻塞了,于是客户端又重新发送了一个新的SYN报文(seq=200),注意不是重传,重传的SYN的序列号是一样的
-
从上图可以看出,当客户端发送的SYN报文被网络阻塞后,再次发送新的SYN报文,由于旧的报文比新的报文先抵达服务端,服务端肯定会回一个SYN+ACK报文给客户端,此时客户端就可以根据这个报文来判断这是一个历史连接 ,由此客户端就会发送一个RST报文,要求断开此次连接(即历史连接)。等新的SYN报文抵达服务端后,才会正式的建立连接
-
如果是两次握手,那就不能阻止历史连接,两次握手成功的状态图如下所示
-
当服务端收到客户端发来的SYN报文后,就进入了ESTABLISHED状态,这就意味着服务端此时就可以给客户端发送数据了,但是客户端还没有进入ESTABLISHED状态,必须收到服务端的SYN+ACK报文后,才会进入ESTABLISHED状态
-
从上图可以看出,当服务端收到第一个SYN报文后(旧的)就已近建立了连接(服务端并不知道这是历史连接),并且在回复给客户端的报文中携带了数据,但是客户端通过收到的SYN+ACK报文,发现这是历史连接;对于客户端来说,它根本来不及在服务端给客户端发送数据前来阻止这个历史连接,导致这个历史连接被创建,服务端白白发送了数据(数据创建是需要时间和空间的),造成资源浪费;只有在收到客户端发来的RST报文后才会断开连接
- 因此,要解决这样的问题,客户端就必须在服务端发送数据之前来阻止掉这个历史连接,而要实现这个功能就需要三次握手
2.三次握手才可以同步双方的初始化序列号
- TCP协议通信的双方,都必须要维护序列号,序列号是实现可靠传输的一个关键因素,其作用如下:
- 接收端可以根据序列号进行重复数据的去重
- 接收端可以根据序列号按序接受
- 通过ACK报文中的序列号可以识别发出去的数据包中,哪些已经被对方收到了
- 当客户端给服务端发送SYN(携带着自己的序列号)报文的时候,需要服务端回一个ACK应答报文,表明客户端的SYN报文已经成功接收;同样,在这个报文中除了ACK应答号还有服务端自己的序列号(发送的是SYN+ACK报文),也需要客户端回一个ACK应答报文,来确保服务端的SYN被成功接收;这样一来一回就能保证双方的初始化序列号被可靠同步
- 从下图可以看出:
- 四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了三次握手
- 而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收
3.三次握手才可以避免资源浪费
- 假设有这样一种场景,服务端收到了大量的SYN报文,客户端对服务端发过来的ACK报文压根不管,因为是两次握手,服务端只要收到一个SYN报文就进入了ESTABLISHED状态,对于服务端来说,一定要为这些连接分配资源,但是资源是有限的;这些连接占用着资源却什么事都不干,完全是浪费资源
4.小结
- TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输
- 不使用两次握手和四次握手的原因
- 两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号
- 四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数
3.三次握手失败会发生什么?
1.第一次握手失败,会发生什么?
- 当客户端想和服务端建立TCP连接的时候,首先第一个发的就是SYN报文,然后进入到SYN_SENT状态
- 在此之后,如果客户端迟迟收不到服务端的SYN-ACK报文(第二次握手),就会触发超时重传机制,重传SYN报文
- 不同版本的操作系统可能超时时间不同,这个超时时间是写死在内核里的,Linux预设是1秒
- 如果想要更改则需要重新编译内核,比较麻烦
- 当客户端在1秒后没收到服务端的SYN-ACK报文后,客户端就会重发SYN报文,那到底重发几次呢?
- 在 Linux 里,客户端的SYN报文最大重传次数由tcp_syn_retries(客户端SYN包的重试次数)内核参数控制
- 这个参数是可以自定义的,默认值一般是6,可以通过命令进行查看
cat /proc/sys/net/ipv4/tcp_syn_retries
- 通常,第一次超时重传是在1秒后,第二次超时重传是在2秒后,第三次超时重传是在4秒后,第四次超时重传是在8秒后
- 每次超时的时间是上一次的2倍
- 当第五次超时重传后,会继续等待32秒,如果服务端仍然没有回应ACK,客户端就不再发送SYN包,然后断开TCP连接
- 所以,总耗时是 1+2+4+8+16+32=63 秒,大约1分钟左右
2.第二次握手失败,会发生什么?
-
当服务端收到客户端的第一次握手后,就会回SYN-ACK报文给客户端,此时服务端会进入SYN_RCVD状态
-
第二次握手的SYN-ACK报文其实有两个目的
- 第二次握手里的ACK,是对第一次握手的确认报文
- 第二次握手里的SYN,是服务端发起建立TC 连接的报文
-
所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
- 因为第二次握手报文里是包含对客户端的第一次握手的ACK确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的SYN报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传SYN报文
- 然后,因为第二次握手中包含服务端的SYN报文,所以当客户端收到后,需要给服务端发送 ACK确认报文(第三次握手),服务端才会认为该SYN报文被客户端收到了
- 如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传SYN-ACK报文
-
在 Linux 下,SYN-ACK报文的最大重传次数由tcp_synack_retries**(**服务端SYN+ACK包的重试次数)内核参数决定,默认值是 5,可以通过命令进行查看cat /proc/sys/net/ipv4/tcp_synack_retries
-
因此,当第二次握手丢失了,客户端和服务端都会重传
- 客户端会重传SYN报文,也就是第一次握手,最大重传次数由tcp_syn_retries内核参数决定
- 服务端会重传SYN-ACK报文,也就是第二次握手,最大重传次数由tcp_synack_retries内核参数决定
3.第三次握手失败,会发生什么?
- 客户端收到服务端的SYN-ACK报文后,就会给服务端回一个ACK报文,此时客户端状态进入到ESTABLISH状态
- 因为这个第三次握手的ACK是对第二次握手的SYN的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传SYN-ACK报文,直到收到第三次握手,或者达到最大重传次数
- **注意:**ACK报文是不会有重传的,当ACK丢失了,应该由服务端重传SYN+ACK
4.TCP连接断开
1.四次挥手全过程
- 客户端打算关闭连接,此时会发送一个FIN报文,之后客户端进入FIN_WAIT_1状态
- 服务端收到FIN报文后,就会向客户端回一个ACK应答报文,然后服务端进入CLOSED_WAIT状态
- 客户端收到服务端的ACK应答报文后,之后进入FIN_WAIT_2状态
- 等待服务端处理完数据后,也向客户端发送FIN报文,之后服务端进入LAST_ACK状态
- 客户端收到服务端的FIN报文后,回一个ACK应答报文,之后进入TIME_WAIT状态
- 服务器收到了ACK应答报文后,就进入了CLOSED状态,至此服务端已经完成连接的关闭
- 客户端在经过2MSL一段时间后,自动进入CLOSED状态,至此客户端也完成连接的关闭
- **注意:**主动关闭连接的,才有TIME_WAIT状态**
2.为什么要四次挥手
- 首先,断开连接是客户端和服务端协商的一个过程
- 客户端发送FIN报文,想要告诉服务端,我没有任何数据需要请求了,我想和你断开连接
- 这里表明客户端不会再给服务端发送数据,但是还可以接受数据
- 而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN报文给客户端来表示同意现在关闭连接
- 从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的ACK和FIN一般都会分开发送,从而比三次握手导致多了一次
- 前两次握手是客户端请求断开连接的,后两次握手是服务器请求断开连接的,因此4次挥手是协商次数最少的
5.四次挥手失败,会发生什么?
1.第一次挥手失败,会发生什么?
- 当客户端调用close函数后,就会向服务端发送FIN报文,试图与服务端断开连接,此时客户端的连接进入到FIN_WAIT_1状态
- 正常情况下,如果能及时收到服务端的ACK,则会很快变为FIN_WAIT_2状态
- 如果第一次挥手丢失了,那么客户端迟迟收不到被动方的ACK的话,也就会触发超时重传 机制,重传FIN报文
- 重发次数由tcp_orphan_retries参数控制,默认值是0,特指8次
- 当客户端重传FIN报文的次数超过tcp_orphan_retries后,就不再发送FIN报文,直接进入到close状态
2.第二次挥手失败,会发生什么?
- 当服务端收到客户端的第一次挥手后,就会先回一个ACK确认报文,此时服务端的连接进入到CLOSE_WAIT状态
- 在前面也提到了,ACK报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传FIN报文,直到收到服务端的第二次挥手,或者达到最大的重传次数
- 这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的ACK报文后,客户端就会处于FIN_WAIT_2状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的FIN报文
- 对于close函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT_2状态不可以持续太久,而tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是60秒
- 这意味着对于调用close关闭的连接,如果在60秒后还没有收到FIN报文,客户端的连接就会直接关闭
3.第三次挥手失败,会发生什么?
- 当服务端收到客户端的FIN报文后,内核会自动回复ACK,同时连接处于CLOSE_WAIT状态,顾名思义,它表示等待应用进程调用close函数关闭连接
- 此时,内核是没有权利替代进程关闭连接的,必须由进程主动调用close函数来触发服务端发送FIN报文
- 服务端处于CLOSE_WAIT状态时,调用了close函数,内核就会发出FIN报文,同时连接进入LAST_ACK状态,等待客户端返回ACK来确认连接关闭
- 如果迟迟收不到这个ACK,服务端就会重发FIN报文,重发次数仍然由tcp_orphan_retries参数控制,这与客户端重发FIN报文的重传次数控制方式是一样的
4.第四次挥手失败,会发生什么?
- 当客户端收到服务端的第三次挥手的FIN报文后,就会回ACK报文,此时客户端连接进入TIME_WAIT状态
- 在Linux系统,TIME_WAIT状态会持续2MSL后才会进入关闭状态
- 然后,服务端没有收到ACK报文前,还是处于LAST_ACK状态
- 如果第四次挥手的ACK报文没有到达服务端,服务端就会重发FIN报文,重发次数仍然由前面介绍过的tcp_orphan_retries参数控制
6.TIME_WAIT状态
0.问题抛出
-
若启动Server后,Ctrl+C终止Server,再马上运行Server,会有以下报错
bind error:Address already in use
- **原因:**虽然Server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的Server端口
-
TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL的时间后才能回到CLOSED状态
- 使用Ctrl-C终止了Server,所以Server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的Server端口
1.为什么TIME_WAIT等待的时间是2MSL?
- MSL是Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃
- TIME_WAIT等待2倍的MSL,比较合理的解释是:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待2倍的时间
- 比如:如果被动关闭方没有收到断开连接的最后的ACK报文,就会触发超时重发FIN报文,另一方接收到FIN后,会重发ACK给被动关闭方, 一来一去正好2个MSL
- 可以看到2MSL时长,这其实是相当于至少允许报文丢失一次
- 比如:若ACK在一个MSL内丢失,这样被动方重发的FIN会在第2个MSL内到达,TIME_WAIT状态的连接可以应对
- 为什么不是4或者8MSL的时长呢?
- 可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比
- 2MSL的时间是从客户端接收到FIN后发送ACK开始计时的
- 如果在TIME-WAIT时间内,因为客户端的ACK没有传输到服务端,客户端又接收到了服务端重发的FIN报文,那么2MSL时间将重新计时
- 在 Linux系统里2MSL默认是60秒,那么一个MSL也就是30秒。Linux系统停留在TIME_WAIT的时间为固定的60秒
2.为什么需要TIME_WAIT状态
-
主动发起断开连接的一方才会有TIME_WAIT状态,它是四次挥手的最后一个状态
- 它的出现,就是为了解决网络的丢包和网络不稳定所带来的其他问题
-
需要TIME_WAIT状态有以下两个原因
- 防止历史连接中因网络延迟的数据包或者丢失重传的数据包,被新的连接(与历史连接的端口号相同)复用
- 保证被动关闭连接的一方,能够正确关闭
-
防止历史连接中因网络延迟的数据包或者丢失重传的数据包,被新的连接(与历史连接的端口号相同)复用
- 服务端向客户端发送了 Seq=300的报文和Seq=301的报文,因为是处于连接状态的,此次报文必然是夹带着数据的,由于网络原因,导致Seq=301的报文阻塞在网络中
- 紧接着,服务端以相同的四元组重新打开了新连接,前面被延迟的Seq=301这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题
- 为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此TCP设计了TIME_WAIT状态,状态会持续2MSL时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的
-
确保连接方能在时间范围内,关闭自己的连接
- 客户端在进行四次挥手后进TIME_WAIT状态,如果第四次挥手的报文丢包了,客户端在一段时间内仍然能够接收服务器重发的FIN报文并对其进行响应,能够较大概率保证最后一个ACK被服务器收到
3.解决TIME_WAIT状态引起的bind失败
-
在Server的TCP连接没有完全断开之前不允许重新监听,某些情况下可能是不合理的
- 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求)
-
这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接
-
由于请求量很大,就可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信五元组(源ip,源端口,目的ip,目的端口,协议)
- 其中服务器的ip和端口和协议是固定的,如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了,就会出现问题
-
使用**setsockopt()设置socket描述符的选项 SO_REUSEADDR为1**,表示允许创建端口号相同但IP地址不同的多个socket描述符
cppint opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
7.CLOSED_WAIT状态
- 双方在进行四次挥手时,如果只有客户端调用了close函数,而服务器不调用close函数,此时服务器就会进入CLOSE_WAIT状态,而客户端则会进入到FIN_WAIT_2状态
- 但只有完成四次挥手后连接才算真正断开,此时双方才会释放对应的连接资源。如果服务器没有主动关闭不需要的文件描述符,此时在服务器端就会存在大量处于CLOSE_WAIT状态的连接,而每个连接都会占用服务器的资源,最终就会导致服务器可用资源越来越少
- 因此如果不及时关闭不用的文件描述符,除了会造成文件描述符泄漏以外,可能也会导致连接资源没有完全释放,这其实也是一种内存泄漏的问题
- 因此在编写网络套接字代码时,如果发现服务器端存在大量处于CLOSE_WAIT状态的连接,此时就可以检查一下是不是服务器没有及时调用close函数关闭对应的文件描述符