TCP 协议(一)报文结构
TCP 协议(二)连接与断开
TCP 协议(三)十种核心机制
TCP 协议(四)传输异常
TCP 十种核心机制
1.确认应答(可靠机制)
2.超时重传(可靠机制)
3.连接管理(可靠机制)
4.滑动窗口(效率机制)
5.流量控制(效率机制)
6.拥塞控制(效率机制)
7.延时应答(效率机制)
8.捎带应答(效率机制)
9.面向字节流(粘包处理)
10.特殊情况(异常处理)
1.确认应答(可靠机制)
在 TCP 协议中,发送方发送数据后,接收方需要对数据进行确认应答(ACK acknowledge的缩写),以确保数据已经被正确接收。如果发送方没有收到确认应答,就会重新发送数据,直到接收方发送确认应答为止。这样可以保证数据的可靠传输,避免数据丢失或损坏。TCP 进行可靠传输,最主要的就是靠这个确认应答机制。
同时,TCP还采用了序列号和确认号的机制,用于保证数据的顺序和完整性。
1.1.后发先至(乱序问题)
“小叶”给“小彭”发送了个消息,“小彭”收到之后就会返回给“小叶”一个应答报文,此时,“小叶”收到应答报文之后,就知道刚才发送的消息“小彭”已经成功接收到了。但如果隔了一段时间后,还是没有收到“小彭”发送的应答报文,那就说明刚刚“小叶”发送的消息丢包了,也就是消息没有成功发送出去。
现在考虑复杂一点的情况,“后发先至”的问题:如果“小彭”连续发送两条信息,“小叶”收到消息的顺序是可能存在变数的。(网络中数据的后发先至,主要是因为两个主机之间,路线存在很多条,数据报文1 和 数据报文2 走的是不同的路线,此时,这两数据报文到达的顺序就存在变数了。)
很明显,比对图后半部分原始的应答含义,图前半部分的应答就错乱了,出现了应答歧义了。这种网络后发先至现象是客观存在的,不可避免,因此应答报文到达的顺序也可能发生变动,此时就需要考虑方法来规避这种顺序错乱带来的歧义。
解决上述“后发先至”问题的方法就是给发送报文和应答报文进行编号。
当我们引入序号之后,就不再怕顺序错乱了,即使顺序错乱了,也可以通过确认序号来区分当前应答报文是响应哪个数据的了。
PS:任何一条数据(传输数据和应答报文)都是有序号的,但是,确认序号,只有应答报文有(普通报文里确认序号字段里的值无意义)。一条报文是不是应答报文,取决于 TCP 报文首部 ACK 这个标志位,如果标志位1,就是应答报文,否则不是。
1.2.TCP 的序列号和确认号
在真实的网络传输中,TCP 并不是以“消息”为单位进行编号的,而是以字节为单位进行编号的,也就是在我们发送的消息中对消息的每一个字节进行编号,编号称为序列号,发送方发送的报头里面会包含序号,接收方会返回一个 ACK 确认号,发送方通过这个 ACK 确认号就可以知道接收方那些数据收到了,哪些数据没收到。 这也就是 TCP 中的确认应答机制。
假设主机A的一条发送数据中,所有字节的编号是从1开始的,第一个字节编号1,第二个字节编号2…最后一个字节编号1000,由于这1000个字节同在一个 TCP 报文中发送,属于同一个TCP 报文,该TCP 报头就只记录当前发送数据的第一个字节的编号,也就是说,此处报头序号标记为1。
主机B应答序号为1001,可理解为:1. 主机A发送的编号< 1001 的数据都已经收到了。2. 接下来,A主机应该从 1001 这个编号发送新数据。(B向A索要1001及以后的数据)
主机A继续给主机B发送第二条数据,此时数据的第一个字节编号为1001,则第二个 TCP 报头的序号标记为1001。假设数据长度还是1000字节,该数据最后一个字节的编号就是2000,应答报头确认序号就是2001。
确认序号的取值,是接收到的数据的最后一个字节的编号+1。
1.3.累计确认或累计应答
需要注意的是:TCP 是提供累计确认这一种机制的,即 TCP 确认该报文流中至第一个丢失为止的字节序号。
比如:接收方发送的确认号为3001,他不仅仅代表2001-3000这个包收到了,还代表0-2000的包也收到了,但没收到3001-6000的字节。这种方式叫累计确认或累计应答。
1.4.小结
TCP 可靠传输能力,最主要的就是靠确认应答机制来保证的。
通过应答报文,就可以让发送方清楚的知道数据是否传输成功。
进一步的引入了序号和确认序号,针对多组数据进行详细的区分。
2.超时重传(可靠机制)
超过一定时间,还没响应,就重新传输。
确认应答考虑的是数据成功传输的情况,如果数据传输过程中丢包了呢?这就是超时重传机制要解决的问题。
丢包涉及到两种情况:
1).发的数据丢了
2).返回的 ACK 丢了
对于发送方而言,就是没有收到 ACK,区分不了是哪种情况。此时,就都会认为是丢包了。
丢包是一个概率性问题(通常情况下,丢包的概率是比较小的)因此,如果重新发一下该数据报,还是有很大的概率传输成功的。因此,TCP就引入了重传机制,在丢包的时候,就要重新发送一次同样的数据。
那么怎么去判断当前这次传输,是丢包了,还是因为 ACK 走的慢,正在路上呢?TCP 引入一个时间阈值,发送方发送一个数据后,就会等待 ACK,此时开始计时,当超过这个时间阈值后,不管是哪种情况,还没有响应,就重新传输。
往返时间段的预估和超时时限的设置
TCP 是如何预估往返时间并设置超时的实现的呢?我们首先要清楚,TCP不会抽取所有的样本,而是在我们成功建立的TCP连接中取一个不超时的样本来获取往返时间SampleRTT,同时会动态更新。 同时,TCP会对SampleRTT取一个均值EstimatedRTT,并利用公式:E=(1-a)E+aS 来获取往返时间。而对于超时时限,必须满足大于往返时间这个条件。
关于超时的时间该如何确定这个问题,其实 Linux 中会以 500ms 为单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍,累积到一定的重传次数,TCP 就会认为网络或者主机端出现异常,强制关闭连接。
2.1.重复传输问题
超时重传,又会带来一个新问题:导致重复的消息,接收方可能收到多次。
TCP 对于这种重复数据的传输,是有特殊处理的。保证不会重复传输同样的数据。TCP 存在一个 “接收缓冲区” (接收方操作系统内核的一段内存),每个 TCP 的 socket 对象,都有一个 “接收缓冲区” (其实也有一个发送缓冲区),主机B 收到主机A 的数据,其实就是B的网卡读取到数据了,然后把这个数据放到B 的对应的 “接收缓冲区” 里。然后在 “接收缓冲区” 中(可以想象成一个阻塞队列),根据数据的序号,TCP 很容易识别当前缓冲区里的这两条数据是否是重复的,如果重复了,就把后来的这份数据丢弃了,保证应用程序调用 read 读取到的数据,一定是不重复的。
TCP 使用这个 “接收缓冲区”,对收到的数据进行重新排序,使应用程序 read 到的数据是保证有序的(和发送顺序一致)。
由于去重和重新排序机制的存在,发送方只要发现 ACK 没有按时到达,就会重传数据,即使重复了,顺序乱了,接收方也能很好的处理(去重和排序都依赖TCP报头的序号)。
2.2. 多次重传问题
重传的数据是否可能又丢包,那必然是会有可能的,因此超时重传是可能重传 N 次的,此处的 N 并非是一个很大的数字。对于重传这个事情,当你重传几次都传不出去,此时继续重传,意义已经不大了。因为一般重传丢包的概率是不大的,多次重传失败,可能就是基础设施的问题了。
假设一次传输丢包的概率是 10%,那么连续两次丢包的概率就是 1%,连续三次丢包的概率就是 0.1%,所以说,连续重传丢包,此时的概率原则上讲,是很低的。如果真的出现了这种情况,也只能说明,此时丢包的概率是远不止 10%的,说明网络大概率是出现了故障。
因此,重传达到一定的次数后,就不会继续重传了,会认为是网络故障。接下来 TCP 会尝试重置连接(相当于断开重连),如果重置还是失败,就彻底断开连接。
**在重传的时候,第一次重传和第二次重传,中间的等待时间还不一样,一般来说,重传的轮次越大,这个等待时间也就越大。(每激发一次重传都会出现超时时限加倍)**因为等待时间越大,重传失败的概率就越低,前面重传失败了,也就说明了重传成功的概率并不高(相对而言),所以此时重传的等待时间太快也是白浪费系统资源。
2.3.小结
可靠传输是 TCP 最核心的部分,TCP 的可靠传输主要通过 确认应答 + 超时重传 来体现的,其中确认应答描述的是传输顺利的情况,超时重传描述的是传输出问题的情况,这两者互相配合,共同支撑整体的 TCP 可靠性。
注:
TCP 的可靠性是通过三次握手来保证的,这句话是错误的!!!
三次握手与 TCP 的可靠性有关,但是起到的效果是与超时重传和确认应答相比是微乎其微的。
3.连接管理(可靠机制)
TCP 建立连接,例如主机A和主机B建立连接,A就需要有一个空间存储着B的IP和端口,B就需要有一个空间存储着A的IP和端口。当这两部分信息被维护好了之后,此时连接就有了,同时也把保存这部分信息的空间(数据结构)称为连接。
进一步,断开连接就是 A和B 把自己存储的连接信息(数据结构)删了,连接就断开了。
管理:就是描述了连接如何创建,如何断开…
TCP 建立连接的过程是:三次握手
TCP 断开连接的过程是:四次挥手
由于三次握手和四次挥手比较重要,放在TCP 协议(二)连接与断开专门介绍。
4.滑动窗口(效率机制)
确认应答,超时重传,连接管理,都是给 TCP 的可靠性机制。
引入可靠机制,实际上是付出了一定代价的,消耗了传输效率,来补全可靠性。因此,虽然 UDP 没有可靠性,但是其传输效率是要比 TCP 要高的。
所以就引入了滑动窗口,其本质上就是降低了确认应答,等待 ACK 消耗的时间,来提高传输效率的。(进行IO操作的时候,其实时间成本主要是两个部分:1.等(大部分);2.数据传输)
具体的操作也就是:批量发送,批量等待,把多份等待时间,合并成一份。
4.1.原理分析
对于基本的确认应答的情况来说,每次发送一条数据,都需要等待 ack 到了,才能发送下一条。
在这里插入图片描述
而滑动窗口的本质就是不等待的批量发送一组数据,然后使用同一份时间来等待该组数据的多个 ACK。把不需要等待,就能直接发送的数据的最大量,称为 “窗口大小” ,下图中的窗口大小就是4000。
当批量发送了窗口大小的数据之后,发送方就要等待 ACK 了。那么发送方什么时候继续往下发送呢?这里并不是说等待所有的 ACK 都到达后才继续发送,而是等到一个 ACK 了,就继续往下发送。 上图中就是只要等到了一条 ACK,就继续往下发送数据,使得每次等待的 ACK 始终是 4 条。
如上图:1-1000已经确认收到,主机A发送了1001-2000,2001-3000,3001-4000,4001-5000一组四个数据。
当前等待 ACK 范围为:2001,3001,4001,5001一组四个确认,不需要等到5001到了,才继续向下发送数据。
如果 ACK 2001到了就可以发送下一组数据了(5001-6000),此时等待的 ACK 范围为3001,4001,5001,6001。窗口向后滑动一。
扩展分析:
如果 ACK 5001到了就可以发送下一组数据了(7001-8000,8001-9000),此时等待的 ACK 范围为6001,7001,8001,9001。(TCP 是提供累计确认这一种机制的)
4.2.丢包情况1:ack丢了
确认序号的含义:表示该序号往前的所有数据都已经确认到达了。
所以此时是不需要做什么处理的,部分 ACK 丢了并不要紧,因为可以通过后续的 ACK 进行确认;例如:确认序号为1001的 ACK 丢了,没关系,确认序号为2001的 ACK 会判定 1-2000 的数据已经被累计确认。此时就可以发送4000-6000的数据,也就意味着此时等待 ACK 的范围是 2001~6000(2001,3001,4001,5001)。
所以实际上,这里的 ACK 有可能不是老老实实的全部发送的,可能会少发一些 ACK,在不影响可靠性的前提下,节省系统资源。而且 ACK 不至于都丢完,如果丢包概率很大,也只能说明网络环境已经出现了严重故障了。
2.3.丢包情况2:数据丢了(快速重传)
当某一段报文段丢失(1001-2000)之后,发送端会一直收到 1001 这样的 ACK,就像是在提醒发送端 “我想要的是 1001 的数据” 一样,来提醒发送方,没有收到 1001 开头的数据。
如果发送端主机连续三次收到了同样一个 “1001” 这样的应答,就会将对应的数据 1001-2000 重新发送;
这个时候接收端收到了 1001 之后,再次返回的 ACK 就是7001了(因为2001 - 7000 接收端其实之前就已经收到了),被放到了接收端操作系统内核的接收缓冲区中;
此时后面补发丢失的数据,是否会造成数据的顺序错乱呢?答案是不会的,TCP中会有一个接收缓冲区,会在缓冲区中按照序号重新排序的。
PS: ACK 其实也可以理解成向发送方 “索要” 对应序号的数据。
上述丢包重传的方式,也称为 “快速重传”,也可以视为是超时重传机制,在滑动窗口下的变形。如果当前传输数据密集,按照滑动窗口的方式来传输,此时按照快速重传来处理丢包;如果当前传输密集稀疏,不再按照滑动窗口方式了,就会按照之前的超时重传处理丢包。
5.流量控制
流量控制是一种干预发送的窗口大小的机制。
滑动窗口,窗口越大,传输效率就越高(一份时间等的ack就多了),但是,窗口的大小不能无限大。
1).完全不等 ACK,可靠性的保障就会出现问题。
2).窗口太大也会消耗太多的系统资源。
3).发送方发送的太快,接收方可能处理不过来,此时发了也白发。
流量控制要做的工作就是第三点,根据接收方的处理能力,协调发送方的发送速率。
衡量接收方的处理能力
如何衡量接收方的处理能力呢?直接看接收方的“接收缓冲区”的剩余大小!!!
每次主机A给主机B发送个数据,B就需要计算一下“接收缓冲区”的剩余大小,然后把这个值,通过 ACK 报文返回给A,A就根据这个返回的值决定接下来的发送速率是多少,也就是窗口大小是多少。这个窗口大小也就是之前 TCP 报文结构报头中的窗口大小(报文是 ACK 的时候,才有效)
16位窗口大小,并不是意味着,窗口的大小最大是64Kb。TCP 为了让窗口更大,在选项部分引入了窗口扩展因子.
比如窗口的大小已经是64kb,窗口扩展因子中写了个2,意思就是让 64kb << 2 = 256kb。
发送方的窗口大小并不是固定的,而是随着传输过程的进行,动态调整的。
当窗口大小为0的时候,发送方就要暂停发送数据,开始等待 ACK,在等待 ACK 的过程中,会给B发送窗口探测报文,这个报文不具备任何业务逻辑,只是为了触发 ACK 查询窗口大小.
6.拥塞控制(效率机制)
流量控制和拥塞控制共同决定发送方的窗口大小是多少。
流量控制考虑的是接收方的处理能力;
拥塞控制描述的是传输过程中中间节点的处理能力;
发送方按照滑动窗口的方式发送,此处的"窗口大小"描述了发送速率;
窗口大小只是"发送方"的概念,只不过这个窗口大小,是通过接收方的 ack 报头里的窗口大小字段来决定的,从接收方告诉发送方的。
更加详细参考一下:
TCP 拥塞控制技术与 BBR 的加速原理
6.1.拥塞控制逻辑
在5.流量控制,对A的发送速率的分析中,只是考虑了B的处理能力,而没考虑,中间节点的处理能力。对于中间节点来说,它们的处理能力是不好进行直接衡量的。 因此TCP就采取 “实验” 的方式,来测出一个合适的值。(拥塞控制本质上就是通过实验的方式,来逐渐找到一个合适的窗口大小,也就是一个合适的发送速率)
刚开始的时候,窗口大小为1,以非常慢的速度进行发送数据(此处的1不是1个字节,而是1个单位,一个单位具体代表多少个字节,这里不多过多讨论),发现传输顺利,没丢包,就扩大窗口。初始阶段,由于窗口比较小,每一轮不丢包都会使窗口扩大一倍,呈指数增长。当增长到阈值 ssthresh 的时候,就开始线性增长。注意:增长的前提是不丢包。
再接下来,当传输过程中发生网络拥塞了,就说明此时发送的速率已经接近网络的极限了,此时就把窗口大小一下缩成很小的值(重复刚才的指数增长和线性增长的过程),而且阈值也会随之变化。(少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞)
所以拥塞窗口不是固定数值,而是一直动态变化的,随着时间的推移,逐渐达到一个动态平衡的过程。
拥塞窗口的大小为 cwnd,流量控制窗口的大小为 rwnd,共同决定了发送方实际的发送窗口,需要取它俩的最小值。窗口大小 = min(cwnd, rwnd)
6.2.拥塞控制算法
拥塞控制算法主要包括三个状态:慢启动、拥塞避免和快速恢复,其中慢启动和拥塞避免是必不可少的。我们把拥塞窗口长度记为 cwnd,往下看这三个概念:
慢启动
在慢启动的状态下,cwnd 以一个 MSS 开始,每当有一个确认报文段反馈回来就会增加一个 MSS,所以从首次开始,一次 RTT 内发出的报文段呈倍数递增:1,2,4,8…。结束这种指数增长的情况有:
一是超时丢包事件,TCP 发送方会将 cwnd 重置为1并将状态变量 ssthresh设置为 cwnd/2;
二是当变量 ssthresh 设置为 cwnd/2 时,如果 cwnd 等于 ssthresh 时,会结束慢启动并转移到拥塞避免模式;
三是丢包后的快速重传并直接进入快速恢复状态。
拥塞避免
一旦进入拥塞避免状态,cwnd 会变成原来的一半,并且在一个 RTT 内到达新的确认就只会增加一个 MSS。这种线性增长在遇到超时后,与慢启动一样会将 cwnd 设置为一个 MSS 并将状态变量 ssthresh 设置为 cwnd/2。如果时冗余 ACK 事件指示的丢包,反应程度不会那么剧烈,只是将 cwnd 减半并设置 ssthresh 值为 cwnd/2,然后进入快速恢复状态。
快速恢复
在快速恢复状态中,对于引起 TCP 进入快速恢复状态的 ACK 报文段,对收到每一个冗余的 ACK,cwnd 都会增加一个 MSS,当丢失报文段的一个 ACK 到达时,TCP 会在降低 cwnd 后进入拥塞避免状态。如果出现超时事件,快速恢复在执行如同慢启动和拥塞避免相似的动作后会进入慢启动状态;当丢包事件出现时,执行如同慢启动和拥塞避免相似的动作。
7.延时应答(效率机制)
延时应答,也是提高效率的机制,也是建立在滑动窗口的基础上的。
滑动窗口的关键,让窗口大一点,传输速率就快一点,因此,可以做到的是,在接收方能够处理的前提下,尽可能把窗口大小放大一点。
**延时:**指的就是收到数据后,不是立即就放回 ACK 了,而是稍微再等等,在这个等待的时间里,接收方的应用程序,就可以把缓冲区里的数据给处理一下,此时接收缓冲区里的剩余空间就大大提高了。
在实际上,延时应答采取的方式,就是在滑动窗口下,ACK 不再是每一条数据都返回了。因为前文也说明过,确认序号表示的意思也就是,该序号前的数据已经顺利获取。
8.捎带应答(效率机制)
捎带应答,也是一种提高效率的方式,在延时应答的基础上,加上捎带应答。
前文中讲解过,A对B 发起业务请求,ACK 是内核立即返回的,但是有时候,在 ACK 返回后,B也可能需要对A发起业务请求。这两本身是不同的时机的,但是由于 TCP 存在上述的延时应答,就导致等待 ACK 的过程中,B就要给A发送业务请求了,就可以让业务请求捎带这个 ACK 一起发送出去。
也就是本来是不同时机的两条数据,在延时应答下,可能成为相同时机的数据,就合并了一起发送了。捎带应答本身说的就是 “能合并” 这件事,延时应答提高了合并的概率。
9.面向字节流
面向字节流,就会涉及到一个麻烦事:粘包问题。
接收缓冲区中,其实就是把刚才收到的数据都放在一起,那么当应用程序 read 读取的时候,一次读取N个字节,这就导致可能一次读到的数据是半个应用层数据报,也可能是一个应用层数据报,还可能是多个应用层数据报。
所以对于粘包问题,解决方法也很简单,可以对应用层协议进行约定:1. 约定好分隔符;2. 约定好每个包的长度。
设定包的长度, 约定每个应用层数据包的前四个字节来存储数据包的长度, 如 “9helloword”, 应用程序会先读前四个字节读到了 9 这个字节, 然后继续往后读 9 个字节就是一个完整的应用层数据包。
10.特殊情况(异常处理)
异常情况,表示的就是传输过程中出现了不可抗力。
10.1.进程崩溃,主机关机(按照正常流程关机)
进程没了,对应的 PCB 也就没了,对应的文件描述符表也就释放了,相当于是 socket.close(),此时内核会继续完成四次挥手操作,此时其实就是一个正常断开的流程。主机关机的时候,是需要杀死进程的,然后才正式关机,在杀死进程的过程中,就和上述情况是一样的,触发四次挥手。
10.2.主机掉电,网线断开
这种情况下,很显然是来不及进行四次挥手的。
假设是接收方掉电了:
此时发送方仍然在继续发数据,发完数据需要等待 ACK,但是 ACK 等不到了。从而超时重传,也等不到 ACK,重传几次,依然没有应答,尝试重置 TCP 连接 ,重置失败,从而放弃连接。
走超时重传来放弃连接。
假设是发送方掉电了:
接收方发现,没数据了,此时就不确定是发送方挂了,还是需要准备时间来发送。此时接收方就会先等,然后周期性的给发送方发送一条消息,来确认对方是否还正常工作。这个消息也称为心跳包。心跳包是用来确认通信双方是否处于正常的工作状态中,并不存在业务数据。如果通过心跳包发现对方没有任何回应,也就会断开连接。
通过心跳包来发现连接异常来放弃连接。
通过心跳包你来维护连接这个过程也称为保活机制。
参考:
https://blog.csdn.net/weixin_69417428/article/details/129319636
https://blog.csdn.net/qq_54469537/article/details/129158695
https://zhuanlan.zhihu.com/p/516348349