[Linux][网络][TCP][二][三次握手][四次挥手]详细讲解

目录


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描述符

    cpp 复制代码
    int 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函数关闭对应的文件描述符
相关推荐
Decay__4 分钟前
05_MySQL 常用 SQL 大全 —— 诊断、管理、优化辅助 SQL 与三大系统库速查
运维·数据库·sql·mysql·架构
贞明电子2 小时前
工业三色报警灯怎么接入项目系统?开关量与RS485控制应用指南
运维·网络·微信公众平台·贞明电子
HideF3 小时前
Linux
linux·服务器·网络
万联WANFLOW3 小时前
海外网络故障怎么排查?Cloudflare 三季度断网报告解读与 mtr 实战
运维·网络·故障分析
听风说ml3 小时前
socket编程 【C语言】
c语言·udp·tcp·socket编程
sbjdhjd3 小时前
网工进阶笔记:H3CSE 园区网全栈技术与协议汇总
网络·笔记·云计算·ssh·网络工程·vlan·aaa
Ruiery5 小时前
Linux 6.6内核内存管理深度解析(十三):slab/slub 分配器
linux·运维·服务器
taktok_ai5 小时前
用 cc-switch 连接 Taktok
网络
Decay__6 小时前
04_MySQL DBA 日常运维手册 —— 会话管理、锁与阻塞、事务监控、慢查询与日志巡检
运维·数据库·sql·mysql·架构
丰锋ff7 小时前
压缩命令笔记
linux