一般来说,对于接收方数据抵达时接收方可能忙于其他事情并不能立即处理数据,所以会设置一个接受缓存,并使得收到的数据正确有序。由于缓存有一定大小,这时发送方发送太快就会使得很多数据因缓存满了而丢弃,导致不必要的重传,这时就需要称为 滑动窗口 的流量控制措施。
TCP 协议是一个全双工协议,所以双方都既是发送方又是接收方,下面两个窗口虽然是针对特定一方,但实则双方都会维护两个窗口。
发送窗口
与序列号之类的一样,这里依旧是以 字节为单位 维护窗口结构。
在 TCP 头部中有个窗口大小的字段,对于发送方来说,字段值为其可以立即发送的数据量,就是上图中即将发送一段的字节数。
随着数据的传输,窗口会发生移动,有三种运动方式:
- 关闭(close):窗口左边界右移。发送数据得到 ACK 时窗口减小。
- 打开(open):窗口右边界右移。当接收方可用缓存增大时,窗口增大。
- 收缩(shrink):窗口右边界左移。一般来说不应发生,但必须能够处理此问题。
不存在左边界左移,因为窗口左边是已确认的数据,不可能会再次发送。当 ACK 号不断增大,窗口大小保持不变时,则称窗口向前滑动。
接收窗口
相比发送窗口结构简单,这个结构记录了已确认的数据和能接受的最大序列号,这样设置保证了数据接收的正确有序,也不会重复存储。
其中接收窗口的大小代表着缓存中仍能被保存的数据量大小,也是 TCP 头部中窗口大小字段的值。接收到左边界左侧的数据会认为是重复数据而丢弃,接收到右边界右侧数据则会导致潜在的缓存溢出的可能所以也被丢弃。只有在左边界上的才可被正常处理,窗口也才会向前移动。启用 SACK 时,接收窗口内的都能被保存,但是窗口移动仍只发生在左边界数据抵达时。
零窗口的处理
当左右边界相同时称为零窗口,一般由于接收方无法及时处理数据,为了阻止发送方继续发送数据而产生这一情况。当接收方可以接收数据时,会给发送端一个纯 ACK,称为 窗口更新。然而这一数据报并没有确认机制,所以要确保能处理这一丢包。
如果发生这一丢包,双方都会继续等待,产生死锁。发送方为了解决这一情况,有一个持续计时器来触发 窗口探测 机制。接收方收到后必须返回一个带有窗口大小的 ACK。RFC 中推荐计时器时间为一个 RTO,之后以指数级增长。