6. 流量控制机制
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被放满,这个时候如果发送端继续发送,就会造成丢包的问题,继而引起丢包重传等等一系列连锁反应。而如果发送端发的太慢,却又会使得传输数据的效率较低。因此 TCP 可以根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做流量控制。
- 接收端将接收缓冲区的剩余大小放入 TCP 首部中的 " 窗口大小 " 字段,之后,通过 ACK 通知发送端。
- 窗口的字段越大,说明网络的吞吐量越高;窗口字段越小,传输数据的效率越低。
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端,此时,发送端接收到这个窗口之后,就会减慢自己的发送速度。
- 如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
我们可以将流量控制这一机制想象成生产者消费者模型,一边往缓冲区里放数据,另一边从缓冲区里取数据。放数据可以想象成往水池中注水,取数据可以想象成水池出水。如果注水的速率大于出水的速率,水池最终会变满,那么水就会溢出,对于数据来说,就会发生丢包这样的情况;如果注水的速率小于出水的速率,那么取水的时候,效率就会变慢。而我们只能通过将注水和出水的速率控制到一个相对接近的数值,才能够保持一个均衡态。
流量控制机制是如何实现的
在上图中,我们可以看到窗口的大小为 4000,那么接收缓冲区的大小就为 4000. 当1 - 1000 的数据传入缓冲区的时候,缓冲区还剩余 3000,那么在返回的 ack 中,主机B 会把缓冲区剩余容量告诉 主机A. 接着,主机A 再次传输数据的时候,就按照窗口大小为 3000 的数据来发送,以此类推…
如果缓冲区满了,即剩余容量为 0,此时,按上面的理解,主机A 应该停止给 主机B 发送数据了。但实际上 主机A 从最开始,就会定期发送一个窗口探测包,以此来看看当前的窗口大小。当接收缓冲区数据满了,窗口探测包在某个时间段就能够探测到窗口为 0,那么,主机B 就通过在返回的 ack 告诉 主机A,这样一来,窗口大小又可以更新了,所以缓冲区也会进行更新,不再是满状态了;那么发送方又可以正常发送数据了。
举个例子:小明是一个送货员,他每天早上开车负责送冰淇淋给超市。
第一天,超市老板告诉送货员,说:冰箱总共能装下 100 只冰淇淋,但现在只有 50 只,我从你的车里面取 50 只冰淇淋就可以了。
第二天,老板发现冰箱剩余 10只 冰淇淋了,就往小明的车里取 90 只冰淇淋。
第三天,老板有事,超市关门一天。
第四天,小明来到超市的时候,老板说:冰箱里已经放不下冰淇淋了,就不从小明车里拿了。
第五天,第六天,第七天…
之后的每一天,小明送货之前,都要先打个电话问问老板,冰箱需要多少冰淇淋,根据需求来送,冰箱冰淇淋较多,就送少一点;冰箱冰淇淋较少,就送多一些;但是,即使冰箱满了,小明依旧每一天需要打电话问问,这就相当于窗口探测包,探测某一时间段的情况,再决定后面怎么发送数据。
7. 延迟应答机制
首先,我们必须明确:延迟应答机制中表示的是:延迟接收方 ACK 的应答。
在前面我们提到流量控制机制的时候,窗口大小实际上就是接收缓冲区的剩余空间大小,而窗口大小又作为下一次发送方即将发送的数据量。而站在接收方的角度来看,接收方从缓冲区中取数据。那么随着时间推移,只要缓冲区中有数据,那么接收方就会不断地从缓冲区中取数据。
那么延迟应答机制,顾名思义,当发送方传输数据给接收方后,如果接收方立刻返回 ACK 应答,此时下一次发送方的窗口大小,即为当前缓冲区剩余的空间。但如果延迟一会,接收方再 返回 ACK 应答,情况就不同了,在刚刚延迟的时间中,接收方能够消耗掉一部分数据,此时的缓冲区的剩余空间就更大了。那么下一次对于发送方的窗口大小,就能够提高一些,以此来提高效率。
理解延迟应答机制
这就和之前的注水和出水的逻辑是一样的,发送方往水池中注水,接收方从缓冲区中取水。那么只要水池中有水,那么它就会源源不断地出水。例子如下:
接收方正常取水 共 1L,那么发送方第二次注水 1L
接收方延迟取水 共 4L,那么发送方第二次注水 4L
以此看来,将接收方的 ack 应答延迟一会返回给发送方,这样会带来效率的提升,当然,这个延迟时间也需要控制在较少时间内,否则就会造成数据在传输过程中出现问题。所以说,虽然这个机制能够提高效率,但也得基于 TCP 可靠性的前提下,才能这么做。
8. 捎带应答机制
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 " 一发一收 " 的。所以,捎带应答机制就实现了将两个应答合并在一起,返回给客户端。但我们必须明确:捎带应答机制本身就是一个 " 概率性的机制 ",并不会每次都能触发,而我们只需要理解其内在思想即可,下图阐明了这个 " 捎带 " 逻辑。
同样地,在我们之前说到四次挥手,使得客户端与服务器断开连接的时候,四次挥手就有可能会变成三次挥手。当然,我也说明过,这是一个概率触发事件,因为这和 ACK 应答,和你正在使用应用程序响应的时间都有关系。
9. TCP 的粘包问题
首先我们需要明确,粘包问题中的 " 包 " ,是指的应用层的数据包。在 TCP 协议中,没有如同 UDP 一样的 " 报文长度 " 这样的字段,但 TCP 有一个序号这样的字段。
站在传输层的角度,TCP 是一个一个报文传输过来的。按照序号排好序放在接收缓冲区中。站在应用层的角度,它看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据,就不知道某个数据包的具体的开始位置和结束位置了。那么,这些应用层的数据包就会粘在一起,所以我们形象地把这个问题叫做粘包。而 TCP 是面向字节流的,所以说,这并不是 TCP 独有的缺点,而是面向字节流所带来的缺点。
(1) 如何解决粘包问题
设计一个应用层协议来明确包的边界,这实际上就是一个约定,告诉发送方采取事先设计好的规则,与此同时,也告诉接收方按照此规则读取数据即可。
方法一:为应用层数据设定 " 分隔符或结束符 "
方法二:为应用层数据设定 " 数据长度 "
(2) 区别于 UDP
10. TCP 的异常情况
(1) 进程终止:不管进程是通过什么方式终止的,本质上都会释放内核中的 PCB,也会释放对应的文件描述符,这样一来,就会触发四次挥手。我们必须明确:进程终止不代表连接立即就终止,因为它还未通过四次挥手这一过程。而进程终止其实就相当于调用了 socket.close( ),它只是相当于关闭了文件而已。
(2) 机器重启:机器重启就是主动关闭了进程,所以异常和进程终止的情况相同。
(3) 机器掉电 / 网线断开:这是一个突发情况,机器来不及进行任何动作反应。
我们再分为两种情况讨论机器掉电的情况:
情况一:掉电的是接收方
接收方一旦掉电,此时发送方还在发送数据,显然发送方不会再有接收方返回的 ACK,于是发送方就会超时重传,重传几次之后,就会通过复位报文段 RST 来尝试重置连接。而显然接收方已经断开连接了,那么最终,发送方也就会放弃这个连接,把连接资源回收。
情况二:掉电的是发送方
此时接收方正在尝试接收数据,显然它接收不到任何数据,那么接收方如何知道,发送方是断开连接了?还是暂时还没发呢?
此时接收方采取的策略,就是 " 心跳包 " 机制,也叫做 " 保活 " 机制。即接收方每隔一段时间,向对方发送一个 PING 包,期待对方返回一个 PONG 包。如果接收方 PING包 发过去了很长时间,发送方还没有返回 PONG,并且重试几次也解决不了问题,此时就认为发送方已经断开连接了。而 " 心跳包 " 机制,这是一个应用非常广泛的机制,不仅仅是在 TCP.
五、TCP 机制总结
- 确认应答 [ 可靠性 ]
- 超时重传 [ 可靠性 ]
- 连接管理 [ 可靠性 ]
- 滑动窗口 [ 效率 ]
- 流量控制 [ 可靠性 ]
- 拥塞控制 [ 可靠性 ]
- 延时应答 [ 效率 ]
- 捎带应答 [ 效率 ]
- 面向字节流 ( 粘包问题 ) [ 其他 ]
- 异常情况 ( 心跳机制 ) [ 其他 ]
六、一些面试题
1. 如何基于 UDP 协议实现可靠传输
题目虽然在问你 UDP,但实际上在问你 TCP,你只需要把 TCP 的思想往 UDP 上靠即可。也就是问你怎么通过自己的代码将 UDP 在应用层中附加上一些 TCP 的机制?当然,并不是让你真的将代码写出来,只需要阐明思路即可。
- 实现确认应答机制,接收方把每个数据接收到之后,都要为发送方反馈一个 ACK ( 这不是内核返回的了,而是应用程序自己定义一个 ACK 包,发送回去 )
- 实现发送序号以及确认序号,以及实现去重
- 实现超时重传
- 实现连接管理
- 要想提高效率,实现滑动窗口
- 为了限制滑动窗口,实现流量控制和拥塞控制
- 实现延时应答、捎带应答、心跳机制…
2. 什么样的场景中适合使用 TCP 和 UDP
① 如果需要可靠性,首选 TCP
② 如果传输的单个数据报较长 ( 超过64K ),使用TCP
③ 如果特别注重效率,优先考虑 UDP
UDP 的典型适用场景:机房内部的主机之间通信,具有网络环境简单、带宽充裕、数据丢包概率较低的特点。此外,机房内部主机之间的通信,往往传输数据量更大,需要更快的速度。
④ 如果需要广播,优先考虑 UDP,因为此时的场景需要将一份数据同时发给多个主机,而UDP 自身就支持广播的,但 TCP 自身不支持,就只能在应用层程序中设计实现。
3. 注意
传输层协议,并不仅仅是 TCP 和 UDP 两种,只是这两种协议十分广泛。而除了 TCP 和 UDP之外,还有很多其他的传输层协议,可以尽可能的兼顾到可靠性和效率。然而,既能够兼顾可靠性,又能够兼顾高效率,付出的代价可能就是更多的机器资源。而有些传输协议在特定的场景下,大佬可以自己来实现。