本文解释了TCP为何能保证数据传输的可靠性,以及如何保证整个网络的顺畅。
1 网络分层模型
这是一切的本质。网络被设计成分层的,所以网络的操作就可以称作一个“栈”,这就是网络协议栈的名称的由来。在具体的操作上,数据包最终形成的过程就是一层一层封装的过程,在栈上形成一段连续的数据,我们可以称作是一层一层的push操作。同样的,数据包的解封装的过程,则可以认为是一层一层的pop操作。
1.1 网络包的发送流程
- 应用程序发送数据,调用send方法来将数据发送出去,此时便会触发系统调用来发送数据
- 将用户数据从【用户空间】拷贝到【内核空间】,
- 将数据封装为一个个【skb结构】(skb可以简单理解为一个封装待发送数据的数据结构,在内存层面待发送的数据都是以skb来表示并传递的。分配skb时会分配一个大小包含了所有协议头和数据长度的空结构,也就是最终发送出去的以太网帧的大小,随后经过不同协议层,不断的在skb中填充数据,为了防止不断扩充数组带来的消耗)
- skb进入协议栈进行处理(分别经过传输层、网络层)
- 然后skb会被传送到网卡【传输队列RingBuffer】里(网卡有多个队列,那么就有多个RingBuffer,并且每个队列对应一个发送队列一个接收队列),RingBuffer中存储的是skb的指针
- 网卡将RingBuffer里面的数据真实的发送到网络上
- 当网卡发送完数据后,网卡会向CPU发出一个数据发送完毕的【硬中断】
- CPU响应该硬中断,找到硬中断处理函数,在硬中断处理函数里会发出软中断,然后由【内核线程ksoftirqd】去响应并处理软中断,【内核线程ksoftirqd】找到该软中断对应的处理函数(网卡驱动启动时注册的),然后进行调用,在处理函数中会清理RingBuffer
- 等收到ack后会清理socket发送缓冲区
1.2 网络包的接收流程
网络包的接收流程主要涉及以下几个步骤:
- 网卡接收数据:当网络帧到达网卡时,网卡通过DMA(直接内存访问)方式将网络包放到收包队列中,并通过硬中断通知中断处理程序已经收到了网络包。
- 内核处理网络帧:网卡中断处理程序为网络帧分配内核数据结构(sk_buff),并将其拷贝到sk_buff缓冲区中。然后通过软中断通知内核收到了新的网络帧。
- 协议栈处理:内核协议栈从缓冲区中取出网络帧,并通过网络协议栈从下到上逐层处理这个网络帧。例如,在链路层检查报文的合法性,找出上层协议的类型(IPv4还是IPv6),去掉帧头、帧尾,然后交给网络层。
- 网络层处理:网络层取出IP头,判断网络包下一步的走向,如果是要发送到本机,则取出上层协议的类型(TCP或UDP),去掉IP头,再交给传输层处理。
- 传输层处理:传输层取出TCP头或UDP头后,根据<源IP、源端口、目的IP、目的端口>四元组作为标识,找出对应的Socket,并把数据拷贝到Socket的接收缓存中。
- 应用程序读取数据:最后,应用程序可以使用Socket接口读取到新接收到的数据。
1.3 socket缓冲区
TCP的socket缓冲区是TCP协议为了实现可靠的数据传输而在内存中分配的临时存储区域,每个TCP socket连接都有两个缓冲区:发送缓冲区(send buffer)和接收缓冲区(receive buffer)。只要建立TCP连接,这两个缓冲区就会默认建立。
- 发送缓冲区(Send Buffer): 发送缓冲区用于暂存应用程序打算通过TCP连接发送出去的数据。当应用程序调用send()或write()函数将数据写入socket时,数据并不会立即被发送到网络上,而是首先被放入发送缓冲区。TCP协议负责从缓冲区中取出数据,根据当前的网络状况和拥塞控制等因素逐步将数据发送到目标主机。如果发送缓冲区满,应用程序的写操作可能会被阻塞(在阻塞模式下)或者返回错误(在非阻塞模式下)。
- 接收缓冲区(Receive Buffer): 接收缓冲区用于存储从网络中接收到但尚未被应用程序读取的数据。当数据包从网络到达时,TCP协议将它们放入接收缓冲区,等待应用程序通过recv()或read()函数读取。如果缓冲区满了而新的数据继续到达,超出部分的数据可能会被丢弃,或者根据TCP的流量控制机制暂时停止对方发送更多数据,以避免数据丢失。
2 滑动窗口
2.1 窗口详情
滑动窗口协议属于TCP协议的一种应用,用于网络数据传输时的流量控制,以避免拥塞的发生。该协议允许发送方在停止并等待确认前发送多个数据分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输,提高网络吞吐量。
TCP通过滑动窗口来进行流量控制。设想在发送端发送数据的速度很快而接收端接收速度却很慢的情况下,为了保证数据不丢失,显然需要进行流量控制, 协调好通信双方的工作节奏。所谓滑动窗口,可以理解成接收端所能提供的缓冲区大小。TCP利用一个滑动的窗口来告诉发送端对它所发送的数据能提供多大的缓冲区。由于窗口由16位bit所定义,所以接收端TCP 能最大提供65535个字节的缓冲。由此,可以利用窗口大小和第一个数据的序列号计算出最大可接收的数据序列号。
滑动窗口本质上是描述接受方的TCP数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据。如果发送方收到接受方的窗口大小为0的TCP数据报,那么发送方将停止发送数据,等到接受方发送窗口大小不为0的数据报的到来。
TCP建立连接的初始,B会告诉A自己的接收窗口大小,比如为‘20’:字节31-50为发送窗口。
根据B给出窗口值,A构造自己的窗口
A发送11个字节后,发送窗口位置不变,B接收到了乱序的数据分组,因为最左侧31没收到,即使32、33收到了,给A返回的ack中的窗口依旧不变,也就是B只会返回收到的连续的字节数
只有当A成功发送了数据,即发送的数据得到了B的确认之后,才会移动滑动窗口离开已发送的数据;同时B则确认连续的数据分组,对于乱序的分组则先接收下来,避免网络重复传递,等31也收到后才会在ack中返回seq为34(下次接收的字节编号),同时窗口向右移动+3
如果34一直没收到,A会将剩余的窗口数据继续传过去,然后就不能在发送了
当B接收到所有数据后,假如应用程序一直未取数据,那就导致接收缓冲区中所有的数据都是待交付主机的状态,窗口也就是会变成0,ack就会为 seq=54,win=0
2.3 最大窗口限制
TCP的Window是一个16bit位字段,它代表的是窗口的字节容量,也就是TCP标准窗口最大为2^16 -1 = 65535个字节。
另外在TCP的选项字段中还包括了一个TCP窗口扩大因子,option-kind 为 3 ,option-length为 3 个字节(总字节数),option-data只占一个字节,取值范围 0 -14.窗口扩大因子用来扩大TCP窗口,可把原来16bit的窗口,扩大为31bit,这个在上一篇文章中解释过,详情请看 TCP详解(一)报文详情/MSS/MTU-CSDN博客
3 流量控制
所谓流量控制,主要是接收方传递信息给发送方,使其不要发送数据太快,是一种端到端的控制。主要的方式就是返回的ACK中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送
3.1 持续计时器
这里面涉及到一种情况,如果B已经告诉A自己的缓冲区已满,于是A停止发送数据;等待一段时间后,B的缓冲区出现了富余,于是给A发送报文告诉A我的rwnd大小为400,但是这个报文不幸丢失了,于是就出现A等待B的通知||B等待A发送数据的死锁状态。为了处理这种问题,TCP引入了持续计时器(Persistence timer),当A收到对方的零窗口通知时,就启用该计时器,时间到则发送一个1字节的探测报文,对方会在此时回应自身的接收窗口大小,如果结果仍未0,则重设持续计时器,继续等待。
TCP为每一个连接设有一个持续计时器。只有TCP的一方收到对方的零窗口通知,就启动持续计时器;只要持续计时器超时,就放送一个零窗口探测报文,携带一字节的数据;而对方收到零窗口探测报文时,回复自己现有的接收窗口值。如果窗口大小依旧是零,那么收到报文的一方就重新启动持续计时器。
持续计时器是为了解决双方相互等待(A等待B发送非零窗口的通知,B等待A发送数据)而形成的死锁现象。这种现象一般发生在发送窗口大小数据包丢失时。
3.2 RTT
RTT,全称Round Trip Time,即往返时间,数据发送时刻到接收到确认的时刻的差值。由链路传播时间、末端系统处理时间、路由器缓存中排队和处理的时间组成。
对于TCP来说,路由器缓存中排队和处理的时间会随着网络拥塞程度辩护而变化。通过计算RTT可以反应网络拥塞程度,从而拥塞控制。
3.3 RTO
RTO,全称Retransmission TimeOut,即重传超时时间即从数据发送时刻算起,超过这个时间便执行重传。。
超时之后TCP进入Loss状态,重传所有没有被确认的报文,同时进入慢启动的回复过程。
重传的时间RTO设定是非常重要的,如果设置太短,可能会导致并没有丢包而重传,如果设置太长了,可能因为等待ACK而浪费掉很多时间,牺牲传输的效率。从思想上来讲,其实我们还是希望重传的时间需要稍稍的大于RTT就可以了。但是这个RTT没有什么可以使用的定值,他是不断变化的,所以只能动态的进行设置,RTO只能是根据RTT来进行动态的设置
3.4 拥塞
随着网络上的主机不断增加其发送速率,会使整个网络变得非常拥挤;这会导致网络经常出现丢包现象,使网络传输效率大幅度下降。
如果不对网络做拥塞控制,会降低整个网络的传输效率
3.5 慢启动和拥塞控制
(1)一条TCP连接开始时,window size被设置为1 MSS(最大报文段大小)
(2)TCP发送方发送完发送窗口数据,并收到所有的确认,window size以指数增长(以2的倍数进行翻倍),即慢启动阶段。
(3)window size增长到一个慢启动的阈值thresh,开始执行拥塞控制算法(window size呈线性增长),进入拥塞控制阶段。
(4)随着window size增长,发送速率提高,出现网络拥塞,分组超时重传。比如丢包时,将慢启动门限设置为原来的一半,然后将cwnd设置为1,然后执行慢启动算法(起点较低,指数级增长)
上述方法的目的是在拥塞发生时循序减少 主机发送到网络中的分组数,使得发送拥塞的路由器有足够的时间去把队列中积压的分组处理完毕。
慢启动和拥塞控制算法常常作为一个整体使用,
3.6 快重传和快恢复
1990增加新的拥塞控制算法:快重传和快恢复。用于改进TCP的性能。
有时候,个别报文会在网络中丢失,当实际上网络并没有发生拥塞,这将导致发送方超时重传并认为网络出现了拥塞;从而错误的启动慢启动算法,因此降低传输效率。为此,引入快重传算法,可以让发送方尽早知道个别报文段的丢失。
所谓快重传,就是发送方尽快的进行重传,而不是等超时计时器超时才重传。快重传可以使整个网络吞吐量提高约20%。发送方接收到3个重复确认,就知道只丢失了个别报文段,于是不启动慢启动算法,而是执行快恢复算法。具体方式如下
- 当接受方建立这样的机制,如果一个包丢失,则对后续的包继续发送针对该包的重传请求
- 一旦发送方接收到三个一样的确认,就知道该包之后出现了错误,立刻重传该包;
- 此时发送方执行“快恢复”算法:①慢启动门限减半②CWND设为慢开始门限减半后的数值③执行拥塞避免算法(高起点,线性增长)
所谓快恢复,就是发送方将慢启动上限和拥塞窗口值调整为当前窗口的一半,开始执行拥塞避免算法。
快重传和快恢复是为了提高TCP传输的效率和可靠性