窗口
由来
按数据包进行确认应答,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。为解决这个问题,TCP 引入了窗口这个概念。
有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值
主要目的
- 保证可靠、有序的数据传递,
- 强制发送方和接收方之间进行流量控制。
滑动窗口简介
发送端
发送端包的处理的4个部分
- 发送了并且确认的包
- 发送了但尚未确认的包
- 没有发送,但是等待发送的包
- 没有发送,并且暂时还不会发送的包
其中 2 和 3 加起来即是接收端计算出的TCP有效窗口(EffectiveWindow)大小
EffectiveWindow=AdvertisedWindow−(LastByteSent−LastByteAcked)
发送缓冲区维护的三个指针
- LastByteAcked:指针之前为发送已确认
- LastByteSent:和LastByteAcked之间为发送未确认
- LastByteWritten:和LastByteSent之间为没有发送,并且在等待发送得
LastByteSent+LastByteWritten=对端advertisedwindow
接收端
接收端缓冲区维护一组指针(序列号):
- LastByteRead:指针之后是已经收到的,但是还未被读取应用层读取的
- NextByteExpected:下一个希望收到的字节
- LastByteRcvd
如果数据是按顺序到达的,则NextByteExpected指向LastByteRcvd之后的字节,
如果数据是乱序到达的,则NextByteExpected指向数据中第一个缺口的开始
流量控制
起因:发送端和接收端都有一定大小的缓冲区,所以接收端需要一些方法来减慢发送端速度
前提:LastByteRcvd−LastByteRead≤RcvBufferSize(为了避免缓冲区溢出)
计算窗口
接收端公告窗口大小(表示缓冲区中剩余可用空间量):
AdvertisedWindow=RcvBufferSize−((NextByteExpected−1)−LastByteRead)
工作示意图:
工作机制
- 当数据到达时,只要前面所有字节也已经到达,接收方就会认可。
- LastByteRcvd向右移动(递增),这意味着通告窗口可能会缩小,不过是否收缩取决于本地应用程序进程消费数据的速度。
- 本地进程读取数据的速度和到达数据的速度一样快(导致LastByteRead以与LastByteRcvd相同的速度递增),那么通告窗口将保持打开状态(即AdvertisedWindow = RcvBufferSize)。
- 接收进程落后了(可能因为对读取的每个字节的数据执行非常昂贵的操作),那么通告窗口将随着每一个到达的分片而变小,直到最终变为 0
- 接收缓冲区被填满,通告窗口为 0 意味着发送方不能传输任何数据,即使之前发送的数据已经被成功确认。
- 不能传输任何数据意味着发送缓冲区被填满,这最终会导致 TCP 阻塞发送进程
- 接收进程再次开始读取数据,接收端 TCP 就能够打开窗口,允许发送端 TCP 传输缓冲区数据。
- 当这些数据最终被确认后,LastByteAcked增加,缓冲区空间变得空闲,发送过程被解除阻塞并允许继续发送。
知识补充
发送端如何知道通告窗口不再是 0?
TCP 总是发送一个分片响应接收到的数据,并且这个响应包含了Acknowledge和AdvertisedWindow字段的最新值
目前窗口初始化是多少?
原先最大的值为 65525(64K),不过现在内核基本都支持 scaling factor,所以这个大小也就提高到了 1G 字节。其初始值为 20 个 MSS 大小,即 29200 字节。
如何确认窗口何时打开?
当另一方通告窗口大小为 0 时,发送方坚持每隔一段时间发送 1 字节的数据。这个数据可能不会被接受,但还是会尝试,因为每个 1 字节的分片都会触发包含当前通告窗口的响应,最终该响应携带了非零值。这些 1 字节的消息被称为零窗口探测(Zero Window Probes) ,实际上每 5 到 60 秒会发送一次。
发送窗口和MSS
发送窗口决定了能发送多少字节,而MSS决定了这些字节要分为多少个包完成