什么是窗口
窗口是包含在 TCP 头里的一个16位的字段
窗口
是一个已被发送方注入但还没有完成确认 (比如, 发送方已经发送, 却还没有收到 ACK) 的分组的集合, 窗口大小
就是这个分组的数量.
滑动窗口在发送方和接收方又分为接收窗口和发送窗口
发送窗口
记录了哪些分组可以被释放, 哪些分组正在等待 ACK. 和哪些分组还不能在排队等待发送
接收窗口
记录着哪些分组已经被接收和确认, 哪些分组是是接下来期望收到的 (和分配多少内存来保存他们), 以及哪些分组即使被接受也会因为内存的限制而被抛弃
窗口的目的
我们知道数据的传输建立在网络层之上, 但是网络层是不可靠的, 他不关心数据是否已经正确到达对端, 上一篇说过 TCP 是建立在不可靠传输上的可靠协议
所以 TCP 要解决 :
- 数据包的丢失
- 数据包没有按顺序到达对端 (接收方收到的是乱序的)
- 网络实际的传输带宽和两端处理数据速度的不理想而导致可能出现的网络拥堵
为了解决上述问题, TCP 引入了滑动窗口.
滑动窗口可以很好地解决可靠传输和包乱序的问题
滑动窗口
TCP 是一个全双工的协议, 所以连接的两端都能收发数据, 而数据的收发就是通过滑动窗口来控制的,
发送窗口示意图
这是一个发送窗口
的示意图
TCP 是一个具有流式特性的协议 (收发数据都是字节流的概念, 而非包), 它以字节 (而非包) 为单位来维护其窗口
ACK 里的重要信息
我们也说过 TCP 是靠 ACK 来做到可靠传输的, 没有收到 ACK 就重发, 那么这个 ACK 里包含了两个重要的信息 :
- 期望收到的下一字节的序号, 假如 A 收到了对方发来的 1~100 字节的数据, 那么他回复的 ACK 里包含的序列号就是 101, 而这个时候如果 A 收到了不是第 101 字节数据的时候, 他是不会回复对应的 ACK 的, 假如收到了 102~202 字节的数据, 是不会回复序列号为 203 的 ACK 的, 而是继续发送序列号为 101 的ACK
- 当前窗口大小, 如此发送方在接收到ACK包含的这两个数据后就可以计算出还可以发送多少字节的数据给对方,假定当前发送方已发送到第x字节,则可以发送的字节数就是y=m-(x-n).
这段得自知乎
如上图是一个从 3~7 字节的滑动窗口, 窗口大小为 3 字节
第 3 字节已经被确认 (收到了 ACK), 可以被释放掉了, 而第 7 字节已经准备好了但还不能发送, 因为还没有进入"窗口"
滑动窗口是动态的, 在下一刻我们可能收到了第 4 字节的 ACK, 于是窗口右移一个分组, 这时第 7 字节就能发送了
如何滑动
可以想象一下, 滑动窗口就像是在月台排队买票:
只有在他付钱(发送数据)后拿到他要的票(收到 ACK 确认) 才会走, 队伍也得以向前移动
即, 滑动窗口是 “一格一格” 地往前挪动的, 这个一格就是你收到的 ACK 的数据, 只有在位于左边界的数据得到确认后, 窗口才能向前滑动
而假如像上图中, 第 4 字节未收到 ACK, 但是 5, 6 字节都已收到 ACK 了, 这时可以使用 SACK(带选择确认的重传)
选项来避免重复发送,
ACK 号与接收端缓存中其他数据之间的间隔称为
空缺
, 序列号高于空缺
的数据称为失序数据
, 因为这些数据和之前接收的序列号不连续, 这里不详细介绍 SACK 了
随着时间的推移, 接收到 ACK, 滑动窗口也在不停右移, 窗口两端的相对运动使得窗口增大或减小(窗口大小不是固定不变的)
- 当已经发送的数据得到 ACK 确认时, 窗口左边界右移, 窗口变小
- 窗口右边界右移, 使得可发送的数据量增大. 当已经确认的数据得到处理, 接收端缓存变大, 窗口也随之变大
- 每个窗口的左边界都不能左移, 因为他控制的是已经确认的 ACK, 具有累积性, 不能返回
当得到的 ACK 号增大而窗口保持不变时 (通常如此), 即窗口向前滑动, 若随着 ACK 号增大窗口大小却在减小, 则左右边界距离减小, 当左右边界相等时, 称之为零窗口
, 此时发送端不能发送新数据, 这种情况下, TCP 发送端开始探测
对方窗口, 伺机增大提供窗口
还有一个大神制作的视频, 建议 0.5 倍速看一下他是如何"滑动"的, 十分清晰明了
前面提到的接收窗口
相对发送窗口
来说更加简单, 左边界以左是已接受并确认的数据, 窗口中是接收后并要保存的数据, 右边界以右是不能接收的数据
接收窗口
保证了接收数据的正确性, 特别是, 接收端希望避免存储重复的已接受和确认的数据, 以及避免存储不应接收的数据(位于右边界以右的数据)
窗口大小和缓冲区的关系
我们知道, 在 send 和 recv 端都有发送缓冲区和接收缓冲区, 而你的进程读写数据都不是发送给对方, 或者从对端拿到的, 都是拷贝入发送缓冲区里, 或者从接收缓冲区中拷贝出来的,
以发送缓冲区为例, TCP 协议栈里有一个 buffer 来作为每个 TCP 套接字都拥有的一个发送缓冲区, 你 send 的数据是先拷贝进入这个缓冲区, 并不是直接就发送到对端了
在判断 send 是否成功发送的时候只能判断的是 send 成功返回(在成功拷贝数据进入缓冲区后返回), 我们只能将数据成功全部拷贝进入发送缓冲区当做成功发送的标志, 其实还没有
而 TCP 滑动窗口的大小实际上就是接收缓冲区大小的字节数
可以通过
setsockopt
函数更改
而且该函数必须位于 listen / connect 函数之前, 因为,accept 时新产生的 socket 会继承监听 socket 的缓冲区大 小。而 connet 时需要进行三次握手过程,会通知对方自己的窗口大小。在 connet 之后再设置缓冲区,已经没有什么意义。