一、概述
TCP 通过三次握手建立两端之间的连接,一旦完成所有设置(如 TCBs、序列号确认等),两端就可以互相传输数据。
通常,我们总是希望数据传输得更快、更多。但是是复杂的网络背景下,需要在网络条件允许的情况下尽可能多的占满网络带宽。TCP 对端到端数据发送使用 流量控制(flow control)维持收发双方数据传输稳定和高效。所谓的 流量控制 就是让发送方的发送速率不要太快,要让接收方来得及接收。
TCP 利用 滑动窗口
实现流量控制。
- 收发速率匹配,防止接收方被数据流淹没
方法
_**rwnd,Receiver window, RWND,**_
接收方窗口大小。- 该参数用于表示 接收方现存窗口大小,发送方通过该窗口大小可以调整发送的分组数量。避免出现丢包重传。
- 单位: 字节。
在 TCP 建立连接时,两端需要经过 TCP 三次握手互相交换信息,其中包括 **_rwnd_**
接收窗口大小。
swnd
MSS
_**Maximu Segment Size, MSS**_
,最大报文段长度。表示 Segment 可容纳最大的数据量,不包含 TCP 头部。- 单位: 字节。默认 TCP MSS 大小为
536字节
。- 在 IP 层中,数据报大小也有限制,称为
Maximum Transmission Unit, MTU
,MTU 最小值为576字节
,因此得MSS=576-20-20=536字节
。
- 在 IP 层中,数据报大小也有限制,称为
属于 TCP 协议定义的一个选项。在 TCP 三次握手时,收发双方协商通信时每一个报文段所能承载的最大数据长度(如果没有确定,则使用默认值 536)。如果设备希望使用更大的 MSS 值,通过 MTU 路径发现 确定两端合适的 MSS 大小。
二、窗口说明
滑动窗口在概述上将字节划分以下类别:已发送并收到确认字节。#1 所示。
- 已发送但未收到确认字节。#2 所示。
- 未发送但准备好接收字节。#3 所示。
-
发送端窗口概况
连接中的客户端和服务端都必须跟踪它在传输的流和正在接收的流。这是通过一组特殊的指针变量完成的。使用 3 个指针变量将 TCP 字节流切分成 4 个不同部分。指针功能如下: Send Unacknowledged (SND.UNA): 已发送但尚未确认的数据的第一个字节的序列号。这标记了传输类别 #2的第一个字节; 该指针的前面的序号都表示已发送且收到确认,即类别 #1。
- Send Next (SND.NXT): 该指针指向下一个被发送的字节序号。即类型 #3。
- Send Window (SND.WND): 窗口的大小。
因此,已发送数据计算如下
SND.UNA+SND.WND-SND.NXT
接收端窗口概况
接收窗口只需要分为三部分:
- 已接收且 ACK 应答成功。即 #1。
- 未被接收,但允许发送端发送。即 #2。
- 未被接收且不允许发送端发送。即#3。
需要两个指针就可以完成划分:
- Receive Next(RCV.NXT): 期望发送端发送的下一个数据字节的序列号。
Receive Window(RCV.WIND): 通知对端本连接接收窗口的大小。通常为此连接的而分配的缓冲区的大小。
三、TCP 滑动窗口数据传输和确认机制
两端都会通过传输控制块(TCP)维护
SND
和RCV
指针信息。- 随着数据被交换,相关指定得到更新。并且交换用于 TCP 流的控制字段。其中最重要的三点是:
- Sequence Number: 标识要传输的段中第一个字节的序列号。通常在发送数据时和
SND.UNA
的值相等。 - Acknowledgment Number: 下一次传输所期望的序列号。此字段通常等于发送端的
RCV.NXT
指针。 Window: 窗口大小。对接收端来说,就是发送窗口大小。对发送端来说,就是接收窗口大小。
TCP 滑动窗口机制示意图
发送窗口大小动态可变
- 接收方能行当前可用接收缓冲区大小
- 发送方用该能行值调整发送窗口大小
- 优点
- 更加有效的传输,同时还可控制数据流量
极端情况: 接收方能行的可用缓冲区 == 0
客户端请求服务端数据。要记住,这是双向传输,因此,两端都需要维护发送指针和接收指针。
- 对客户端来说,作为请求的一方,需要维护
RCV.WND
、RCV.NXT
两个指针。 - 对服务端来说,作为发送的一方,需要维护
SND.WND
、SND.NXT
、SND.UNA
三个指针。
- 对客户端来说,作为请求的一方,需要维护
- 客户端
- 第一次发送请求,长度为 40字节,起始序列号为1,更新
SND.NXT=141
。 - 收到服务端响应 + 数据。ACK 序列号为 141,表示序列号 141 之前的数据已正常收到,更新
SND.UNA = 141
。后续,客户端只响应服务端所发送的数据(即响应 ACK)。
- 第一次发送请求,长度为 40字节,起始序列号为1,更新
服务端
- 第一次收到客户端请求,则更新
RCV.NXT=141
。 - 第一次回复客户端,数据报中包含 ACK 序列号确认信息和数据。服务端一开始发送 80字节数据,因此,更新
SND.NXT=321
,由于还没有收到 ACK 确认,所以SND.UNA
保持不变。此时,还可以继续发送窗口内的数据,即长度为 120字节,当前数据报起始序列号为 321。 - 收到客户端的 ACK 确认,因此,更新
SND.UNA=321
,由于SND.WND
窗口大小不变,但是SND.UNA
增大,因此,以前不可发送的数据现在可以发送了。相当于窗口向右滑动了。 - 服务端继续收到客户端 ACK=441 的确认报文,更新
SND.NXT=441
,继续发送数据,此时长度为 160,序列号为 441。 - …
小结
- 第一次收到客户端请求,则更新
TCP 是全双工,只要发送数据,就必须维护
SND.NXT
、SND.WND
、SND.UNA
这三个指针,只能接收数据,就必须维护RCV.NXT
、RCV.WND
这两个指针。- TCP 首部报文中存在 窗口字段 用于传输控制。告诉发送方自己接收窗口还有多大空间,发送方根据这个参数控制发送数据量。以实现流量控制。
当然,上述介绍的是最理想的情况下,在现实世界的连接还包括以下复杂性:
- 传输重叠。实际上,客户端和服务端可能会连续快速地互相发送许多请求和响应,客户端将会用自身包含新请求的段来确认从服务端接收的段。即 数据报中包含对上一次报文的 ACK 确认。
- 累积确认。
- 用于流量控制的波动窗口的大小。如果当前接收方处理速度慢,可以调整自己的接收窗口大小,并告知发送方,以便让他减慢发送速度。这就是 TCP 实现流量控制的方式。
- 报文丢失。需要利用 TCP 重传机制 处理。
- 避免小窗口问题。我们并不总是尽可能快的发送数据,比如来一个字节就发送,这会导致 糊涂窗口综合症。现阶段,对于 TCP 而言,内部会有一个缓存,一般等到缓存满了之后再发送。当然,你也可以通过其他方式(比如设置 PSH标志)直接发送。
- 拥塞避免与拥塞管理。虽然滑动窗口机制用来避免 TCP 连接导致的网络拥塞并在它们在检测到拥塞时处理拥塞。
零窗口
窗口可以理解为缓存,TCP 接收数据并放入缓存并通过上层应用去取,但是出于某种情况,上层应用来不及消费数据。缓存容量大小有限。如果在一段时间内缓存数据都无法被消费,就会造成零窗口现象: 发送方发送窗口为0,接收方接收窗口为0。类似死锁。
- TCP 使用 ZWP(Zero Window Prob)技术解决这个问题。TCP 为每一个连接设有一个 持续计时器(persistence timer)。只要 TCP 连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计算器设置的时间到期,就发送一个零窗口 探测报文段(仅携带1字节的数据)。而对方就在确认这个探测报文段时给出了现存的窗口值。
- 一般会经过3次尝试,每次 30~60秒。如果 3 次得到的结果都为0,部分 TCP 实现会发送 RST 重置报文直接把链路切断。
- 当重新打开关闭的窗口时,还存在一个陷阱: 刚打开的窗口太小。通常,当接收窗口太小时,这会导致生成许多小的报文段,从而大大降低 TCP 的整体效率。
零窗口攻击
只要有等待的地方都可能出现DDoS攻击,Zero Window也不例外,一些攻击者会在和HTTP建好链发完GET请求后,就把Window设置为0,然后服务端就只能等待进行ZWP,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。
四、减少小报文发送提高网络效率
TCP 报文段发送策略
运输层 为 应用层 提供服务,应用进程 只需要把数据传送到 TCP 的 发送缓存 后就可以放手不管了,剩下的发送任何就由 TCP 控制。
- 可以用不同的机制来控制 TCP 报文段的发送时机。(时间+MSS大小+主动PSH)
- TCP 维持一个变量,它等于 最大报文段长度 MSS 时,就组装成一个 TCP 报文段 送往 IP 层。
- 应用进程 指明要发送的报文段大小。TCP 支持主动的推送(PSH)操作。
- 当应用进程需要立即通过互联网发送数据时,利用 TCP 推送功能就能立即将数据推送至网络。无需等待更多数据填满缓冲区才发送。
- 接收端收到数据报,发送 PSH 位置为 1,因此,也会直接跳过缓冲区上报至应用进程。
- 虽然应用程序可以选择推送操作,但是很少使用。
- 发送方的一个定时器期限到了,就把当前已有的缓存数据装入报文段(但长度不能超过 MSS) 发送出去。
- 如果发送数据量太小,会导致头重脚轻(TCP 首部最少 20 字节)。如果等待 MSS 大小才发送,有些实时交互的应用进程会明显感觉到延迟。
数据优先级传输—Urgent 功能
RFC813
- 存在原因
- 通信双方的应用进程以不同速率工作时,会出现严重的性能问题。
- 接收方: 确认报文通告小窗口。
- 发送方: 报文段携带少量数据。
- TCP 接收方的缓存已满,而交互式的应用进程一次只从接收缓存中读取 1 个字节,然后向发送方发送确认,并把窗口大小设置为 1 个字节。
- 接着,发送方又发来 1 个字节的数据,接收方发回确认,仍将窗口设置为 1 个字节。
- 如果按照这样进行,网络效率极低。
TCP 糊涂窗口综合症示意图
- 服务端非常繁忙,无法及时清理接收缓存区,客户端每次发送数据时,服务端都会减小其接收窗口。
- 客户端的发送窗口也会被动减小,直到仅能发送非常小的数据报,或变成零窗口。零窗口并非最坏情况,而是当服务端接收窗口只有接收 1 个字节数据时,发送端也最多只能发送一个字节,这样的网络效率是最低的。
- 解决思路: 避免对小的滑动窗口大小作出响应,直到有足够大的窗口时再响应。可以用在发送端和接收端。
- 接收方的 SWS 避免
- 推迟窗口通告
- 推迟零窗口之后,在接收窗口显著增加之前,推迟窗口的通告
- 达到接收缓冲区的一半
- 或达到最大报文段长度
- 推迟零窗口之后,在接收窗口显著增加之前,推迟窗口的通告
- 推迟确认
- 推迟确认的发送
- 直到窗口值增大到一定程序,或
- 有数据要发送,或
- 超时时限快到
- 推迟确认的发送
- 推迟窗口通告
- 发送方的 SWS 避免
- 延迟发送
- 收集应用程序的发送数据,聚集合理的数据量
- 延迟时间
- 长 — 反应变慢(如对话应用程序)
- 短 — 数据量少,吞吐率下降
- 延迟策略: 根据当前网络性能而定
- 自定时方式,使用确认的到达来触发报文的发送。
- 延迟发送
- 接收方的 SWS 避免
- 如果 SWS 发生在接收端,则会使用
David D Clark's
方案。 如果 SWS 发生在发送端,则会使用著名的
Nagle算法
。该算法的思路也是延时处理。David D Clark’s
原理: 在接收端,如果收到的数据导致窗口小于某个值,就可以直接发送
ACK(0)
报文,这样就变为 零窗口状态。发送端也停止发送数据。等到接收端上层处理部分数据,此时窗口大小≥MSS
或者Receiver Buffer
有一半为空,就可以打开窗口让接收数据。窗口边界移动值小于
_**Min(MSS,缓存/2)**_
时,通知窗口为 0。Nagle 算法
Nagle 有两个主要的条件
- 需要等到
窗口大小≥MSS
或是数据报大小≥ MSS。
- 收到之前发送数据的
ACK
确认报文。
- 需要等到
- 在 TCP 的实现中广泛使用 Nagle 算法。算法如下:
- 如果包长度达到 MSS,则允许发送。
- 如果该包含有
_FIN_
,则允许发送。 - 设置了
TCP_NODELAY
选项,则允许发送。 - 未设置
TCP_CORK
选项时,若所有发出去的小数据包(包长度小于 MSS)均被确认,则允许发送。 - 上述条件都未满足,但发生了超时(一般为 200ms),则立即发送。
TCP_NODELAY
用于关闭Nagle
算法。
三、如何确定缓存大小
3.1 带宽时延积
[1] TCP/IP Guide.com