Files
netstack/tcpip/transport/tcp

TCP 协议

tcp特点

  1. tcp 是面向连接的传输协议。
  2. tcp 的连接是端到端的。
  3. tcp 提供可靠的传输。
  4. tcp 的传输以字节流的方式。
  5. tcp 提供全双工的通信。
  6. tcp 有拥塞控制。

img

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |           |U|A|P|R|S|F|                               |
| Offset| Reserved  |R|C|S|S|Y|I|            Window             |
|       |           |G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             data                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  1. 源端口和目的端口 各占 2 个字节,分别 tcp 连接的源端口和目的端口。关于端口的概念之前已经介绍过了。
  2. 序号 占 4 字节,序号范围是[02^32 - 1],共 2^32即 4294967296个序号。序号增加到 2^32-1 后,下一个序号就又回到 0。TCP 是面向字节流的,在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始序号ISN必须在连接建立时设置。首部中的序号字段值则是指的是本报文段所发送的数据的第一个字节的序号。例如一报文段的序号是 301而接待的数据共有 100 字节。这就表明:本报文段的数据的第一个字节的序号是 301最后一个字节的序号是 400。显然下一个报文段如果还有的话的数据序号应当从 401 开始,即下一个报文段的序号字段值应为 401。
  3. 确认号 占 4 字节是期望收到对方下一个报文段的第一个数据字节的序号。例如B 正确收到了 A 发送过来的一个报文段,其序号字段值是 501而数据长度是 200 字节(序号 501~700这表明 B 正确收到了 A 发送的到序号 700 为止的数据。因此B 期望收到 A 的下一个数据序号是 701于是 B 在发送给 A 的确认报文段中把确认号置为 701。注意现在确认号不是 501也不是 700而是 701。 总之:若确认号为 N则表明到序号 N-1 为止的所有数据都已正确收到。TCP 除了第一个 SYN 报文之外,所有 TCP 报文都需要携带 ACK 状态位。
  4. 数据偏移 占 4 位,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。这个字段实际上是指出 TCP 报文段的首部长度。由于首部中还有长度不确定的选项字段,因此数据偏移字段是必要的,但应注意,“数据偏移”的单位是 4 个字节,由于 4 位二进制数能表示的最大十进制数字是 15因此数据偏移的最大值是 60 字节。
  5. 保留 占 6 位,保留为今后使用,但目前应置为 0。
  6. 控制报文标志
    • 紧急URGURGent 当 URG=1 时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快发送(相当于高优先级的数据),而不要按原来的排队顺序来传送。例如,已经发送了很长的一个程序要在远地的主机上运行。但后来发现了一些问题,需要取消该程序的运行,因此用户从键盘发出中断命令。如果不使用紧急数据,那么这两个字符将存储在接收 TCP 的缓存末尾。只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用进程。这样做就浪费了很多时间。 当 URG 置为 1 时,发送应用进程就告诉发送方的 TCP 有紧急数据要传送。于是发送方 TCP 就把紧急数据插入到本报文段数据的最前面而在紧急数据后面的数据仍然是普通数据。这时要与首部中紧急指针Urgent Pointer字段配合使用。
    • 确认ACKACKnowledgment 仅当 ACK=1 时确认号字段才有效,当 ACK=0 时确认号无效。TCP 规定,在连接建立后所有的传送的报文段都必须把 ACK 置为 1。
    • 推送 PSHPuSH 当两个应用进程进行交互式的通信时有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下TCP 就可以使用推送push操作。这时发送方 TCP 把 PSH 置为 1并立即创建一个报文段发送出去。接收方 TCP 收到 PSH=1 的报文段,就尽快地交付接收应用进程。
    • 复位RSTReSeT 当 RST=1 时,表名 TCP 连接中出现了严重错误如由于主机崩溃或其他原因必须释放连接然后再重新建立传输连接。RST 置为 1 用来拒绝一个非法的报文段或拒绝打开一个连接。
    • 同步SYNSYNchronization 在连接建立时用来同步序号。当 SYN=1 而 ACK=0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使 SYN=1 和 ACK=1因此 SYN 置为 1 就表示这是一个连接请求或连接接受报文。
    • 终止FINFINis意思是“完”“终” 用来释放一个连接。当 FIN=1 时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
  7. 窗口 占 2 字节,窗口值是[02^16-1]之间的整数。窗口指的是发送本报文段的一方的接受窗口(而不是自己的发送窗口)。窗口值告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量(以字节为单位)。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。总之,窗口值作为接收方让发送方设置其发送窗口的依据,作为流量控制的依据,后面会详细介绍。 总之:窗口字段明确指出了现在允许对方发送的数据量。窗口值经常在动态变化。
  8. 检验和 占 2 字节,检验和字段检验的范围包括首部和数据这两部分。和 UDP 用户数据报一样,在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部。伪首部的格式和 UDP 用户数据报的伪首部一样。但应把伪首部第 4 个字段中的 17 改为 6TCP 的协议号是 6把第 5 字段中的 UDP 中的长度改为 TCP 长度。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。若使用 IPv6则相应的伪首部也要改变。
  9. 紧急指针 占 2 字节,紧急指针仅在 URG=1 时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据) 。因此在紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时TCP 就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为 0 时也可以发送紧急数据。
  10. 选项 选项长度可变,最长可达 40 字节。当没有使用“选项”时TCP 的首部长度是 20 字节。TCP 首部总长度由 TCP 头中的“数据偏移”字段决定,前面说了,最长偏移为 60 字节。那么“tcp 选项”的长度最大为 60-20=40 字节。

tcp选项

TCP 最初只规定了一种选项,即最大报文段长度 MSSMaximum Segment Szie。后来又增加了几个选项如窗口扩大选项、时间戳选项等下面说明常用的选项。

  1. kind=0 是选项表结束选项。

  2. kind=1 是空操作nop选项

    没有特殊含义,一般用于将 TCP 选项的总长度填充为 4 字节的整数倍,为啥需要 4 字节整数倍?因为前面讲了数据偏移字段的单位是 4 个字节。

  3. kind=2 是最大报文段长度选项 TCP 连接初始化时通信双方使用该选项来协商最大报文段长度Max Segment SizeMSS。TCP 模块通常将 MSS 设置为MTU-40字节减掉的这 40 字节包括 20 字节的 TCP 头部和 20 字节的 IP 头部)。这样携带 TCP 报文段的 IP 数据报的长度就不会超过 MTU假设 TCP 头部和 IP 头部都不包含选项字段,并且这也是一般情况),从而避免本机发生 IP 分片。对以太网而言MSS 值是 14601500-40字节。

  4. kind=3 是窗口扩大因子选项 TCP 连接初始化时,通信双方使用该选项来协商接收通告窗口的扩大因子。在 TCP 的头部中,接收通告窗口大小是用 16 位表示的,故最大为 65535 字节,但实际上 TCP 模块允许的接收通告窗口大小远不止这个数(为了提高 TCP 通信的吞吐量)。窗口扩大因子解决了这个问题。假设 TCP 头部中的接收通告窗口大小是 N窗口扩大因子移位数是 M那么 TCP 报文段的实际接收通告窗口大小是 N 乘 2M或者说 N 左移 M 位。注意M 的取值范围是 0 14。

    和 MSS 选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不执行窗口扩大操作,即同步报文段头部的接收通告窗口大小就是该 TCP 报文段的实际接收通告窗口大小。当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。关于窗口扩大因子选项的细节,可参考标准文档 RFC 1323。

  5. kind=4 是选择性确认Selective AcknowledgmentSACK选项 TCP 通信时,如果某个 TCP 报文段丢失,则 TCP 模块会重传最后被确认的 TCP 报文段后续的所有报文段,这样原先已经正确传输的 TCP 报文段也可能重复发送,从而降低了 TCP 性能。SACK 技术正是为改善这种情况而产生的,它使 TCP 模块只重新发送丢失的 TCP 报文段,不用发送所有未被确认的 TCP 报文段。选择性确认选项用在连接初始化时,表示是否支持 SACK 技术。

  6. kind=5 是 SACK 实际工作的选项 该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块从而让发送端可以据此检查并重发丢失的数据块。每个块边沿edge of block参数包含一个 4 字节的序号。其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。因为一个块信息占用 8 字节,所以 TCP 头部选项中实际上最多可以包含 4 个这样的不连续数据块(考虑选项类型和长度占用的 2 字节)。

  7. kind=8 是时间戳选项 该选项提供了较为准确的计算通信双方之间的回路时间Round Trip TimeRTT的方法从而为 TCP 流量控制提供重要信息。

tcp连接的建立

img

上面的图片显示了 tcp 的三次握手,但只是简单的降了三次报文的交互,下面讲讲详细的三次握手。先讲三次握手的正常情况,接着我们再讲异常情况。

正常情况(没有丢包)

主机 A 的 TCP 向主机 B 的 TCP 发出连接请求,发送 syn 报文段在发送 syn 之前,设置握手状态为 SynSent还需要做一些准备工作包括随机生成 ISN1、计算 MSS、计算接收窗口扩展因子、是否开启 sack。 根据这些参数生成 syn 报文的选项参数,附在 tcp 选项中,然后发送带着这些选项的 syn 报文。

主机 B 的 TCP 收到连接请求 syn 报文段后,需要回复 syn+ack 报文因为 tcp 的控制报文需要消耗一个字节的序列号,所以回复的 ack 序列号为 ISN1+1设置接收窗口设置握手状态为 SynRcvd并随机生成 ISN2、计算 MSS、计算接收窗口扩展因子、是否开启 sack。根据这些参数生成 syn+ack 报文的选项参数,附在 tcp 选项中,回复给主机 A。

主机 A 的 TCP 收到 syn+ack 报文段后,还要向 B 回复确认和上面一样tcp 的控制报文需要消耗一个字节的序列号,所以回复的 ack 序列号为 ISN2+1发送 ack 报文给主机 B。

主机 A 的 TCP 通知上层应用进程,连接已经建立,可以发送数据了,当主机 B 的 TCP 收到主机 A 的确认后,也通知上层应用进程,连接建立。

异常情况(有丢包)

主机 A 发给主机 B 的 SYN 中途丢失,没有到达主机 B 因为在发送 syn 之前,就设置了超时定时器,如果在一定的时间内没收到回复,就会触发重传,所以主机 A 会周期性超时重传,直到收到主机 B 的确认。重传的周期,一开始默认 1s每重传一次变为原来的 2 倍,如果重传周期超过 1 分钟,返回错误,不再尝试重连。

主机 B 发给主机 A 的 SYN +ACK 中途丢失,没有到达主机 A 主机 B 会周期性超时重传,直到收到主机 A 的确认,重传的策略和 syn 报文一样,每重传一次,周期变为原来的 2 倍。

主机 A 发给主机 B 的 ACK 中途被丢,没有到达主机 B 主机 A 发完 ACK单方面认为 TCP 为 Established 状态,而 B 显然认为 TCP 为 Active 状态:

a. 如果此时双方都没有数据发送,主机 B 会周期性超时重传,直到收到 A 的确认,收到之后主机 B 的 TCP 连接也为 Established 状态,双向可以发包。 b. 如果此时 A 有数据发送,主机 B 收到主机 A 的 Data + ACK自然会切换为 established 状态,并接受主机 A 的 Data。

TCP连接的释放

img

1数据传输结束后主机 A 的应用进程调用 Close 函数,先向其 TCP 发出释放连接请求不再发送数据。TCP 通知对方要释放从主机 A 到主机 B 的连接,将发往主机 B 的 TCP 报文段首部的终止比特 FIN 置为 1序号 seq1 等于已传送数据的最后一个字节的序号加 1。

2主机 B 的 TCP 收到释放连接通知后发出确认,其序号为 seq1+1同时通知应用进程这样主机 A 到主机 B 的连接就释放了,连接处于半关闭状态。主机 B 不在接受主机 A 发来的数据;但主机 B 还向 A 发送数据,主机 A 若正确接收数据仍需要发送确认。

3在主机 B 向主机 A 的数据发送结束后,其应用进程应该主动调用 Close 函数,释放 TCP 连接。主机 B 发出的连接释放报文段必须将终止比特置为 1并使其序号 seq2 等于前面已经传送过的数据的最后一个字节的序号加 1还必须回复 ACK=seq1+1。

4主机 A 对主机 B 的连接释放报文段发出确认,将 ACK 置为 1ACK=seq2+1, seq=seq1+1。这样才把从 B 到 A 的反方向连接释放掉,主机 A 的 TCP 再向其应用进程报告,整个连接已经全部释放。

还有一个要注意的是fin 包和数据包一样,如果丢失了,会进行重传,实际上可能是是 fin 丢失或 ack 丢失。重传的周期由 rto 决定。

tcp的可靠性机制

本小节讨论 tcp 可靠性的实现,首先得知道可靠性指的是什么。可靠性指的是网络层能通信的前提下,保证数据包正确且按序到达对端。

比如发送端发送了“12345678”那么接收端一定能收到“12345678”不会乱序“12456783”也不会少或多数据。

实现 TCP 的可靠传输有以下机制:

  1. 校验和机制(检测和重传受到损伤的报文段)
  2. 确认应答机制(保存失序到达的报文段直至缺失的报文到期,以及检测和丢弃重复的报文段)
  3. 超时重传机制(重传丢失的报文段)

正因为 tcp 实现了可靠性,那么基于 tcp 的应用就可以不用担心发送的数据包丢失、乱序、不正确等,减轻了上层开发的负担。

检验和

每个 tcp 段都包含了一个检验和字段,用来检查报文段是否收到损伤。如果某个报文段因检验和无效而被检查出受到损伤,就由终点 TCP 将其丢弃并被认为是丢失了。TCP 规定每个报文段都必须使用 16 位的检验和。

确认机制

控制报文段不携带数据,但需要消耗一个序号,它也需要被确认,而 ACK 报文段永远不需要确认ACK 报文段不消耗序号也不需要被确认。在以前TCP 只使用一种类型的确认,叫积累确认,目前 TCP 实现还实现了选择确认。

累积确认 ACK

接收方通告它期望接收的下一个字节的序号,并忽略所有失序到达并被保存的报文段。有时这被称为肯定累积确认。在 TCP 首部的 32 位 ACK 字段用于积累确认,而它的值仅在 ACK 标志为 1 时才有效。举个例子来说,这里先不考虑 tcp 的序列号,如果发送方发了数据包 p1p2p3p4接受方成功收到 p1p2p4。那么接收方需要发回一个确认包序号为 3(3 表示期望下一个收到的包的序号),那么发送方就知道 p1 到 p2 都发送接收成功,必要时重发 p3。一个确认包确认了累积到某一序号的所有包而不是对每个序号都发确认包。实际的 tcp 确认的都是序列号,而不是包的序号,但原理是一样的。

累积确认是快速重传的基础,这个后面讲拥塞控制的时候会详细说明。

选择确认 SACK

选择确认 SACK 要报告失序的数据块以及重复的报文段块是为了更准确的告诉发送方需要重传哪些数据块。SACK 并没有取代 ACK而是向发送方报告了更多的信息。SACK 是作为 TCP 首部末尾的选项来实现的。 首先是否要启动 sack应该在握手的时候告诉对方自己是否开启了 sack这个是通过 kind=4 是选择性确认Selective AcknowledgmentSACK选项来实现的。 实际传送 sack 信息的是 kind=5 的选项,其格式如下:

         +--------+--------+
         | Kind=5 | Length |
+--------+--------+--------+---------+
|          Start of 1st Block        |
+--------+--------+--------+---------+
|           End of 1st Block         |
+--------+--------+--------+---------+
|                                    |
/            . . . . . .             /
|                                    |
+--------+--------+--------+---------+
|          Start of nth Block        |
+--------+--------+--------+---------+
|           End of nth Block         |
+--------+--------+--------+---------+

sack 的每个块是由两个参数构成的 { Start, End } Start 不连续块的第一个数据的序列号。End 不连续块的最后一个数据的序列号之后的序列号。

该选项参数告诉对方已经接收到并缓存的不连续的数据块,注意都是已经接收的,发送方可根据此信息检查究竟是哪个块丢失,从而发送相应的数据块。

inmg

如图所示tcp 接收方在接收到不连续的 tcp 段,可以看出,序号 1 10001501 30003501 4500 接收到了,但却少了序号 1001 15003001 3500 。

前面说了sack 报告的是已接收的不连续的块在这个例子中sack 块的内容为 {Start:1501, End:3001},{Start:3501, End:4501}。

注意:这里的 End 不是接收到数据段最后的序列号,而是最后的序列号加 1。

产生确认的情况

  1. 当接收方收到了按序到达(序号是所期望的)的报文段,那么接收方就累积发送确认报文段。
  2. 当具有所期望的序号的报文段到达,而前一个按序到达的报文段还没有被确认,那么接收方就要立即发送 ACK 报文段。
  3. 当序号比期望的序号还大的失序报文段到达时,接收方立即发送 ACK 报文段,并宣布下一个期望的报文段序号。这将导致对丢失报文段的快重传。
  4. 当一个丢失的报文段到达时,接收方要发送 ACK 报文段,并宣布下一个所期望的序号。
  5. 如果到达一个重复的报文段,接收方丢弃该报文段,但是应当立即发送确认,指出下一个期望的报文段。
  6. 收到 fin 报文的时候,立即回复确认。

重传机制

  • RTO 即超时重传时间 30s后重发一次
  • RTT 数据包往返时间 从发送到收到确认的时间
  • 平均偏差 |∑Pi-P均值|/N N为测定的次数

img

可靠性的核心就是报文段的重传。在一个报文段发送时,它会被保存到一个队列中,直至被确认为止。当重传计时器超时,或者发送方收到该队列中第一个报文段的三个重复的 ACK 时,该报文段被重传。

超时重传的概念很简单,就是一定时间内未收到确认,进行再次发送,但是如何计算重传的时间确实 tcp 最复杂的问题之一毕竟要适应各种网络情况。TCP 一个连接期间只有一个 RTO 计时器目前大部分实现都是采用Jacobaon/Karels 算法,详细可以看 RFC6298其计算公式如下

第一次rtt计算
SRTT = R
RTTVAR = R/2
RTO = SRTT + max (G, K*RTTVAR) = R + max(G, 2 * R)
K = 4

之后:
RTTVAR = (1 - beta) * RTTVAR + beta * |SRTT - R'| = 0.75 * RTTVAR + 0.25 * |SRTT - R'|
SRTT = (1 - alpha) * SRTT + alpha * R' = 0.875 * SRTT + 0.125 * R'
RTO = SRTT + max (G, K*RTTVAR) = SRTT + max(G, 4 * RTTVAR)
K = 4

  • SRTT(smoothed round-trip time)平滑 RTT 时间
  • RTTVAR(round-trip time variation)RTT 变量,其实就是 rtt 平均偏差
  • G 表示系统时钟的粒度一般很小us 级别。beta = 1/4, alpha = 1/8

发送方 TCP 的计时器时间到TCP 发送队列中最前面的报文段(即序列号最小的报文段),并重启计时器。

tcp的流量控制

本节介绍 tcp 的流量控制和源码分析。

为何需要流量控制

任何生产消费者模型都会有速率不对等的问题,对于 tcp 来说发送方是生产者它通过网络通道发送数据给接收端接收方是消费者如果消费者消费的速率不够快那么就会导致数据在接收方累积或者丢弃接收端一但丢弃数据tcp 发送端就会认为丢包,导致进入拥塞避免阶段。

那如何避免这种速率不对等呢?一般有两种方案

  • 增大接收方的 buffer地铁进站排队通道来回迂折
  • 通过反馈机制,告诉发送方我们能接收多少

增大接收方的 buffer是很常用的一种手段它能够一定程度缓解速率不对等问题但是它不能从根本上解决问题因为计算机的资源是有限的增大 buffer 意味要增大内存使用量,如果计算机的内存是无限的,那么确实可以解决这个问题。

通过反馈机制接收方告诉发送方发送的传输速率不能大于应用的数据处理速率tcp 中就是这么实现的,协议中有 window 字段,表示接收端窗口的大小,单位为 byte。

接收端在给发送端回 ACK 中会汇报自己的 AdvertisedWindow而发送方会根据这个窗口来控制发送数据的大小以保证接收方可以处理。

要明确的是滑动窗口分为两个窗口,接收窗口和发送窗口。

接收窗口

img

接收的窗口可以分为四段:

  • 数据已经被 tcp 确认,但用户程序还未读取数据内容
  • 中间还有些数据没有到达
  • 数据已经接收到,但 tcp 未确认
  • 通告窗口,也就是接收端在给发送端回 ACK 中会汇报自己的窗口大小

当接收端接收到数据包时,会判断该数据包的序列号是不是在接收窗口內,如果不在窗口內会立即回一个 ack 给发送端,且丢弃该报文。

滑动: 当用户程序读取接收窗口的内容后,窗口向右滑行。

发送窗口

发送窗口的值是由接收窗口和拥塞窗口一起决定的,发送窗口的大小也决定了发送的速率。

img

发送窗口的上限值 = Min [rwnd, cwnd]cwnd 拥塞窗口

上图中分成了四个部分,分别是:(其中那个黑模型就是滑动窗口)

  1. 已收到 ack 确认的数据
  2. 已经发送,但还没收到 ack 的数据
  3. 在窗口中还没有发出的(接收方还有空间)
  4. 窗口以外的数据(接收方没空间)

滑动: 当发送端收到数据 ack 确认时,窗口向右滑

Zero Window

如果一个处理缓慢的 Server接收端是怎么把 Client发送端的 TCP Sliding Window 给降成 0 的。此时,你一定会问,如果 Window 变成 0 了TCP 会怎么样是不是发送端就不发数据了是的发送端就不发数据了你可以想像成“Window Closed”那你一定还会问如果发送端不发数据了接收方一会儿 Window size 可用了,怎么通知发送端呢?

  1. 当接收方的应用程序读取了接收缓冲区中的数据以后,接收方会发送一个 ACK通过通告窗口字段告诉发送方自己又可以接收数据了发送方收到这个 ACK 之后,就知道自己可以继续发送数据了。

  2. 同时发送端使用了Zero Window Probe技术缩写为 ZWP当接收方的接收窗口为 0 时,每隔一段时间,发送方会主动发送探测包,迫使对端响应来得知其接收窗口有无打开。

Silly Window Syndrome

Silly Window Syndrome 翻译成中文就是“糊涂窗口综合症”。正如你上面看到的一样,如果我们的接收方太忙了,来不及取走 Receive Windows 里的数据,那么,就会导致发送方越来越小。到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的 window而我们的发送方会义无反顾地发送这几个字节。

要知道,我们的 TCP+IP 头有 40 个字节,为了几个字节,要达上这么大的开销,这太不经济了。

所以Silly Windows Syndrome 这个现像就像是你本来可以坐 200 人的飞机里只做了一两个人。要解决这个问题也不难,就是避免对小的 window size 做出响应,直到有足够大的 window size 再响应,这个思路可以同时实现在 sender 和 receiver 两端。

如果这个问题是由 Receiver 端引起的,那么就会使用 David D Clark 方案。在 receiver 端如果收到的数据导致window size小于某个值可以直接 ack(0)回 sender这样就把 window 给关闭了,也阻止了 sender 再发数据过来,等到 receiver 端处理了一些数据后windows size大于等于了 MSS或者receiver buffer有一半为空就可以把 window 打开让 sender 发送数据过来。

如果这个问题是由 Sender 端引起的,那么就会使用著名的 Nagle algorithm。这个算法的思路也是延时处理他有两个主要的条件

  1. 要等到 Window Size >= MSS 或是 Data Size >= MSS
  2. 收到之前发送数据的 ack 回包,他才会发数据,否则就是在攒数据

上面是通过 keepalive 实现的

tcp的拥塞控制

节介绍 tcp 的拥塞控制,拥塞控制控制是 tcp 协议中最复杂问题之一,主要是如何探测链路已经拥塞?探测到拥塞后如何处理?

事实上,早期 TCP 实现是没有拥塞控制的,拥塞控制是网络出现问题后才提出的,在 1986 年互联网首次出现了一系列“拥堵事故”从伯克利实验室到加州大学伯克利分校的数据吞吐量从32 Kbps降至40 bps。然后在伯克利实验室的工作人员 Van_Jacobson 很好奇为何网络会拥堵并开始调查网络变得如此糟糕2 年后,他提出了拥塞算法。

我们知道 TCP 通过采样了 RTT 并计算 RTO但是如果网络上的延时突然增加超过了 RTO那么 TCP 对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这样就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的 TCP 连接都这么行事那么马上就会形成“网络风暴”TCP 这个协议就会拖垮整个网络。这是一个灾难。

所以TCP 不能忽略网络上发生的事情,而一个劲的重发数据,对网络造成更大的伤害。于是就提出了拥塞控制,当拥塞发生的时候,要做自我牺牲,降低发送速率。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。

要注意流量控制和拥塞控制的区别,流量控制只控制两个端的速度,它抑制发送端的速度,以便接收端能接收,但它并不关心中间链路的网络情况。而拥塞控制是关心中间链路的网络情况,防止过多的数据注入到网络中,以便防止中间的链路或路由不过载。

拥塞控制的算法

TCP 通过维护一个拥塞窗口(cwnd 全称 Congestion Window)来进行拥塞控制,拥塞控制的原则是,只要网络中没有出现拥塞,拥塞窗口的值就可以再增大一些,以便把更多的数据包发送出去,但只要网络出现拥塞,拥塞窗口的值就应该减小一些,以减少注入到网络中的数据包数。

拥塞控制主要是四个算法1慢启动2拥塞避免3快速重传4快速恢复。

这四个算法不是一天都搞出来的,这个四算法的发展经历了很时间,至今都还在优化中,仅实现这个四个算法的拥塞算法叫 Reno 算法。

慢启动slow start

慢启动的意思是,加入网络的连接,一点一点地提速,不要一上来就突发流量挤占通道。 慢启动的算法如下:

  1. 连接建好的开始先初始化cwnd = 1表明可以传一个 MSS 大小的数据。
  2. 每当收到一个 ACKcwnd++; 呈线性上升
  3. 每当过了一个 RTTcwnd = cwnd*2; 呈指数上升
  4. 设置一个慢启动阀值 ssthreshslow start threshold是慢启动和拥塞避免的一个临界值当cwnd >= ssthresh时就会进入拥塞避免阶段

拥塞避免congestion avoidance

当 cwnd >= ssthresh 时,就会进入“拥塞避免算法”。一般来说初始 ssthresh 的值是很大的,当 cwnd 达到这个值时后,算法如下:

  • 每当收到一个 ACK 时cwnd = cwnd + 1/cwnd
  • 每当过一个 RTT 时cwnd = cwnd + 1

快速重传Fast Retransmit

快速恢复Fast Recovery

  • 1988 年TCP Tahoe 提出了 1慢启动2拥塞避免3快速重传。
  • 1990 年TCP Reno 在 Tahoe 的基础上增加了 4快速恢复是现有的众多拥塞控制算法的基础被认为标准 tcp 拥塞行为。
  • 2004 年TCP BICBinary Increase Congestion control在Linux 2.6.8中是默认拥塞控制算法,用的是 Binary Search——二分查找来找拥塞窗口。
  • 2008 年TCP CUBIC 是比 BIC 更温和和系统化的分支版本,其使用三次函数代替二分算法作为其拥塞窗口算法,并且使用函数拐点作为拥塞窗口的设置值。 Linux 2.6.19后使用该算法作为默认 TCP 拥塞算法,现在也是。
  • 2016 年TCP BBR 是由 Google 设计,于 2016 年发布的拥塞算法,交替测量一段时间内的带宽极大值和时延极小值,将其乘积作为作为拥塞窗口大小,认为当网络上的数据包总量大于瓶颈链路带宽和时延的乘积时才出现了拥塞。