TCP协议是如何实现可靠性传输的
TCP协议是如何实现可靠性传输的
TCP(Transmission Control Protocol,传输控制协议)是一个具有可靠性传输、面向字节流、面向连接、全双工通信的传输层协议,这里我们主要介绍其可靠性传输特点以及具体的实现方式。
本文分为一下几点进行介绍:
- 序列号(Sequence Numbers)与确认应答(Acknowledgements - ACKs)机制
- 重传机制(Retransmission)
- 滑动窗口机制
- 拥塞控制(Congestion Control,基于Sender端的滑动窗口实现)
- 流量控制(Flow Control,基于Receiver端的滑动窗口实现)
- 校验和(Checksum)
- 连接管理(Connection Management)
序列号与确认应答
序列号
基本概念
- TCP是一种面向字节流的协议。这意味着TCP将应用程序传输的数据看作是一个连续的、无结构的字节序列。
- 为了管理和追踪这些字节,TCP会为整个字节流中的 每一个字节 都编上一个唯一的序号。
- 在TCP段(Segment)的头部,有一个32位的
序列号字段。这个字段包含的值 不是 该段数据的字节数或段的编号,而是该TCP段所携带的 第一个数据字节 在整个字节流中的序号。
初始序列号 (Initial Sequence Number, ISN)
- 当建立一个新的TCP连接时(三次握手期间),通信双方(客户端和服务器)各自会选择一个初始序列号(ISN)。
- 这个ISN 不是 从0或1开始的固定值,而是基于一个随时间变化的计时器(通常每4微秒加1)或其他方法生成的,使其具有一定的随机性。这样做主要是为了安全,防止旧连接的报文被误认为是新连接的报文,以及防止序列号被预测而发起的攻击。
- 双方通过三次握手的
SYN和SYN-ACK报文交换各自的ISN,并确认对方的ISN。
序列号的递增
- 一旦连接建立,发送方发送数据时,后续TCP段的序列号会基于已发送的数据量进行递增。
- 例如:如果一方选择的ISN是
X,它发送的第一个TCP段包含1000字节的数据,那么该段的序列号就是X。如果下一个段紧接着这1000字节发送,并且包含500字节的数据,那么第二个段的序列号就是X + 1001。 - 即使TCP段不携带数据(例如纯ACK段),有时也可能消耗一个序列号(如SYN和FIN段),但通常携带数据的段的序列号是根据其数据部分的第一个字节来确定的。
- 普通数据段:
- 序列号 = 该段中第一个字节在整个数据流中的位置
- 每个数据字节占用一个序列号
- 特殊控制段:
- SYN段:建立连接时发送,消耗1个序列号位置
- FIN段:关闭连接时发送,消耗1个序列号位置
- 这意味着即使这些段不携带实际数据,它们仍然会导致后续段的序列号增加1
- 纯ACK段:
- 只包含确认信息,不携带数据
- 通常不消耗序列号
- 其序列号字段值通常是发送方下一个要发送的数据字节序列号
- 普通数据段:
作用
- 保证数据有序性: 接收方根据TCP段的序列号来重新组装数据,即使这些段在网络中是乱序到达的,也能恢复成原始的、有序的字节流。
- 检测丢失数据: 接收方可以通过检查收到的序列号是否连续来判断是否有数据段丢失。例如,收到了
Seq=1000的段和Seq=3000的段,但没收到Seq=2000的段,就知道中间丢失了数据。 - 去除重复数据: 如果接收方收到了具有相同序列号范围的数据段(通常是由于重传导致),它可以根据序列号识别出这是重复数据并丢弃它。
确认应答
基本概念
确认应答是TCP实现可靠性的关键反馈机制。接收方使用ACK来告知发送方它已经成功收到了哪些数据。
TCP头部也有一个32位的
确认号(Acknowledgement Number) 字段。同时,TCP头部还有一个
ACK标志位 (Flag)。当这个标志位被设置为1时,表示确认号字段有效;否则,确认号字段无效。
在连接建立之后,几乎所有发送的TCP段都会设置ACK标志位。
==确认号的含义(重点)==
- TCP的ACK是 累积确认 (Cumulative Acknowledgement) 的。
- 确认号字段的值 不是 对刚刚收到的那个段的确认,而是表示 “我已经成功接收到序号为
N-1(含)之前的所有字节,现在我期望接收的下一个字节的序号是N”。 - 例如:发送方发送了三个段,Seq分别为 1, 101, 201 (假设每个段100字节)。接收方收到了
Seq=1和Seq=101的段,它会发送一个ACK段,其确认号 (Ack Number) 设置为201。这表示:序号在201之前的所有字节(即1到200)我都收到了,请从序号201开始发送。
ACK的发送时机
- 捎带确认 (Piggybacking): 如果接收方正好有数据要发送给对方,它通常会将ACK信息“捎带”在它发送的数据段中(设置ACK位并填写确认号),这样可以节省网络带宽。
- 延迟确认 (Delayed ACK): 如果接收方没有数据要立即发送,它通常不会为每个收到的数据段都立刻发送一个单独的ACK段。它可能会稍等片刻(例如,几十到几百毫秒),看是否有多个段可以一次性确认,或者是否有数据可以捎带确认。这可以减少网络中的ACK报文数量,但可能略微增加传输延迟。规范通常建议最多延迟对一个数据段的确认。
- 收到乱序段时: 当接收方收到一个乱序的数据段(即Seq号大于期望收到的序号),它会 立即 发送一个重复的ACK,该ACK的确认号仍然是它当前期望收到的那个最小序号。
作用
- 确认数据接收: 让发送方知道哪些数据已经被对方成功接收。
- 触发数据发送/窗口滑动: 发送方收到ACK后,可以认为被确认的数据已经成功送达,可以将这些数据从发送缓存中移除(或标记为已确认),并根据接收方的窗口通告(包含在ACK段中)继续发送新的数据(滑动发送窗口)。
- 检测丢失数据(通过重复ACK): 当发送方连续收到三个或更多具有相同确认号的ACK(称为“重复ACK”或“Duplicate ACKs”)时,它强烈暗示该确认号对应的那个数据段很可能在网络中丢失了。这会触发“快速重传”机制,让发送方在等待超时之前就重发丢失的段。
重传机制
超时重传
机制原理
- 这是TCP处理数据丢失的 最基本、最核心 的机制,可以看作是最后的
安全网。 - 当TCP发送方发送一个数据段(Segment)后,它会为这个数据段启动一个 重传计时器 (Retransmission Timer)。
- 发送方期望在计时器超时之前收到接收方对该数据段的确认(ACK)。这个确认可以是直接确认该段,也可以是通过确认后续数据段而实现的累积确认。
- 如果在计时器到期时,发送方 仍未收到 对该数据段的有效确认,发送方就会 假定 该数据段在网络中丢失了(或者确认该数据段的ACK丢失了)。
- 此时,发送方就会 重新发送 那个被认为丢失的数据段。
重传超时时间 (Retransmission Timeout, RTO)
- 这个计时器的时长,即
RTO的设定至关重要。- 太短: 可能导致不必要的重传。网络可能只是暂时拥堵导致延迟,数据并未丢失,过早重传会浪费带宽,甚至加剧拥塞。
- 太长: 会导致数据丢失后需要等待很长时间才进行重传,降低了数据传输的效率和吞吐量。
- 动态计算RTO:TCP的RTO是动态调整的,它基于对**网络往返时间 (Round-Trip Time, RTT)**的持续测量和估计。
- TCP会测量发送数据段到接收到其确认之间的时间(
RTT样本)。 - 它使用复杂的算法(如Jacobson/Karels算法)来平滑RTT样本,计算出 平滑RTT (Smoothed RTT, SRTT) 和 RTT方差 (RTT Variation, RTTVAR)。
- RTO通常设置为
SRTT + 4 * RTTVAR。这个公式考虑了平均延迟和延迟的抖动,提供了一个相对健壮的超时估算。 - RTO有一个最小值(通常至少几百毫秒)和一个最大值限制。
- TCP会测量发送数据段到接收到其确认之间的时间(
指数退避 (Exponential Backoff)
- 如果一个数据段发生了超时重传,TCP通常会将其下一次重传的RTO值加倍(例如,变成原来的2倍、4倍、8倍…),直到达到一个上限。
- 这种“指数退避”策略的目的是:如果超时是由于网络拥塞引起的,快速连续地重传只会让网络更加拥堵。通过延长等待时间,给网络一个恢复的机会。
- 一旦收到了对重传数据的确认,RTO的计算会恢复到基于SRTT和RTTVAR的正常方式。
缺点
超时重传的主要缺点是反应较慢。必须等待整个RTO时间过去才能发现和处理丢包,尤其在RTT较大的网络中,这个等待时间可能很长,导致连接停顿,吞吐量显著下降。
快速重传
机制原理
- 快速重传机制旨在 更快地 检测和恢复丢失的数据段,尤其是在只有少量数据段丢失,而其后的数据段仍然能够到达接收方的情况下。它是一种优化,试图避免等待漫长的RTO。
- 它的触发条件 不是计时器超时,而是发送方 收到 了来自接收方的 重复确认 (Duplicate ACKs)。
重复确认 (Duplicate ACKs)
- 当接收方收到一个 乱序 的数据段(即序列号大于当前期望收到的序列号)时,根据TCP的累积确认规则,它无法确认这个乱序段。
- 此时,接收方会 立即 发送一个ACK,其确认号仍然是它 当前正在等待的那个最小序列号。
- 如果发送方发送了Seq=100, 200, 300, 400, 500 的段,但Seq=200丢失了。
- 接收方收到Seq=100,发送ACK=200。
- 接收方收到Seq=300(乱序),发送ACK=200(重复ACK 1)。
- 接收方收到Seq=400(乱序),发送ACK=200(重复ACK 2)。
- 接收方收到Seq=500(乱序),发送ACK=200(重复ACK 3)。
- 这些
ACK=200的确认被称为重复确认,因为它们都在重复确认Seq=100及之前的数据。
触发条件
TCP发送方通常在 连续收到三个或以上 的重复ACK时,会触发快速重传机制。(即收到了原始ACK之外的3个重复ACK,总共4个具有相同确认号的ACK)。
为什么是3个
- 收到1或2个重复ACK可能是由于网络中的轻微乱序造成的,不一定代表丢包。
- 收到3个重复ACK则强烈暗示
Seq=200很可能丢失,因为后面的3个段都已经到达。
执行动作
一旦触发快速重传,发送方就 不再等待 该数据段(这里是Seq=200)的重传计时器超时,而是 立即重传 那个被认为丢失的数据段(Seq=200)。
优点
相比超时重传,快速重传能够 显著更快地 发现并修复丢包,尤其是在高带宽、高延迟的网络中对吞吐量的提升效果明显。它使得TCP能够在不中断数据流的情况下(不需要等待超时)恢复丢失。
与快速恢复 (Fast Recovery)
快速重传通常与 快速恢复 算法结合使用。在快速重传一个数据段后,发送方不会像超时重传那样将拥塞窗口降得很低(例如降到1),而是采用一种更温和的方式调整拥塞窗口,试图在修复丢包的同时维持较高的数据传输速率。(快速恢复算法请查看拥塞控制章节)
滑动窗口机制
滑动窗口机制是TCP在数据传输过程中,用于控制发送速率与接收能力匹配的一种流量管理方式。它通过动态调整“窗口”范围,实现高效、可靠的字节流传输。
工作原理
我们分为发送方(Sender)和接收方(Receiver)两个方面介绍
Sender
组成
我们来分别介绍一下这四个部分:
- 已发送并已确认 (Sent and Acknowledged):、 这部分数据已经成功发送并收到了接收方的确认。窗口的左边界会向右移动,越过这些字节。
- 已发送但未确认 (Sent but Not Yet Acknowledged): 这部分数据已经发送出去,但还没有收到接收方的确认。这些数据仍在发送窗口内,发送方需要保留这些数据的副本,以备重传。
- 允许发送但尚未发送 (Allowed to Send but Not Yet Sent): 这是发送窗口中可以立即发送的新数据部分。
- 不允许发送 (Not Allowed to Send): 这部分数据在发送窗口之外,暂时不能发送。
==注意==
动画演示可参考《计算机网络:自顶向下方法(第七版)》所提供的动画演示,配套动画资源
动态变化分析
滑动窗口(指的是==已发送未确认==+==允许发送但尚未发送==两个部分)是==动态变化==的,而不是一段固定大小的字节块,下面我们就来分析一下具体的动态变化过程:
注意,这里的动态变化指的是其大小是受到
cwnd和rwnd的限制的,具体来说滑动窗口大小 = min(cwnd, rwnd),而在边界移动时,窗口时整体变化的,而不是只移动某一边界
- 发送数据: 当发送方发送数据时,窗口内
允许发送但尚未发送的部分字节被发送出去,它们的状态变为==已发送但未确认==。 - 接收确认 (ACK): 当发送方收到接收方的
确认号 (Acknowledgment Number)时,表示==该确认号之前的所有字节(不包括确认号本身)都已被接收方成功接收==。发送窗口的左边界就可以向右滑动到这个确认号的位置。 - 窗口滑动: 左边界向右移动,意味着窗口整体向右滑动。如果此时接收方通告的接收窗口大小没有变化,那么右边界也会相应向右移动,允许发送更多新的数据。
- 超时重传: 如果发送方在一定时间内没有收到对某个已发送报文段的确认,就会认为该报文段丢失,并进行重传。
Receiver
组成
可以看到,不同于发送端,接收端的TCP滑动窗口只有三个部分:
我们来分别介绍一下接收端的这三个部分:
已接收并确认 (Received and Acknowledged): 这部分数据已经成功接收并且已向发送方发送了确认报文。这些数据已经交付给应用层或正在等待按序交付。当确认后,窗口的左边界会向右移动,表示接收方已经处理了这些字节。
允许接收 (Allowed to Receive): 这是接收窗口中可以接收的新数据部分。这些序列号的数据还未到达,但接收方已为它们分配了缓冲区空间,并已告知发送方可以发送这些序列号的数据。当这部分数据到达时,会被存入接收缓冲区并发送确认。
不允许接收 (Not Allowed to Receive): 这部分数据的序列号超出了接收窗口的右边界(
RCV.NXT+RCV.WND),接收方当前无法接收这些数据。如果接收方收到这个范围内的数据段,通常会丢弃它们或者将它们暂存在特殊缓冲区中(取决于具体实现),但不会确认这些数据。只有当接收窗口向右滑动,这些序列号进入”允许接收”范围后,才能被正常处理。
动态变化分析
接收窗口(指的是==允许接收==部分)也是==动态变化==的,下面我们分析接收端滑动窗口的动态变化过程:
接收数据: 当接收方收到序列号落在”允许接收”范围内的数据段时,会将其存入接收缓冲区。如果收到的是当前期望的序列号(RCV.NXT),接收方会向发送方发送一个ACK,确认号为已接收数据的最后一个字节序号加1。
处理有序数据: 当接收到期望序列号的数据后,接收窗口的左边界(RCV.NXT)会向右移动。如果已经接收了连续的数据,左边界会一直向右移动到第一个尚未接收的字节位置。接收方会同时发送新的ACK,其确认号为新的RCV.NXT值。
处理乱序数据: 如果接收到的数据段序列号大于当前期望的序列号(乱序到达),接收方通常会:
缓存这些乱序数据(存储在”允许接收”范围内)
立即发送一个重复ACK,确认号仍为当前期望的序列号(RCV.NXT)
这里发送的ACK报文会参与TCP的另一个机制,即快速重传机制,具体见快速重传
等待缺失数据段到达后,再按序向应用层交付这些数据
窗口大小更新: 接收窗口大小会随着应用层处理数据的速度而变化:
- 应用层从TCP接收缓冲区读取数据后,可用缓冲空间增加
- 接收窗口右边界相应向右移动,扩大”允许接收”范围
- 接收方通过ACK报文中的Window字段通告发送方新的窗口大小
零窗口通告: 如果应用层处理数据太慢,接收缓冲区可能会填满。此时,接收方会向发送方发送窗口大小为0的通告(零窗口),要求发送方暂停发送数据。当应用层继续读取数据并释放缓冲区空间后,接收方会发送窗口更新报文,通知发送方恢复数据传输。
接收窗口的动态变化确保了TCP能够在有限的缓冲区资源下可靠地接收并有序地交付数据,同时通过窗口大小的通告实现了从接收方到发送方的流量控制。
二者的关联
再谈滑动窗口
滑动窗口其实可以理解成是一种标识,标记当前正在处理的数据范围,同时会根据网络的阻塞情况进行动态调整(具体的调整过程一般都是接收端通过TCP报头的Windows字段控制),具体来说,一个滑动窗口的大小有两个量决定,一个是接收端可接收的数据量(rwnd),一个是发送端的可接收数据量(cwnd)
二者的关系
前面也提到过,滑动窗口的大小是由两者共同决定的(取二者中的较小值),具体来说
- 发送端会记录接收端TCP报头中的窗口大小信息(由接收端计算并填充),然后和自己的CWND(拥塞窗口)进行比较
- 而发送端CWND大小的维护,依赖于复杂的拥塞控制机制
- 滑动窗口的大小 = min(RWND, CWND)
拥塞控制
拥塞控制简述
这里做一个澄清:拥塞窗口是在滑动窗口的机制上结合拥塞控制算法实现的一种对窗口的控制机制,而不是像流量控制机制那样完全依赖于滑动窗口机制实现的,所以不可简单的将拥塞控制窗口认为是滑动窗口的产物(因为拥塞窗口的调整只依赖于Sender本端的拥塞控制算法,而不依赖于对端的信息进行调整)
拥塞控制,实际上就是对发送端CWND窗口大小调整的一系列策略,包括慢启动、拥塞避免、超时重传、快速重传、快速恢复等机制,下面具体介绍:
CWND (Congestion Window / 拥塞窗口)
首先是发送端的滑动窗口,指的是SND.WND这一端,而拥塞窗口指的是其中可用的部分,具体来说就是这里的SND.WND - (SND.NXT - SND.WNA)这一部分,拥塞窗口由Sender端独立维护和调整,接收端并不清楚发送端的拥塞窗口大小,而针对Sender端拥塞窗口的调整,也是整个滑动窗口机制的核心,因为它保证了发送端的数据发送速率不会太快造成网络传输阻塞等问题;
慢启动(Slow Start,初始阶段)
在慢启动阶段,每收到一个ACK确认报文,CWND增加一个MSS,注意虽然叫慢启动,但是如果不加以限制,会造成指数爆炸,因为每次发送的数据段数量都是建立在之前窗口大小 + 新增的窗口大小的基础上的,所以这是一个典型的指数增长模型,但是如果不加以限制,指数增长的数据段会造成严重的网络阻塞
MSS和MTU
MTU(Maximum Transmission Unit,最大传输单元)
MTU 是指一种通信协议的某一层上面所能通过的最大数据包大小,通常与网络接口的物理特性和网络层协议有关,例如,在以太网中,MTU 一般是 1500 字节。所以一般
网络层 --> 数据链路层的每个数据包大小都小于1500字节。// TODO
对于一段超过1500字节的数据,传输时就会涉及到分片的问题,具体可见网络中的常见数据分片详解
MSS (Maximum Segment Size,最大分段大小)
MSS是
TCP协议中的部分,它表示TCP数据包每次能够传输的最大数据量。一般而言,MSS都小于MTU,因为TCP传输时会带上报头信息(TCP-20字节,IP-20字节),其主要作用是为了防止网络层(即IP)分片问题,具体解释如下:MSS协商
- 首先在TCP三次握手时,双方会协商MSS的大小(TCP报头中的选项字段,具体来说是 “Maximum Segment Size” (MSS) 选项)
- 客户端 (发起连接方)在发送第一个 SYN 包时,会在 TCP 头部选项中包含它计算出的 MSS_proposed 值
(MSS_proposed = Local_Interface_MTU - IP_Header_Size - TCP_Header_Size)- 服务端在接收到客户端的SYN请求后,会读取客户端通告的MSS值,同时服务端也会根据自己的
MTU计算出自身的MSS_proposed,在向客户端回复SYN-ACK时也会将自身的MSS_proposed加入到TCP报头中- 在完成连接建立后,双方会根据对端的MSS值,选取其中较小的值作为整个连接过程中的MSS
分片问题
所谓的防止IP分片,其实就是在IP的上层TCP对数据做了预先处理,对于超过MSS的数据,进行分段操作(
TCP分段),这样它们在传递到网络层后,就不会因为超过数据传输限制而进行IP分片了。
慢启动阈值(ssthresh - Slow Start Threshold)
慢启动阈值就是为了解决上面慢启动的指数爆炸问题的,用于决定 TCP 何时从“慢启动 (Slow Start)”阶段切换到“拥塞避免 (Congestion Avoidance)”阶段,所以一般都是和拥塞避免算法绑定使用的,具体的调整逻辑见下文;
拥塞避免(Congestion Avoidance)
拥塞避免算法就是通过一系列的算法机制,对CWND的增长进行控制的一种机制,为了实现维持高吞吐量的同时避免网络阻塞的目的
注意这里的避免并不意味着完全消除阻塞,而是通过控制发送速率来推迟拥塞的发生或减轻阻塞带来的影响
一般而言,有两种方式进行拥塞避免阶段:
- 慢启动切换:当拥塞窗口大小达到
慢启动阈值时,就会切换到拥塞避免阶段,此时CWND会执行线性增长 - 快速恢复切换(只会出现在某些实现中):在处理完由快速重传触发的丢包后,经过快速恢复阶段,TCP 通常会进入拥塞避免阶段,而不是重新回到慢启动(因为快速重传被认为是轻微拥塞的信号)
拥塞避免阶段 CWND 的调整机制
在拥塞避免阶段,每经过一个大约一个RTT,CWND增加一个MSS,具体来说,对于每个成功接收到的新的 ACK (确认了之前未确认的数据,而不是重复 ACK),CWND 的增加量为:CWND = CWND + MSS * (MSS / CWND)
可以这样理解,当发送方成功发送并收到一个完整窗口(大小为 CWND)的数据的确认后,它就将窗口大小增加一个 MSS。
拥塞避免阶段的退出条件以及慢启动阈值的更新策略
分为两种情况:
发生超时重传(RTO - Retransmission Timeout),这通常被认为是严重拥塞的信号
- 将 ssthresh 更新为
max(FlightSize / 2, 2 * MSS)(FlightSize 是指超时发生时已发送但未确认的数据量) - 将 CWND 重置为一个很小的值,通常是 1 MSS (或者初始窗口 IW)。
- 重新进入慢启动阶段
- 将 ssthresh 更新为
发生快速重传(收到3个或更多的重复ACK),这通常被认为是轻微或局部拥塞的信号(此时网络依然有能力传输一些数据)
- 将 ssthresh 更新为
max(FlightSize / 2, 2 * MSS) - CWND 减半:CWND = ssthresh (或者 CWND = CWND / 2,然后根据具体算法可能还会加上一些值代表重复ACK所代表的数据)。这被称为乘性减少 (Multiplicative Decrease)
- 执行快速重传,重传丢失的那个数据段
- 进入快速恢复 (Fast Recovery) 阶段。在快速恢复成功后,通常会直接进入拥塞避免阶段,而不是慢启动。
- 将 ssthresh 更新为
==乘性减小==和==加性增大==
二者构成了著名的**AIMD(Additive Increase, Multiplicative Decrease)**拥塞控制策略的两个核心组成部分,是许多经典 TCP 拥塞控制算法(如
TCP Reno)的基础。加性增大(Additive Increase, AI)
当 TCP 连接处于拥塞避免 (Congestion Avoidance) 阶段,并且没有检测到网络拥塞(即没有丢包)时,发送方的拥塞窗口 (CWND) 线性地、缓慢地增加(和上面的一致,不做赘述)乘性减小(Multiplicative Decrease, MD)
当 TCP 连接检测到网络拥塞时,通常是通过快速重传机制(收到3个或更多重复ACK)判断发生的,发送方的拥塞窗口 (CWND) 显著地、按比例地减少
超时重传(Timeout Retransmission)
具体的介绍见上文——重传机制-超时重传
这里主要介绍超时重传机制在拥塞控制算法中的应用:
超时重传在拥塞控制中主要是作为标识网络阻塞的信号作用的,一旦发生超时重传,说明现在网络处于
严重阻塞阶段,拥塞控制算法会调整CWND窗口大小以及慢启动阈值,具体见上文[拥塞避免](#拥塞避免(Congestion Avoidance))
快速重传与快速恢复(Fast Retransmit and Fast Recovery)
快速重传同样见上文——重传机制-快速重传,而对于其在拥塞避免算法的应用,依然在[上文](#拥塞避免(Congestion Avoidance))中有所涉及,这里补充一点
快速重传是超时重传的一种优化方式,具体体现在:
更早的检测和响应丢包
- 对于超时重传算法,只有重传计时器超时,才会触发相应的重传行为
- 而对于快速重传,只需要连续接收到三个相同序号的ACK报文,就会触发对应的重传行为,而无需等待重传计时器超时
这样的机制,在数据量较大时,可以极大的减少普通的超时重传算法带来的时间等待成本,更早的检测并响应丢包事件
减少不必要的等待时间,提高网络吞吐量
对网络拥塞的判断更精细(与快速恢复结合时)
- 超时重传发生时,发送方通常认为网络可能发生了比较严重的拥塞,因此会大幅度降低拥塞窗口(例如,降到1个MSS)。
- 快速重传(通常与快速恢复 Fast Recovery 配合使用)认为,既然还能收到冗余ACK,说明网络连接并未完全中断,只是发生了个别数据包的丢失。因此,在执行快速重传后,拥塞控制算法(如TCP Reno中的快速恢复)通常不会像超时重传那样严厉地减少拥塞窗口,而是将其减半(或设置为ssthresh),并尝试继续发送新的数据(如果窗口允许),从而更快地恢复传输速率。
这里只介绍**快速恢复(Fast Recovery)**机制
快速恢复(以经典的 TCP Reno 为例)
一般发生在快速重传发生后,主要目的是为了避免不必要的慢启动,因为快速重传意味着网络只是轻微阻塞,没有必要从慢启动阶段(过于保守和低效)慢慢增长窗口大小
具体步骤
调整拥塞控制参数 (进入快速恢复前或开始时)
首先是慢启动阈值
ssthreth = max(FlightSize / 2, 2 * MSS),FlightSize是指在收到第 3 个重复 ACK 时,网络中已发送但未确认的数据量第二步是设置拥塞窗口(CWND),
CWND = ssthreth + 3 * MSS,3 * MSS是TCP Reno的特殊做法,为了补偿那 3 个已经离开网络的重复 ACK 所代表的数据包重传丢失的数据段
在接收到 3 个连续的ACK报文后,立即重传被认为丢失的那个数据段
处理后续的重复 ACK (在快速恢复阶段)
每当收到一个重复ACK报文,
CWND = CWND + 1 * MSS,这个新的重复 ACK 进一步确认了又有一个数据包离开了网络(被接收方处理)。因此,发送方可以认为网络中又空出了一个 MSS 的空间,可以尝试发送一个新的数据段。处理新的、非重复的 ACK (结束快速恢复)
如果收到一个新的ACK报文,该ACK报文确认了之前重传的那个丢失的数据段以及它之后的所有数据时(即它确认了所有到某个更高序列号的数据),此时恢复阶段完成,为了避免过高的发送速率,需要对窗口大小进行再次限制,
CWND = ssthreth,设置完成后,正式退出恢复阶段此后,进入拥塞避免阶段,
CWND执行线性增长;
流量控制
流量控制简述
流量控制,和拥塞控制类似,是控制RWND(即接收窗口)的一系列机制,防止发送方发送数据的速度过快,以至于接收方来不及处理,导致接收方的缓冲区溢出而丢弃数据。
RWND(Receive Window / 接收窗口)
// TODO 图片
RWND 是 TCP 流量控制机制的核心组成部分。简单来说,它代表了接收方当前愿意并且能够接收的数据量(以字节为单位)。
零窗口 (Zero Window) 与零窗口探测 (Zero Window Probe)
零窗口
当接收方的 TCP 接收缓冲区完全满了,无法再接收更多数据时,它会通告一个大小为 0 的 RWND(即零窗口),发送端接收到零窗口通告后,必须立刻停止发送任何数据
不过这种机制有一个潜在的隐患——死锁问题:如果接收端后续清空了缓冲区,并发送了一个带有新的非零 RWND 的 ACK(窗口更新报文),但这个窗口更新报文在网络中丢失了,那么发送方将永远不知道接收窗口已经打开,而接收方则在等待数据,导致连接死锁。
为了解决这个问题,引入了零窗口探测机制;
零窗口探测
为了解决这个潜在的死锁问题,发送方在收到零窗口通告后会启动一个持续计时器 (Persist Timer),当持续计时器超时后,发送方会发送一个小的探测报文(通常只包含 1 字节的数据,即使窗口是0,接收方也必须能处理这种探测),接收方收到探测报文后,必须回应一个 ACK,并在其中包含其当前的 RWND 值。
这样,即使之前的窗口更新报文丢失,发送方也能通过周期性的探测最终了解到接收窗口何时重新打开。探测的间隔通常会采用指数退避的方式增加。
可以类比 TCP 的
心跳检测机制,都是用于探测对端状态信息的,只不过一个是探测可接收窗口大小,一个是探测连接是否存在的
窗口缩放选项 (Window Scale Option - RFC 7323, obsoletes RFC 1323)
为什么存在窗口缩放选项
TCP 头部中的窗口大小字段只有 16 位,最大能表示65535 字节 (2^16 - 1)。在高速、高延迟的网络(即“长肥管道” LFN, Long Fat Network)中,这个窗口大小不足以充分利用带宽(因为带宽延迟积 BDP 可能远大于 64KB)。
具体实现
窗口缩放选项是在TCP建立连接过程中通过SYN和SYN-ACK进行协商的,它定义了一个移位计数器(shift count),取值范围 0-14,实际的接收窗口大小就由该移位计数器决定:
实际的接收窗口大小 = TCP 头部中的窗口值 * 2^(移位计数)
这种方式使得 TCP 能够通告更大的接收窗口,从而在 LFN 环境下也能实现高吞吐量,确保流量控制不会成为瓶颈。这本身不是一种新的“控制”机制,而是对核心RWND机制的增强,使其能适应更广泛的网络条件。
校验和
校验和简述
它是一种差错检测机制,用于检测其头部和数据在传输过程中是否发生了比特错误(即数据损坏),尽管校验和机制相对简单,但是却可以有效的检验数据的完整性和正确性;
为什么需要校验和机制
网络传输过程中,由于各种干扰(如电磁干扰、硬件故障等),数据包中的比特位可能会发生翻转(0 变 1 或 1 变 0)。校验和的目的是检测出这些错误,从而保证TCP连接的双方的数据一致性;
校验和的计算
计算范围
TCP 伪头部 (Pseudo-header)
什么是伪头部
这是一个虚拟的头部字段,仅用于校验和的计算,并不实际在网络上传输,一共包含以下字段,每个字段都有其对应的功能
- 源 IP 地址 (Source IP Address) - 4 字节:确保 TCP 段确实来自预期的发送方
- 目标 IP 地址 (Destination IP Address) - 4 字节:确保 TCP 段确实是发往预期的接收方。这是伪头部中最重要的字段之一,主要用于防止错误投递
- 协议字段 (Protocol) - 1 字节 (对于 TCP,值为 6):确保 TCP 段确实应该由 TCP 协议栈来处理,而不是其他传输层协议
- TCP 长度 (TCP Length) - 2 字节 (表示 TCP 头部长度 + TCP 数据长度):确保 TCP 协议栈接收到的 TCP 段的长度(包括 TCP 头部和 TCP 数据)与发送方发送时声明的长度一致
- 一个全零的字节 (Zero) - 1 字节 (用于凑齐偶数长度):这个字段在 IPv4 的伪头部中主要用于填充,使得伪头部的总长度为 12 字节,这是一个偶数,方便进行 16 位字的校验和计算。同时,它也确保了伪头部结构的一致性。
为什么要加入伪头部
考虑以下场景:一个 TCP 数据段在网络传输过程中,其 IP 头部可能因为某种原因(例如路由器故障、配置错误)被损坏或修改,导致这个 TCP 段被错误地路由到了一个非预期的目标主机。但是如果 TCP 校验和只计算 TCP 头部和数据部分,那么即使这个 TCP 段被送到了错误的主机,只要 TCP 头部和数据本身没有损坏,校验和仍然可能是正确的。
然而错误的主机可能会尝试处理这个不属于它的 TCP 段,这可能导致不可预料的行为或安全问题。
通过在伪头部中包含目标 IP 地址,校验和的计算就将目标地址也纳入了保护范围。如果 IP 头部中的目标地址与伪头部中的目标地址不符(这意味着数据段被错误路由),即使 TCP 头部和数据本身完好,接收端的 TCP 校验和计算(使用其本地 IP 地址构造伪头部)也会失败。这将导致该 TCP 段被丢弃,从而防止了对错误投递数据段的处理。
具体的字段及其作用可参考上文。
TCP 头部 (TCP Header)
TCP 协议的完整头部,包括源端口、目标端口、序列号、确认号、标志位、窗口大小、选项等。
==注意,在计算校验和之前,TCP 头部中的校验和字段本身会被置为 0==
TCP 数据 (TCP Data / Payload)
TCP 段中承载的应用层数据,如果数据部分的长度为奇数个字节,会在末尾逻辑上添加一个全零的字节 (padding byte) 来凑成偶数个字节进行计算,但这个填充字节并不会实际发送。(可参考
TCP 伪头部的末尾全0字节)
校验和的计算方法
TCP 校验和采用的是一种称为 “16位反码算术和的反码” (One’s Complement of the One’s Complement Sum of 16-bit Words) 的算法。步骤如下:
数据拼接
首先将伪头部、TCP头部(校验和字段置零)和TCP数据(如果是奇数字节,需要在末尾填充一个零字节)连接起来,形成一个连续的字节序列,将其看作是一系列16位的字(word)
计算16位反码和
将所有的这些16位的字进行反码加法 (One’s Complement Addition)
反码加法的规则:
- 二进制相加
- 如果最高位产生进位,则将这个仅为加回到结果的最低为(
回卷或循环进位) - 重复1,2,直到所有的16位的字都加完,得到一个16位的和
取反码
将上一步得到的16位和进行按位取反 (One’s Complement),最后得到的结果就是TCP的校验和
填充校验和字段
校验和的验证过程(接收端)
数据构造
和校验和生成机制一样,验证过程也需要构造出伪头部字段,将伪头部、收到的TCP头部(此时校验和字段包含发送方计算的值)和TCP数据连接起来,同样将这个序列看作一系列的16位的字
计算16位的反码和
对所有这些16位的字(包括TCP头部中收到的校验和字段的值)进行反码加法
验证结果
先说结果,如果数据在传输过程中没有发生错误,那么接收端计算出的 16 位反码和应该全为 1 (即二进制 1111111111111111,十六进制 0xFFFF)。
为什么?
我们这里将接收端接收到的TCP报文分为这几个部分:
- 伪头部(由接收端自己构造的)
- TCP除校验和字段之外的部分
- TCP 数据(TCP Data)
- 发送端计算并填充的校验和字段(Checksum_S)
对于接收端的反码计算,我们可以把前三个部分(伪头部 + TCP头部(除校验和字段) + TCP数据)计算出来的反码和,即
Sum_S视为一个整体,那么接收端计算的Sum_R可以近似的看作是Sum_R = 反码加法(Sum_S, Checksum_S)而又因为
Checksum_S = NOT(Sum_S),代入上式,可得Sum_R = 反码加法(Sum_S, NOT(Sum_S)),根据反码计算性质,可得如果数据传输没有问题的话,结果总是全1的,即结果为OxFFFF处理错误
如果校验和验证失败(结果不是全1),接收方会静默丢弃该 TCP 段。它不会发送 NACK (Negative Acknowledgment)。
而发送方最终会因为没有收到对该数据段的 ACK(或者因为后续数据段的 ACK 表明该段丢失)而触发重传机制(超时重传或快速重传)。
连接管理
连接管理概述
TCP (Transmission Control Protocol) 是一种面向连接的、可靠的、基于字节流的传输层通信协议。其“面向连接”的特性就体现在它的连接管理机制上。
TCP连接管理主要包括三个阶段,具体来说:
- 连接建立 (Connection Establishment) - 三次握手 (Three-Way Handshake)
- 数据传输 (Data Transfer)
- 连接终止 (Connection Termination) - 四次挥手 (Four-Way Handshake)
后文就会对这三个阶段的具体行为进行介绍。
连接建立阶段
连接建立阶段会经过三次握手,我们分为行为层和状态变化层这两个层面进行介绍:
行为层
TCP连接的建立分为三步——即对应三次握手
- 首先客户端发起连接请求(注意,一定是客户端发起的连接请求),根据上面的
标志位,可以查到当前标志位应该被设置为SYN,向服务端发起请求; - 服务端接收到连接请求(注意,在此之前,服务端一直处于LINSTEN状态,这表示一种监听状态,一旦有连接请求,服务端会立即执行对应操作),先对请求进行确认,随后会向客户也发送一个连接请求(注意,这里为了节省报文发送的数量,采取了捎带应答(后面会谈到),将ACK和SYN并作一起发送给客户端;
- 客户端在接收到了对应的连接请求后,也向服务端发送ACK确认报文,至此,TCP建立连接的三次握手完成;
状态层
- 首先
Client发送SYN连接请求,进入SYN_SENT状态; Server接收到连接请求,向Client同样发送SYN连接请求,同时捎带ACK确认报文(称为捎带应答),从LISTEN状态转变为SYN_RCVD状态;Client接收到Server的确认报文后,从SYN_SENT状态转变为ESTABLISHED状态,说明Client的TCP连接已经建立完成,并向Server发送ACK确认报文;Server接收到ACK后,从SYN_RECD状态转变为ESTABLISHED状态;
自此,TCP的三次握手完成,TCP连接正式建立;
// TODO 图片
连接断开阶段
因为TCP是全双工通信,所以断开连接必须要双端分别发起一次断开请求,只有一方断开连接并不是真正意义上的TCP连接断开行为,正因为如此,TCP断开请求需要4步
行为层
首先,一方(这里是客户端还是服务端没有严格要求)发起断开请求(对应标志位为FIN)
当另一方接收到请求报文时,会向对端发送一个确认报文(ACK);
这时,接收到报文的一方也会向其对端发送断开请求(同样是FIN);
另一方接收报文并返回确认报文(ACK)
为什么这里不能像连接建立阶段那样,进行捎带应答呢?
因为不同于建立连接时,双端都未进行通信,连接中没有任何残留数据(不考虑上一次TCP连接的残留),双方的目的就是为了尽快建立连接,所以采用了捎带应答机制;
而对于断开连接来说,一端发送断开请求,只是意味着此时请求断开的一端没有数据需要发送,但是它还可以继续接收数据,此时的连接中可能会存在由对端发送来的数据,而TCP作为可靠的传输协议,必须等待数据传输完后才可以发送断开请求,从而保证数据的安全、可靠传输,不会发生丢失等问题;
至此,TCP四次挥手过程完成,TCP连接断开
状态层
Client发送FIN请求,请求断开连接,从ESTABLISHED状态转为FIN_WAIT1状态;Server接收到FIN请求,状态变为CLOSE_WAIT,随后向Client发送ACK确认报文Client接收到ACK确认报文,从FIN_WAIT1状态变为FIN_WAIT2状态随后由
Server向Client发送FIN请求,状态转为LAST_ACKClient接收到对应请求,注意此时Client端将保持当前状态,随后向Server发送ACK确认报文,进入TIME_WAIT状态Server在接收到ACK确认报文后,进入CLOSED状态,此时Server的TCP连接关闭,释放连接资源在发送ACK确认报文后,
Client会进入2MSL等待状态(MSL: Maximum Segment Lifetime,报文最大生存时间)
为什么需要经历
2MSL的等待时间?确保发送的最后一个ACK能够到达服务器。如果这个ACK丢失,服务器会因为没收到ACK而重传FIN,客户端在
TIME_WAIT状态下可以重新发送ACK。防止“已失效的连接请求报文段”出现在本连接中。等待2MSL可以确保本次连接中产生的所有报文段都从网络中消失。
至此,双方都完成了TCP连接的关闭,正式断开
- 标题: TCP协议是如何实现可靠性传输的
- 作者: The Redefine Team
- 创建于 : 2024-12-17 13:14:00
- 更新于 : 2025-05-26 22:21:05
- 链接: https://redefine.ohevan.com/2024/12/17/详解TCP协议的可靠性传输实现/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。