Linux网络TCP(下)(13)
如果没错的话,这应该会是我们关于 TCP 的最后一篇了总的来说, TCP 还是利用了很多机制来实现可靠性和提高性能的检验和。序列号。确认应答。超时重传。连接管理。流量控制。拥塞控制。滑动窗口。快速重传。延迟应答。捎带应答。重传定时器:为了控制丢失的报文段或丢弃的报文段,也就是对报文段确认的等待时间坚持定时器:专门为对方零窗口通知而设立的,也就是向对方发送窗口探测的时间间隔保活定时器:为了检查空闲连
前言
如果没错的话,这应该会是我们关于 TCP 的最后一篇了
一、流量控制
TCP 支持根据接收端的接收数据的能力来决定发送端发送数据的速度,这个机制叫做流量控制(Flow Control)。
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,此时发送端继续发送数据,就会造成丢包,进而引起丢包重传等一系列连锁反应
因此接收端可以将自己接收数据的能力告知发送端,让发送端控制自己发送数据的速度
- 接收端将自己可以接收的缓冲区大小放入 TCP首部 中的 窗口大小 字段,通过 ACK 通知发送端
- 窗口大小字段越大,说明网络的吞吐量越高
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端
- 发送端接收到这个窗口之后,就会减慢自己发送的速度
- 如果接收端缓冲区满了,就会将窗口值设置为0,这时发送方不再发送数据,但需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端
当发送端得知接收端接收数据的能力为 0 时会停止发送数据,此时发送端会通过以下两种方式来得知何时可以继续发送数据
- 等待告知。接收端上层将接收缓冲区当中的数据读走后,接收端向发送端发送一个 TCP报文 ,主动将自己的窗口大小告知发送端,发送端得知接收端的接收缓冲区有空间后就可以继续发送数据了。
- 主动询问。发送端每隔一段时间向接收端发送报文,该报文不携带有效数据,只是为了询问发送端的窗口大小,直到接收端的接收缓冲区有空间后发送端就可以继续发送数据了
16位数字最大表示 65535 ,那 TCP 窗口最大就是 65535 吗?
理论上确实是这样的,但实际上 TCP 报头当中 40字节 的选项字段中包含了一个 窗口扩大因子M ,实际窗口大小是窗口字段的值左移 M 位 得到的
第一次向对方发送数据时如何得知对方的窗口大小?
双方在进行 TCP通信 之前需要先进行三次握手建立连接,而双方在握手时除了验证双方通信信道是否通畅以外,还进行了其他信息的交互,其中就包括告知对方自己的接收能力,因此在双方还没有正式开始通信之前就已经知道了对方接收数据能力,所以双方在发送数据时是不会出现缓冲区溢出的问题的
二、滑动窗口
连续发送多个数据
双方在进行 TCP通信 时可以一次向对方发送多条数据,这样可以将等待多个响应的时间重叠起来,进而提高数据通信的效率

需要注意的是,虽然双方在进行 TCP通信 时可以一次向对方发送大量的报文,但不能将自己发送缓冲区当中的数据全部打包发送给对端,在发送数据时还要考虑对方的接收能力。
滑动窗口
发送方可以一次发送多个报文给对方,此时也就意味着发送出去的这部分报文当中有相当一部分数据是暂时没有收到应答的
其实可以将发送缓冲区当中的数据分为三部分:
- 已经发送并且已经收到ACK的数据
- 已经发送还但没有收到ACK的数据
- 还没有发送的数据
这里发送缓冲区的第二部分就叫做滑动窗口。(当然你把这三个部分当成一个整体的滑动窗口也行,哈哈)

而滑动窗口描述的就是,发送方不用等待 ACK 一次所能发送的数据最大量

滑动窗口存在的最大意义就是可以提高发送数据的效率:
- 滑动窗口的大小等于对方窗口大小与自身拥塞窗口大小的较小值,因为发送数据时不仅要考虑对方的接收能力,还要考虑当前网络的状况
- 我们这里先不考虑拥塞窗口,并且假设对方的窗口大小一直固定为 4000 ,此时发送方不用等待 ACK 一次所能发送的数据就是 4000字节 ,因此滑动窗口的大小就是 4000字节 (四个段)
- 现在连续发送 1001-2000、2001-3000、3001-4000、4001-5000 这四个段的时候,不需要等待任何 ACK ,可以直接进行发送
- 当收到对方响应的确认序号为 2001 时,说明 1001-2000 这个数据段已经被对方收到了,此时该数据段应该被纳入发送缓冲区当中的第一部分,而由于我们假设对方的窗口大小一直是 4000 ,因此滑动窗口现在可以向右移动,继续发送 5001-6000 的数据段,以此类推
- 滑动窗口越大,则网络的吞吐率越高,同时也说明对方的接收能力很强
当发送方发送出去的数据段陆陆续续收到对应的 ACK 时,就可以将收到 ACK 的数据段归置到滑动窗口的左侧,并根据当前滑动窗口的大小来决定,是否需要将滑动窗口右侧的数据归置到滑动窗口当中
TCP 的重传机制要求暂时保存发出但未收到确认的数据,而这部分数据实际就位于滑动窗口当中,只有滑动窗口左侧的数据才是可以被覆盖或删除的,因为这部分数据才是发送并被对方可靠的收到了,所以滑动窗口除了限定不收到 ACK 而可以直接发送的数据之外,滑动窗口也可以支持 TCP 的重传机制
滑动窗口一定会整体右移吗?
滑动窗口不一定会整体右移的,以刚才的例子为例,假设对方已经收到了 1001-2000 的数据段并进行了响应,但对方上层一直不从接收缓冲区当中读取数据,此时当对方收到 1001-2000 的数据段时,对方的窗口大小就由 4000 变为了 3000
当发送端收到对方的响应序号为 2001 时,就会将 1001-2000 的数据段归置到滑动窗口的左侧,但此时由于对方的接收能力变为了 3000 ,而当 1001-2000 的数据段归置到滑动窗口的左侧后,滑动窗口的大小刚好就是 3000 ,因此滑动窗口的右侧不能继续向右进行扩展

因此滑动窗口在向右移动的过程中并不一定是整体右移的,因为对方接收能力可能不断在变化,从而滑动窗口也会随之不断变宽或者变窄
如何实现滑动窗口?
TCP接收和发送缓冲区都看作一个字符数组,而滑动窗口实际就可以看作是两个指针限定的一个范围,比如我们用 start 指向滑动窗口的左侧, end 指向的是滑动窗口的右侧,此时在 start 和 end 区间范围内的就可以叫做滑动窗口
当发送端收到对方的响应时,如果响应当中的确认序号为 x ,窗口大小为 win ,此时就可以将 start 更新为 x ,而将 end 更新为 start + win

丢包问题
当发送端一次发送多个报文数据时,此时的丢包情况也可以分为两种
情况一: 数据包已经抵达,ACK丢包

在发送端连续发送多个报文数据时,部分 ACK丢包 并不要紧,此时可以通过后续的ACK进行确认
比如图中 2001 - 3000 和 4001 - 5000 的数据包对应的 ACK 丢失了,但只要发送端收到了最后 5001 - 6000 数据包的响应,此时发送端也就知道 2001 - 3000 和 4001 - 5000 的数据包实际上被接收端收到了的,因为如果接收方没有收到 2001 - 3000 和 4001 - 5000 的数据包,确认序号是不会为 6001 的,确认序号为 6001 的含义就是序号为 1 - 6000 的字节数据我都收到了,你下一次应该从序号为 6001 的字节数据开始发送
情况二: 数据包丢了。

- 当 1001 - 2000 的数据包丢失后,发送端会一直收到确认序号为1001的响应报文,就是在提醒发送端 下一次应该从序号为 1001 的字节数据开始发送
- 如果发送端连续收到三次确认序号为 1001 的响应报文,此时就会将 1001-2000 的数据包重新进行发送
- 此时当接收端收到 1001 - 2000 的数据包后,就会直接发送确认序号为 6001 的响应报文,因为 2001 - 6000 的数据接收端其实在之前就已经收到了
这种机制被称为“高速重发控制”,也叫做“快重传”
需要注意的是,快重传需要在大量的数据重传和个别的数据重传之间做平衡,实际这个例子当中发送端并不知道是 1001-2000 这个数据包丢了,当发送端重复收到确认序号为 1001 的响应报文时,理论上发送端应该将 1001-7000 的数据全部进行重传,但这样可能会导致大量数据被重复传送,所以发送端可以尝试先把 1001-2000 的数据包进行重发,然后根据重发后的得到的确认序号继续决定是否需要重发其它数据包。
滑动窗口中的数据一定都没有被对方收到吗?
滑动窗口当中的数据是可以暂时不用收到对方确认的数据,而不是说滑动窗口当中的数据一定都没有被对方收到,滑动窗口当中可能有一部分数据已经被对方收到了,但可能因为滑动窗口内靠近滑动窗口左侧的一部分数据,在传输过程中出现了丢包等情况,导致后面已经被对方收到的数据得不到响应
例如图中的 1001 - 2000 的数据包如果在传输过程中丢包了,此时虽然 2001 - 5000 的数据都被对方收到了,此时对方发来的确认序号也只能是 1001 ,当发送端补发了 1001 - 2000 的数据包后,对方发来的确认序号就会变为 5001 ,此时发送缓冲区当中 1001 - 5000 的数据也会立马被归置到滑动窗口的左侧

快重传 VS 超时重传
- 快重传是能够快速进行数据的重发,当发送端连续收到三次相同的应答时就会触发快重传,而不像超时重传一样需要通过设置重传定时器,在固定的时间后才会进行重传
- 虽然快重传能够快速判定数据包丢失,但快重传并不能完全取待超时重传,因为有时数据包丢失后可能并没有收到对方三次重复的应答,此时快重传机制就触发不了,而只能进行超时重传
- 因此快重传虽然是一个效率上的提升,但超时重传却是所有重传机制的保底策略,也是必不可少的
三、拥塞控制
两个主机在进行 TCP通信 的过程中,出现个别数据包丢包的情况是很正常的,此时可以通过快重传或超时重发对数据包进行补发。但如果双方在通信时出现了大量丢包,此时就不能认为是正常现象了
TCP不仅考虑了通信双端主机的问题,同时也考虑了网络的问题
- 流量控制:考虑的是对端接收缓冲区的接收能力,进而控制发送方发送数据的速度,避免对端接收缓冲区溢出
- 滑动窗口:考虑的是发送端不用 等待ACK 一次所能发送的数据最大量,进而提高发送端发送数据的效率
- 拥塞窗口:考虑的是双方通信时网络的问题,如果发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞
双方网络通信时出现少量的丢包 TCP 是允许的,但一旦出现大量的丢包,此时量变引起质变,这件事情的性质就变了,此时 TCP 就不再推测是双方接收和发送数据的问题,而判断是双方通信信道网络出现了拥塞问题
网络出现大面积瘫痪时,通信双方作为网络当中两台小小的主机,看似并不能为此做些什么,但“雪崩的时候没有一片雪花是无辜的”,网络出现问题一定是网络中大部分主机共同作用的结果
如果网络中的主机在同一时间节点都大量向网络当中塞数据,此时位于网络中某些关键节点的路由器下就可能排了很长的报文,最终导致报文无法在超时时间内到达对端主机,此时也就导致了丢包问题
当网络出现拥塞问题时,通信双方虽然不能提出特别有效的解决方案,但双方主机可以做到不加重网络的负担
双方通信时如果出现大量丢包,不应该立即将这些报文进行重传,而应该少发数据甚至不发数据,等待网络状况恢复后双方再慢慢恢复数据的传输速率
需要注意的是,网络拥塞时影响的不只是一台主机,而几乎是该网络当中的所有主机,此时所有使用TCP传输控制协议的主机都会执行拥塞避免算法
因此拥塞控制看似只是谈论的一台主机上的通信策略,实际这个策略是所有主机在网络崩溃后都会遵守的策略。一旦出现网络拥塞,该网络当中的所有主机都会受到影响,此时所有主机都要执行拥塞避免,这样才能有效缓解网络拥塞问题。通过这样的方式就能保证雪崩不会发生,或雪崩发生后可以尽快恢复

- TCP 除了有窗口大小和滑动窗口的概念以外,还有一个窗口叫做拥塞窗口。拥塞窗口是可能引起网络拥塞的阈值,如果一次发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞。
- 刚开始发送数据的时候拥塞窗口大小定义以为 1 ,每收到一个 ACK应答 拥塞窗口的值就加一。
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送数据的窗口大小,即滑动窗口的大小。
每收到一个 ACK应答 拥塞窗口的值就加一,此时拥塞窗口就是以指数级别进行增长的,如果先不考虑对方接收数据的能力,那么滑动窗口的大家就只取决于拥塞窗口的大小,就会呈现恐怖的 1 -> 2 -> 4 -> 8
- 为了避免短时间内再次导致网络拥塞,因此不能一直让拥塞窗口按指数级的方式进行增长。
- 此时就引入了慢启动的阈值,当拥塞窗口的大小超过这个阈值时,就不再按指数的方式增长,而按线性的方式增长。
- 当 TCP 刚开始启动的时候,慢启动阈值设置为对方窗口大小的最大值。
- 在每次超时重发的时候,慢启动阈值会变成当前拥塞窗口的一半,同时拥塞窗口的值被重新置为1,如此循环下去。

图示说明:
- 指数增长。刚开始进行 TCP通信 时拥塞窗口的值为 1 ,并不断按指数的方式进行增长。
- 加法增大。慢启动的阈值初始时为对方窗口大小的最大值,图中慢启动阈值的初始值为16,因此当拥塞窗口的值增大到16时就不再按指数形式增长了,而变成了的线性增长。
- 乘法减小。拥塞窗口在线性增长的过程中,在增大到 24 时如果发生了网络拥塞,此时慢启动的阈值将变为当前拥塞窗口的一半,也就是 12 ,并且拥塞窗口的值被重新设置为 1 ,所以下一次拥塞窗口由指数增长变为线性增长时拥塞窗口的值应该是 12 。
主机在进行网络通信时,实际就是在不断进行指数增长、加法增大和乘法减小
需要注意的是,不是所有的主机都是同时在进行指数增长、加法增大和乘法减小的。每台主机认为拥塞窗口的大小不一定是一样的,即便是同区域的两台主机在同一时刻认为拥塞窗口的大小也不一定是完全相同的。因此在同一时刻,可能一部分主机正在进行正常通信,而另一部分主机可能已经发生网络拥塞了
四、延迟应答
如果接收数据的主机收到数据后立即进行 ACK应答 ,此时返回的窗口可能比较小。
- 假设对方接收端缓冲区剩余空间大小为 1M ,对方一次收到 500K 的数据后,如果立即进行 ACK应答 ,此时返回的窗口就是 500K 。
- 但实际接收端处理数据的速度很快, 10ms 之内就将接收缓冲区中 500K 的数据消费掉了。
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
- 如果接收端稍微等一会再进行 ACK应答 ,比如等待 20ms 再应答,那么这时返回的窗口大小就是 1M 。
需要注意的是,延迟应答的目的不是为了保证可靠性,而是留出一点时间让接收缓冲区中的数据尽可能被上层应用层消费掉,此时在进行 ACK响应 的时候报告的窗口大小就可以更大,从而增大网络吞吐量,进而提高数据的传输效率。

数量限制:每隔 N 个包就应答一次。
时间限制:超过最大延迟时间就应答一次(这个时间不会导致误超时重传)。
五、捎带应答
捎带应答其实是 TCP通信 时最常规的一种方式,就好比 主机A 给 主机B 发送了一条消息,当 主机B 收到这条消息后需要对其进行 ACK应答 ,但如果 主机B 此时正好也要给 主机A 发生消息,此时这个 ACK 就可以搭顺风车,而不用单独发送一个 ACK应答 ,此时 主机B 发送的这个报文既发送了数据,又完成了对收到数据的响应,这就叫做捎带应答

捎带应答最直观的角度实际也是发送数据的效率,此时双方通信时就可以不用再发送单纯的确认报文了
此外,由于捎带应答的报文携带了有效数据,因此对方收到该报文后会对其进行响应,当收到这个响应报文时不仅能够确保发送的数据被对方可靠的收到了,同时也能确保捎带的 ACK应答 也被对方可靠的收到了
六、面向字节流
当创建一个 TCP 的 socket 时,同时在内核中会创建一个发送缓冲区和一个接收缓冲区
调用 write 函数就可以将数据写入发送缓冲区中,此时 write函数 就可以进行返回了,接下来发送缓冲区当中的数据就是由 TCP 自行进行发送的
如果发送的字节数太长, TCP 会将其拆分成多个数据包发出。如果发送的字节数太短, TCP 可能会先将其留在发送缓冲区当中,等到合适的时机再进行发送
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,可以通过调用 read函数 来读取接收缓冲区当中的数据
而调用 read函数 读取接收缓冲区中的数据时,也可以按任意字节数进行读取
由于缓冲区的存在, TCP 程序的读和写不需要一一匹配,例如:
写 100 个字节数据时,可以调用一次 write 写 100字节 ,也可以调用 100 次 write ,每次写一个字节
读 100 个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次 read 100 个字节,也可以一次 read 一个字节,重复 100次
实际对于 TCP 来说,它并不关心发送缓冲区当中的是什么数据,在 TCP 看来这些只是一个个的字节数据,它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流
七、粘包问题
先来解答一下什么是粘包吧!
首先要明确,粘包问题中的“包”,是指的应用层的数据包,在 TCP 的协议头中,没有如同 UDP 一样的“报文长度”这样的字段,站在传输层的角度, TCP 是一个一个报文过来的,按照序号排好序放在缓冲区中,但站在应用层的角度,看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包
如何解决?
要解决粘包问题,本质就是要明确报文和报文之间的边界。
- 对于定长的包,保证每次都按固定大小读取即可
- 对于变长的包,可以在报头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。比如HTTP报头当中就包含 Content-Length 属性,表示正文的长度
- 对于变长的包,还可以在包和包之间使用明确的分隔符。因为应用层协议是程序员自己来定的,只要保证分隔符不和正文冲突即可
而对于UDP而言,就显然不会有粘包的问题:
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,同时,UDP是一个一个把数据交付给应用层的,有很明确的数据边界
- 站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现“半个”的情况
七、一些异常情况
进程终止
当客户端正常访问服务器时,如果客户端进程突然崩溃了,此时建立好的连接会怎么样?
当一个进程退出时,该进程曾经打开的文件描述符都会自动关闭,因此当客户端进程退出时,相当于自动调用了 close函数 关闭了对应的文件描述符,此时双方操作系统在底层会正常完成四次挥手,然后释放对应的连接资源。也就是说,进程终止时会释放文件描述符, TCP底层 仍然可以发送 FIN ,和进程正常退出没有区别。
机器重启
当客户端正常访问服务器时,如果将客户端主机重启,此时建立好的连接会怎么样?
当我们选择重启主机时,操作系统会先杀掉所有进程然后再进行关机重启,因此机器重启和进程终止的情况是一样的,此时双方操作系统也会正常完成四次挥手,然后释放对应的连接资源。
就像我们关闭或者重启我们的电脑的时候,通常是会先让我们决定一下是否关闭,这个时候我们可以看到电脑上面是有一些进程的
机器掉电/网线断开
当客户端正常访问服务器时,如果将客户端突然掉线了,此时建立好的连接会怎么样?
当客户端掉线后,服务器端在短时间内无法知道客户端掉线了,因此在服务器端会维持与客户端建立的连接,但这个连接也不会一直维持,因为TCP是有保活策略的。
- 服务器会定期客户端客户端的存在状况,检查对方是否在线,如果连续多次都没有收到 ACK应答 ,此时服务器就会关闭这条连接
- 此外,客户端也可能会定期向服务器“报平安”,如果服务器长时间没有收到客户端的消息,此时服务器也会将对应的连接关闭
其中服务器定期询问客户端的存在状态的做法,叫做基于保活定时器的一种心跳机制,是由TCP实现的。此外,应用层的某些协议,也有一些类似的检测机制,例如基于长连接的HTTP,也会定期检测对方的存在状态
八、总结
总的来说, TCP 还是利用了很多机制来实现可靠性和提高性能的
可靠性:
- 检验和。
- 序列号。
- 确认应答。
- 超时重传。
- 连接管理。
- 流量控制。
- 拥塞控制。
提高性能:
- 滑动窗口。
- 快速重传。
- 延迟应答。
- 捎带应答。
四个定时器:
- 重传定时器:为了控制丢失的报文段或丢弃的报文段,也就是对报文段确认的等待时间
- 坚持定时器:专门为对方零窗口通知而设立的,也就是向对方发送窗口探测的时间间隔
- 保活定时器:为了检查空闲连接的存在状态,也就是向对方发送探查报文的时间间隔
- TIME_WAIT定时器:双方在四次挥手后,主动断开连接的一方需要等待的时长
TCP 的各种机制实际都没有谈及数据真正的发送,这些都叫做传输数据的策略。 TCP协议 是在网络数据传输当中做决策的,它提供的是理论支持,比如 TCP要求 当发出的报文在一段时间内收不到 ACK应答 就应该进行超时重传,而数据真正的发送实际是由底层的 IP和MAC帧 完成的
TCP 做决策和 IP + MAC 做执行,我们将它们统称为通信细节,它们最终的目的就是为了将数据传输到对端主机。而传输数据的目的是什么则是由应用层决定的。因此应用层决定的是通信的意义,而传输层及其往下的各层决定的是通信的方式
总结
差不多就结束了,最后可能还会有个源码的讲解环节,但是这个我感觉真的很底层了,涉及了C语言的多态,你肯定很好奇吧!!!
更多推荐



所有评论(0)