TCP
TCP 头格式
你需要注意这么几点:
- TCP 的包是没有 IP 地址的,那是 IP 层上的事。但是有源端口和目标端口。
- 一个 TCP 连接需要四个元组来表示是同一个连接(src_ip, src_port, dst_ip, dst_port)准确说是五元组,还有一个是协议。但因为这里只是说 TCP 协议,所以,这里我只说四元组。
- 注意上图中的四个非常重要的东西:
- Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。
- Acknowledgement Number就是 ACK——用于确认收到,用来解决不丢包的问题。
- Window 又叫 Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的。
- TCP Flag ,也就是包的类型,主要是用于操控 TCP 的状态机的。
关于其它的东西,可以参看下面的图示
TCP 状态机
下面是:“TCP 协议的状态机”(图片来源) 和 “TCP 建链接”、“TCP 断链接”、“传数据” 的对照图,我把两个图并排放在一起,这样方便在你对照着看。另外,下面这两个图非常非常的重要,你一定要记牢。(吐个槽:看到这样复杂的状态机,就知道这个协议有多复杂,复杂的东西总是有很多坑爹的事情,所以 TCP 协议其实也挺坑爹的)
很多人会问,为什么建链接要 3 次握手,断链接需要 4 次挥手?
- 对于建链接的 3 次握手, 主要是要初始化 Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的 Sequence Number(缩写为 ISN:Inital Sequence Number)——所以叫 SYN,全称 Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP 会用这个序号来拼接数据)。
- 对于 4 次挥手, 其实你仔细看是 2 次,因为 TCP 是全双工的,所以,发送方和接收方都需要 Fin 和 Ack。只不过,有一方是被动的,所以看上去就成了所谓的 4 次挥手。如果两边同时断连接,那就会就进入到 CLOSING 状态,然后到达 TIME_WAIT 状态。下图是双方同时断连接的示意图(你同样可以对照着 TCP 状态机看):
另外,有几个事情需要注意一下:
- 关于建连接时 SYN 超时 。试想一下,如果 server 端接到了 clien 发的 SYN 后回了 SYN-ACK 后 client 掉线了,server 端没有收到 client 回来的 ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server 端如果在一定时间内没有收到的 TCP 会重发 SYN-ACK。在 Linux 下,默认重试次数为 5 次,重试的间隔时间从 1s 开始每次都翻售,5 次的重试时间间隔为 1s, 2s, 4s, 8s, 16s,总共 31s,第 5 次发出后还要等 32s 都知道第 5 次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP 才会把断开这个连接。
- 关于 SYN Flood 攻击。一些恶意的人就为此制造了 SYN Flood 攻击——给服务器发了一个 SYN 后,就下线了,于是服务器需要默认等 63s 才会断开连接,这样,攻击者就可以把服务器的 syn 连接的队列耗尽,让正常的连接请求不能处理。于是,Linux 下给了一个叫tcp_syncookies的参数来应对这个事——当 SYN 队列满了后,TCP 会通过源地址端口、目标地址端口和时间戳打造出一个特别的 Sequence Number 发回去(又叫 cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie 发回来,然后服务端可以通过 cookie 建连接(即使你不在 SYN 队列中)。请注意,请先千万别用 tcp_syncookies 来处理正常的大负载的连接的情况。因为,synccookies 是妥协版的 TCP 协议,并不严谨。对于正常的请求,你应该调整三个 TCP 参数可供你选择,第一个是:tcp_synack_retries 可以用他来减少重试次数;第二个是:tcp_max_syn_backlog,可以增大 SYN 连接数;第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。
- 关于 ISN 的初始化。ISN 是不能 hard code 的,不然会出问题的——比如:如果连接建好后始终用 1 来做 ISN,如果 client 发了 30 个 segment 过去,但是网络断了,于是 client 重连,又用了 1 做 ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client 的 Sequence Number 可能是 3,而 Server 端认为 client 端的这个号是 30 了。全乱了。RFC793中说,ISN 会和一个假的时钟绑在一起,这个时钟会在每 4 微秒对 ISN 做加一操作,直到超过 2^32,又从 0 开始。这样,一个 ISN 的周期大约是 4.55 个小时。因为,我们假设我们的 TCP Segment 在网络上的存活时间不会超过 Maximum Segment Lifetime(缩写为 MSL – Wikipedia 语条),所以,只要 MSL 的值小于 4.55 小时,那么,我们就不会重用到 ISN。
- 关于 MSL 和 TIME_WAIT。通过上面的 ISN 的描述,相信你也知道 MSL 是怎么来的了。我们注意到,在 TCP 的状态图中,从 TIME_WAIT 状态到 CLOSED 状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定义了 MSL 为 2 分钟,Linux 设置成了 30s)为什么要这有 TIME_WAIT?为什么不直接给转成 CLOSED 状态呢?主要有两个原因:1)TIME_WAIT 确保有足够的时间让对端收到了 ACK,如果被动关闭的那方没有收到 Ack,就会触发被动端重发 Fin,一来一去正好 2 个 MSL,2)有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存 IP 数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。你可以看看这篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems》
- 关于 TIME_WAIT 数量太多。从上面的描述我们可以知道,TIME_WAIT 是个很重要的状态,但是如果在大并发的短链接下,TIME_WAIT 就会太多,这也会消耗很多系统资源。只要搜一下,你就会发现,十有八九的处理方式都是教你设置两个参数,一个叫tcp_tw_reuse,另一个叫tcp_tw_recycle的参数,这两个参数默认值都是被关闭的,后者 recyle 比前者 resue 更为激进,resue 要温柔一些。另外,如果使用 tcp_tw_reuse,必需设置 tcp_timestamps=1,否则无效。这里,你一定要注意,打开这两个参数会有比较大的坑——可能会让 TCP 连接出一些诡异的问题(因为如上述一样,如果不等待超时重用连接的话,新的连接可能会建不上。正如官方文档上说的一样“It should not be changed without advice/request of technical experts”)。
- 关于 tcp_tw_reuse。官方文档上说 tcp_tw_reuse 加上 tcp_timestamps(又叫 PAWS, for Protection Against Wrapped Sequence Numbers)可以保证协议的角度上的安全,但是你需要 tcp_timestamps 在两边都被打开(你可以读一下tcp_twsk_unique的源码 )。我个人估计还是有一些场景会有问题。
- 关于 tcp_tw_recycle。如果是 tcp_tw_recycle 被打开了话,会假设对端开启了 tcp_timestamps,然后会去比较时间戳,如果时间戳变大了,就可以重用。但是,如果对端是一个 NAT 网络的话(如:一个公司只用一个 IP 出公网)或是对端的 IP 被另一台重用了,这个事就复杂了。建链接的 SYN 可能就被直接丢掉了(你可能会看到 connection time out 的错误)(如果你想观摩一下 Linux 的内核代码,请参看源码 tcp_timewait_state_process)。
- 关于 tcp_max_tw_buckets。这个是控制并发的 TIME_WAIT 的数量,默认值是 180000,如果超限,那么,系统会把多的给 destory 掉,然后在日志里打一个警告(如:time wait bucket table overflow),官网文档说这个参数是用来对抗 DDoS 攻击的。也说的默认值 180000 并不小。这个还是需要根据实际情况考虑。
Again,使用 tcp_tw_reuse 和 tcp_tw_recycle 来解决 TIME_WAIT 的问题是非常非常危险的,因为这两个参数违反了 TCP 协议(RFC 1122)
其实,TIME_WAIT 表示的是你主动断连接,所以,这就是所谓的“不作死不会死”。试想,如果让对端断连接,那么这个破问题就是对方的了,呵呵。另外,如果你的服务器是于 HTTP 服务器,那么设置一个HTTP 的 KeepAlive有多重要(浏览器会重用一个 TCP 连接来处理多个 HTTP 请求),然后让客户端去断链接(你要小心,浏览器可能会非常贪婪,他们不到万不得已不会主动断连接)。
关于 TIME_WAIT 这个问题,还可以参考
最合适的解决方案是增加更多的四元组数目,比如,服务器监听端口,或服务器 IP,让服务器能容纳足够多的 TIME-WAIT 状态连接。在我们常见的互联网架构中(NGINX 反代跟 NGINX,NGINX 跟 FPM,FPM 跟 redis、mysql、memcache 等),减少 TIME-WAIT 状态的 TCP 连接,最有效的是使用长连接,不要用短连接,尤其是负载均衡跟 web 服务器之间。尤其是链家事件中的 PHP 连不上 redis。
Q: TIME_WAIT 过多怎么办?
TIME_WAIT 存在的理由
- 可靠地实现 TCP 全双工连接的终止
在进行关闭连接四次挥手协议时,最后的 ACK 是由主动关闭端发出的,如果这个最终的 ACK 丢失,服务器将重发最终的 FIN,因此客户端必须维护状态信息允许它重发最终的 ACK。如果不维持这个状态信息,那么客户端将响应 RST 分节,服务器将此分节解释成一个错误(在 java 中会抛出 connection reset 的 SocketException)。
因而,要实现 TCP 全双工连接的正常终止,必须处理终止序列四个分节中任何一个分节的丢失情况,主动关闭的客户端必须维持状态信息进入 TIME_WAIT 状态。
- 允许老的重复分节在网络中消逝
TCP 分节可能由于路由器异常而“迷途”,在迷途期间,TCP 发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个原来的迷途分节就称为 lost duplicate。
在关闭一个 TCP 连接后,马上又重新建立起一个相同的 IP 地址和端口之间的 TCP 连接,后一个连接被称为前一个连接的化身(incarnation),那么有可能出现这种情况,前一个连接的迷途重复分组在前一个连接终止后出现,从而被误解成从属于新的化身。
为了避免这个情况,TCP 不允许处于 TIME_WAIT 状态的连接启动一个新的化身,因为 TIME_WAIT 状态持续 2MSL,就可以保证当成功建立一个 TCP 连接的时候,来自连接先前化身的重复分组已经在网络中消逝。
数据传输中的 Sequence Number
下图是我从 Wireshark 中截了个我在访问 coolshell.cn 时的有数据传输的图给你看一下,SeqNum 是怎么变的。(使用 Wireshark 菜单中的 Statistics ->Flow Graph… )
你可以看到,SeqNum 的增加是和传输的字节数相关的。上图中,三次握手后,来了两个 Len:1440 的包,而第二个包的 SeqNum 就成了 1441。然后第一个 ACK 回的是 1441,表示第一个 1440 收到了。
注意:如果你用 Wireshark 抓包程序看 3 次握手,你会发现 SeqNum 总是为 0,不是这样的,Wireshark 为了显示更友好,使用了 Relative SeqNum——相对序号,你只要在右键菜单中的 protocol preference 中取消掉就可以看到“Absolute SeqNum”了
TCP 重传机制
TCP 要保证所有的数据包都可以到达,所以,必需要有重传机制。
注意,接收端给发送端的 Ack 确认只会确认最后一个连续的包,比如,发送端发了 1,2,3,4,5 一共五份数据,接收端收到了 1,2,于是回 ack 3,然后收到了 4(注意此时 3 没收到),此时的 TCP 会怎么办?我们要知道,因为正如前面所说的,SeqNum 和 Ack 是以字节数为单位,所以 ack 的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。
超时重传机制
一种是不回 ack,死等 3,当发送方发现收不到 3 的 ack 超时后,会重传 3。一旦接收方收到 3 后,会 ack 回 4——意味着 3 和 4 都收到了。
但是,这种方式会有比较严重的问题,那就是因为要死等 3,所以会导致 4 和 5 即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到 Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致 4 和 5 的重传。
对此有两种选择:
- 一种是仅重传 timeout 的包。也就是第 3 份数据。
- 另一种是重传 timeout 后所有的数据,也就是第 3,4,5 这三份数据。
这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等 timeout,timeout 可能会很长(在下篇会说 TCP 是怎么动态地计算出 timeout 的)
快速重传机制
于是,TCP 引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就 ack 最后那个可能被丢了的包,如果发送方连续收到 3 次相同的 ack,就重传。Fast Retransmit 的好处是不用等 timeout 了再重传。
比如:如果发送方发出了 1,2,3,4,5 份数据,第一份先到送了,于是就 ack 回 2,结果 2 因为某些原因没收到,3 到达了,于是还是 ack 回 2,后面的 4 和 5 都到了,但是还是 ack 回 2,因为 2 还是没有收到,于是发送端收到了三个 ack=2 的确认,知道了 2 还没有到,于是就马上重转 2。然后,接收端收到了 2,此时因为 3,4,5 都收到了,于是 ack 回 6。示意图如下:
Fast Retransmit 只解决了一个问题,就是 timeout 的问题,它依然面临一个艰难的选择,就是,是重传之前的一个还是重传所有的问题。对于上面的示例来说,是重传#2 呢还是重传#2,#3,#4,#5 呢?因为发送端并不清楚这连续的 3 个 ack(2)是谁传回来的?也许发送端发了 20 份数据,是#6,#10,#20 传来的呢。这样,发送端很有可能要重传从 2 到 20 的这堆数据(这就是某些 TCP 的实际的实现)。可见,这是一把双刃剑。
SACK 方法
另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在 TCP 头里加一个 SACK 的东西,ACK 还是 Fast Retransmit 的 ACK,SACK 则是汇报收到的数据碎版。参看下图:
这样,在发送端就可以根据回传的 SACK 来知道哪些数据到了,哪些没有到。于是就优化了 Fast Retransmit 的算法。当然,这个协议需要两边都支持。在 Linux 下,可以通过tcp_sack参数打开这个功能(Linux 2.4 后默认打开)。
这里还需要注意一个问题——接收方 Reneging,所谓 Reneging 的意思就是接收方有权把已经报给发送端 SACK 里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖 SACK,还是要依赖 ACK,并维护 Time-Out,如果后续的 ACK 没有增长,那么还是要把 SACK 的东西重传,另外,接收端这边永远不能把 SACK 的包标记为 Ack。
注意:SACK 会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆 SACK 的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。详细的东西请参看《TCP SACK 的性能权衡》
Duplicate SACK – 重复收到数据的问题
Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉发送方有哪些数据被重复接收了。RFC-2883 里有详细描述和示例。下面举几个例子(来源于RFC-2883)
D-SACK 使用了 SACK 的第一个段来做标志,
- 如果 SACK 的第一个段的范围被 ACK 所覆盖,那么就是 D-SACK
- 如果 SACK 的第一个段的范围被 SACK 的第二个段覆盖,那么就是 D-SACK
示例一:ACK 丢包
下面的示例中,丢了两个 ACK,所以,发送端重传了第一个数据包(3000-3499),于是接收端发现重复收到,于是回了一个 SACK=3000-3500,因为 ACK 都到了 4000 意味着收到了 4000 之前的所有数据,所以这个 SACK 就是 D-SACK——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是 ACK 包。
Transmitted Received ACK Sent
Segment Segment (Including SACK Blocks)
3000-3499 3000-3499 3500 (ACK dropped)
3500-3999 3500-3999 4000 (ACK dropped)
3000-3499 3000-3499 4000, SACK=3000-3500
示例二,网络延误
下面的示例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到 ACK,而后面到达的三个包触发了“Fast Retransmit 算法”,所以重传,但重传时,被延误的包又到了,所以,回了一个 SACK=1000-1500,因为 ACK 已到了 3000,所以,这个 SACK 是 D-SACK——标识收到了重复的包。
这个案例下,发送端知道之前因为“Fast Retransmit 算法”触发的重传不是因为发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延时了。
Transmitted Received ACK Sent
Segment Segment (Including SACK Blocks)
500-999 500-999 1000
1000-1499 (delayed)
1500-1999 1500-1999 1000, SACK=1500-2000
2000-2499 2000-2499 1000, SACK=1500-2500
2500-2999 2500-2999 1000, SACK=1500-3000
1000-1499 1000-1499 3000
1000-1499 3000, SACK=1000-1500
---------
可见,引入了 D-SACK,有这么几个好处:
1)可以让发送方知道,是发出去的包丢了,还是回来的 ACK 包丢了。
2)是不是自己的 timeout 太小了,导致重传。
3)网络上出现了先发的包后到的情况(又称 reordering)
4)网络上是不是把我的数据包给复制了。
知道这些东西可以很好得帮助 TCP 了解网络情况,从而可以更好的做网络上的流控。
Linux 下的 tcp_dsack 参数用于开启这个功能(Linux 2.4 后默认打开)
TCP 的 RTT 算法
从前面的 TCP 重传机制我们知道 Timeout 的设置对于重传非常重要。
- 设长了,重发就慢,丢了老半天才重发,没有效率,性能差;
- 设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
而且,这个超时时间在不同的网络的情况下,根本没有办法设置一个死的值。只能动态地设置。 为了动态地设置,TCP 引入了 RTT——Round Trip Time,也就是一个数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间,从而可以方便地设置 Timeout——RTO(Retransmission TimeOut),以让我们的重传机制更高效。 听起来似乎很简单,好像就是在发送端发包时记下 t0,然后接收端再把这个 ack 回来时再记一个 t1,于是 RTT = t1 – t0。没那么简单,这只是一个采样,不能代表普遍情况。
经典算法
RFC793 中定义的经典算法是这样的:
1)首先,先采样 RTT,记下最近好几次的 RTT 值。
2)然后做平滑计算 SRTT( Smoothed RTT)。公式为:(其中的 α 取值在 0.8 到 0.9 之间,这个算法英文叫 Exponential weighted moving average,中文叫:加权移动平均)
SRTT = ( α _ SRTT ) + ((1- α) _ RTT)
3)开始计算 RTO。公式如下:
RTO = min [ UBOUND, max [ LBOUND, (β * SRTT) ] ]
其中:
- UBOUND 是最大的 timeout 时间,上限值
- LBOUND 是最小的 timeout 时间,下限值
- β 值一般在 1.3 到 2.0 之间。
Karn / Partridge 算法
但是上面的这个算法在重传的时候会出有一个终极问题——你是用第一次发数据的时间和 ack 回来的时间做 RTT 样本值,还是用重传的时间和 ACK 回来的时间做 RTT 样本值?
这个问题无论你选那头都是按下葫芦起了瓢。 如下图所示:
- 情况(a)是 ack 没回来,所以重传。如果你计算第一次发送和 ACK 的时间,那么,明显算大了。
- 情况(b)是 ack 回来慢了,但是导致了重传,但刚重传不一会儿,之前 ACK 就回来了。如果你是算重传的时间和 ACK 回来的时间的差,就会算短了。
所以 1987 年的时候,搞了一个叫Karn / Partridge Algorithm,这个算法的最大特点是——忽略重传,不把重传的 RTT 做采样(你看,你不需要去解决不存在的问题)。
但是,这样一来,又会引发一个大 BUG——如果在某一时间,网络闪动,突然变慢了,产生了比较大的延时,这个延时导致要重转所有的包(因为之前的 RTO 很小),于是,因为重转的不算,所以,RTO 就不会被更新,这是一个灾难。 于是 Karn 算法用了一个取巧的方式——只要一发生重传,就对现有的 RTO 值翻倍(这就是所谓的 Exponential backoff),很明显,这种死规矩对于一个需要估计比较准确的 RTT 也不靠谱。
Jacobson / Karels 算法
前面两种算法用的都是“加权移动平均”,这种方法最大的毛病就是如果 RTT 有一个大的波动的话,很难被发现,因为被平滑掉了。所以,1988 年,又有人推出来了一个新的算法,这个算法叫 Jacobson / Karels Algorithm(参看RFC6289)。这个算法引入了最新的 RTT 的采样和平滑过的 SRTT 的差距做因子来计算。 公式如下:(其中的 DevRTT 是 Deviation RTT 的意思)
SRTT **= S****RTT** + α **(****RTT** – S**RTT**) —— 计算平滑 RTT
DevRTT = (1-β**)*DevRTT** + β***(|**RTT-SRTT**|)** ——计算平滑 RTT 和真实的差距(加权移动平均)
*RTO= µ * SRTT + ∂ DevRTT —— 神一样的公式
(其中:在 Linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——这就是算法中的“调得一手好参数”,nobody knows why, it just works…) 最后的这个算法在被用在今天的 TCP 协议中(Linux 的源代码在:tcp_rtt_estimator)。
TCP 滑动窗口
需要说明一下,如果你不了解 TCP 的滑动窗口这个事,你等于不了解 TCP 协议。我们都知道,TCP 必需要解决的可靠传输以及包乱序(reordering)的问题,所以,TCP 必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。
所以,TCP 引入了一些技术和设计来做网络流控,Sliding Window 是其中一个技术。 前面我们说过,TCP 头里有一个字段叫 Window,又叫 Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。 为了说明滑动窗口,我们需要先看一下 TCP 缓冲区的一些数据结构:
上图中,我们可以看到:
- 接收端 LastByteRead 指向了 TCP 缓冲区中读到的位置,NextByteExpected 指向的地方是收到的连续包的最后一个位置,LastByteRcved 指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。
- 发送端的 LastByteAcked 指向了被接收端 Ack 过的位置(表示成功发送确认),LastByteSent 表示发出去了,但还没有收到成功确认的 Ack,LastByteWritten 指向的是上层应用正在写的地方。
于是:
- 接收端在给发送端回 ACK 中会汇报自己的 AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
- 而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。
下面我们来看一下发送方的滑动窗口示意图:
(图片来源)
上图中分成了四个部分,分别是:(其中那个黑模型就是滑动窗口)
- #1 已收到 ack 确认的数据。
- #2 发还没收到 ack 的。
- #3 在窗口中还没有发出的(接收方还有空间)。
- #4 窗口以外的数据(接收方没空间)
下面是个滑动后的示意图(收到 36 的 ack,并发出了 46-51 的字节):
下面我们来看一个接受端控制发送端的图示:
(图片来源)
Zero Window
上图,我们可以看到一个处理缓慢的 Server(接收端)是怎么把 Client(发送端)的 TCP Sliding Window 给降成 0 的。此时,你一定会问,如果 Window 变成 0 了,TCP 会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想像成“Window Closed”,那你一定还会问,如果发送端不发数据了,接收方一会儿 Window size 可用了,怎么通知发送端呢?
解决这个问题,TCP 使用了 Zero Window Probe 技术,缩写为 ZWP,也就是说,发送端在窗口变成 0 后,会发 ZWP 的包给接收方,让接收方来 ack 他的 Window 尺寸,一般这个值会设置成 3 次,第次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后还是 0 的话,有的 TCP 实现就会发 RST 把链接断了。
注意:只要有等待的地方都可能出现 DDoS 攻击,Zero Window 也不例外,一些攻击者会在和 HTTP 建好链发完 GET 请求后,就把 Window 设置为 0,然后服务端就只能等待进行 ZWP,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。(关于这方面的攻击,大家可以移步看一下Wikipedia 的 SockStress 词条)
另外,Wireshark 中,你可以使用 tcp.analysis.zero_window 来过滤包,然后使用右键菜单里的 follow TCP stream,你可以看到 ZeroWindowProbe 及 ZeroWindowProbeAck 的包。
Silly Window Syndrome
Silly Window Syndrome 翻译成中文就是“糊涂窗口综合症”。正如你上面看到的一样,如果我们的接收方太忙了,来不及取走 Receive Windows 里的数据,那么,就会导致发送方越来越小。到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的 window,而我们的发送方会义无反顾地发送这几个字节。
要知道,我们的 TCP+IP 头有 40 个字节,为了几个字节,要达上这么大的开销,这太不经济了。
另外,你需要知道网络上有个 MTU,对于以太网来说,MTU 是 1500 字节,除去 TCP+IP 头的 40 个字节,真正的数据传输可以有 1460,这就是所谓的 MSS(Max Segment Size)注意,TCP 的 RFC 定义这个 MSS 的默认值是 536,这是因为 RFC 791里说了任何一个 IP 设备都得最少接收 576 尺寸的大小(实际上来说 576 是拨号的网络的 MTU,而 576 减去 IP 头的 20 个字节就是 536)。
如果你的网络包可以塞满 MTU,那么你可以用满整个带宽,如果不能,那么你就会浪费带宽。(大于 MTU 的包有两种结局,一种是直接被丢了,另一种是会被重新分块打包发送) 你可以想像成一个 MTU 就相当于一个飞机的最多可以装的人,如果这飞机里满载的话,带宽最高,如果一个飞机只运一个人的话,无疑成本增加了,也而相当二。
所以,Silly Windows Syndrome 这个现像就像是你本来可以坐 200 人的飞机里只做了一两个人。 要解决这个问题也不难,就是避免对小的 window size 做出响应,直到有足够大的 window size 再响应,这个思路可以同时实现在 sender 和 receiver 两端。
- 如果这个问题是由 Receiver 端引起的,那么就会使用 David D Clark’s 方案。在 receiver 端,如果收到的数据导致 window size 小于某个值,可以直接 ack(0)回 sender,这样就把 window 给关闭了,也阻止了 sender 再发数据过来,等到 receiver 端处理了一些数据后 windows size 大于等于了 MSS,或者,receiver buffer 有一半为空,就可以把 window 打开让 send 发送数据过来。
- 如果这个问题是由 Sender 端引起的,那么就会使用著名的 Nagle’s algorithm。这个算法的思路也是延时处理,他有两个主要的条件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到之前发送数据的 ack 回包,他才会发数据,否则就是在攒数据。
另外,Nagle 算法默认是打开的,所以,对于一些需要小包场景的程序——比如像 telnet 或 ssh 这样的交互性比较强的程序,你需要关闭这个算法。你可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)
1
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (``char *)&value,``sizeof``(``int``));
另外,网上有些文章说 TCP_CORK 的 socket option 是也关闭 Nagle 算法,这不对。TCP_CORK 其实是更新激进的 Nagle 算汉,完全禁止小包发送,而 Nagle 算法没有禁止小包发送,只是禁止了大量的小包发送。最好不要两个选项都设置。
TCP 的拥塞处理 – Congestion Handling
上面我们知道了,TCP 通过 Sliding Window 来做流控(Flow Control),但是 TCP 觉得这还不够,因为 Sliding Window 需要依赖于连接的发送端和接收端,其并不知道网络中间发生了什么。TCP 的设计者觉得,一个伟大而牛逼的协议仅仅做到流控并不够,因为流控只是网络模型 4 层以上的事,TCP 的还应该更聪明地知道整个网络上的事。
具体一点,我们知道 TCP 通过一个 timer 采样了 RTT 并计算 RTO,但是,**如果网络上的延时突然增加,那么,TCP 对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是,这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的 TCP 连接都这么行事,那么马上就会形成“网络风暴”,TCP 这个协议就会拖垮整个网络。**这是一个灾难。
所以,TCP 不能忽略网络上发生的事情,而无脑地一个劲地重发数据,对网络造成更大的伤害。对此 TCP 的设计理念是:TCP 不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。
关于拥塞控制的论文请参看《Congestion Avoidance and Control》(PDF)
拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。这四个算法不是一天都搞出来的,这个四算法的发展经历了很多时间,到今天都还在优化中。 备注:
- 1988 年,TCP-Tahoe 提出了 1)慢启动,2)拥塞避免,3)拥塞发生时的快速重传
- 1990 年,TCP Reno 在 Tahoe 的基础上增加了 4)快速恢复
慢热启动算法 – Slow Start
首先,我们来看一下 TCP 的慢热启动。慢启动的意思是,刚刚加入网络的连接,一点一点地提速,不要一上来就像那些特权车一样霸道地把路占满。新同学上高速还是要慢一点,不要把已经在高速上的秩序给搞乱了。
慢启动的算法如下(cwnd 全称 Congestion Window):
1)连接建好的开始先初始化 cwnd = 1,表明可以传一个 MSS 大小的数据。
2)每当收到一个 ACK,cwnd++; 呈线性上升
3)每当过了一个 RTT,cwnd = cwnd*2; 呈指数让升
4)还有一个 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入“拥塞避免算法”(后面会说这个算法)
所以,我们可以看到,如果网速很快的话,ACK 也会返回得快,RTT 也会短,那么,这个慢启动就一点也不慢。下图说明了这个过程。
这里,我需要提一下的是一篇 Google 的论文《An Argument for Increasing TCP’s Initial Congestion Window》Linux 3.0 后采用了这篇论文的建议——把 cwnd 初始化成了 10 个 MSS。而 Linux 3.0 以前,比如 2.6,Linux 采用了RFC3390,cwnd 是跟 MSS 的值来变的,如果 MSS< 1095,则 cwnd = 4;如果 MSS>2190,则 cwnd=2;其它情况下,则是 3。
拥塞避免算法 – Congestion Avoidance
前面说过,还有一个 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入“拥塞避免算法”。一般来说 ssthresh 的值是 65535,单位是字节,当 cwnd 达到这个值时后,算法如下:
1)收到一个 ACK 时,cwnd = cwnd + 1/cwnd
2)当每过一个 RTT 时,cwnd = cwnd + 1
这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。
拥塞状态时的算法
前面我们说过,当丢包的时候,会有两种情况:
1)等到 RTO 超时,重传数据包。TCP 认为这种情况太糟糕,反应也很强烈。
- sshthresh = cwnd /2
- cwnd 重置为 1
- 进入慢启动过程
2)Fast Retransmit 算法,也就是在收到 3 个 duplicate ACK 时就开启重传,而不用等到 RTO 超时。
-
TCP Tahoe 的实现和 RTO 超时一样。
-
TCP Reno 的实现是:
- cwnd = cwnd /2
- sshthresh = cwnd
- 进入快速恢复算法——Fast Recovery
上面我们可以看到 RTO 超时后,sshthresh 会变成 cwnd 的一半,这意味着,如果 cwnd<=sshthresh 时出现的丢包,那么 TCP 的 sshthresh 就会减了一半,然后等 cwnd 又很快地以指数级增涨爬到这个地方时,就会成慢慢的线性增涨。我们可以看到,TCP 是怎么通过这种强烈地震荡快速而小心得找到网站流量的平衡点的。
快速恢复算法 – Fast Recovery
TCP Reno
这个算法定义在[RFC5681](http://tools.ietf.org/html/rfc5681 ““TCP Congestion Control”")。快速重传和快速恢复算法一般同时使用。快速恢复算法是认为,你还有 3 个 Duplicated Acks 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。 注意,正如前面所说,进入 Fast Recovery 之前,cwnd 和 sshthresh 已被更新:
- cwnd = cwnd /2
- sshthresh = cwnd
然后,真正的 Fast Recovery 算法如下:
- cwnd = sshthresh + 3 * MSS (3 的意思是确认有 3 个数据包被收到了)
- 重传 Duplicated ACKs 指定的数据包
- 如果再收到 duplicated Acks,那么 cwnd = cwnd +1
- 如果收到了新的 Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。
如果你仔细思考一下上面的这个算法,你就会知道,上面这个算法也有问题,那就是——它依赖于 3 个重复的 Acks。注意,3 个重复的 Acks 并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到 RTO 超时,于是,进入了恶梦模式——超时一个窗口就减半一下,多个超时会超成 TCP 的传输速度呈级数下降,而且也不会触发 Fast Recovery 算法了。
通常来说,正如我们前面所说的,SACK 或 D-SACK 的方法可以让 Fast Recovery 或 Sender 在做决定时更聪明一些,但是并不是所有的 TCP 的实现都支持 SACK(SACK 需要两端都支持),所以,需要一个没有 SACK 的解决方案。而通过 SACK 进行拥塞控制的算法是 FACK(后面会讲)
TCP New Reno
于是,1995 年,TCP New Reno(参见 RFC 6582 )算法提出来,主要就是在没有 SACK 的支持下改进 Fast Recovery 算法的——
- 当 sender 这边收到了 3 个 Duplicated Acks,进入 Fast Retransimit 模式,开发重传重复 Acks 指示的那个包。如果只有这一个包丢了,那么,重传这个包后回来的 Ack 会把整个已经被 sender 传输出去的数据 ack 回来。如果没有的话,说明有多个包丢了。我们叫这个 ACK 为 Partial ACK。
- 一旦 Sender 这边发现了 Partial ACK 出现,那么,sender 就可以推理出来有多个包被丢了,于是乎继续重传 sliding window 里未被 ack 的第一个包。直到再也收不到了 Partial Ack,才真正结束 Fast Recovery 这个过程
我们可以看到,这个“Fast Recovery 的变更”是一个非常激进的玩法,他同时延长了 Fast Retransmit 和 Fast Recovery 的过程。
算法示意图
下面我们来看一个简单的图示以同时看一下上面的各种算法的样子:
FACK 算法
FACK 全称 Forward Acknowledgment 算法,论文地址在这里(PDF)Forward Acknowledgement: Refining TCP Congestion Control 这个算法是其于 SACK 的,前面我们说过 SACK 是使用了 TCP 扩展字段 Ack 了有哪些数据收到,哪些数据没有收到,他比 Fast Retransmit 的 3 个 duplicated acks 好处在于,前者只知道有包丢了,不知道是一个还是多个,而 SACK 可以准确的知道有哪些包丢了。 所以,SACK 可以让发送端这边在重传过程中,把那些丢掉的包重传,而不是一个一个的传,但这样的一来,如果重传的包数据比较多的话,又会导致本来就很忙的网络就更忙了。所以,FACK 用来做重传过程中的拥塞流控。
- 这个算法会把 SACK 中最大的 Sequence Number 保存在snd.fack这个变量中,snd.fack 的更新由 ack 带秋,如果网络一切安好则和 snd.una 一样(snd.una 就是还没有收到 ack 的地方,也就是前面 sliding window 里的 category #2 的第一个地方)
- 然后定义一个awnd = snd.nxt – snd.fack(snd.nxt 指向发送端 sliding window 中正在要被发送的地方——前面 sliding windows 图示的 category#3 第一个位置),这样 awnd 的意思就是在网络上的数据。(所谓 awnd 意为:actual quantity of data outstanding in the network)
- 如果需要重传数据,那么,awnd = snd.nxt – snd.fack + retran_data,也就是说,awnd 是传出去的数据 + 重传的数据。
- 然后触发 Fast Recovery 的条件是: ( ( snd.fack – snd.una ) > (3*MSS) ) || (dupacks == 3) ) 。这样一来,就不需要等到 3 个 duplicated acks 才重传,而是只要 sack 中的最大的一个数据和 ack 的数据比较长了(3 个 MSS),那就触发重传。在整个重传过程中 cwnd 不变。直到当第一次丢包的 snd.nxt<=snd.una(也就是重传的数据都被确认了),然后进来拥塞避免机制——cwnd 线性上涨。
我们可以看到如果没有 FACK 在,那么在丢包比较多的情况下,原来保守的算法会低估了需要使用的 window 的大小,而需要几个 RTT 的时间才会完成恢复,而 FACK 会比较激进地来干这事。 但是,FACK 如果在一个网络包会被 reordering 的网络里会有很大的问题。
其它拥塞控制算法简介
TCP Vegas 拥塞控制算法
这个算法 1994 年被提出,它主要对 TCP Reno 做了些修改。这个算法通过对 RTT 的非常重的监控来计算一个基准 RTT。然后通过这个基准 RTT 来估计当前的网络实际带宽,如果实际带宽比我们的期望的带宽要小或是要多的活,那么就开始线性地减少或增加 cwnd 的大小。如果这个计算出来的 RTT 大于了 Timeout 后,那么,不等 ack 超时就直接重传。(Vegas 的核心思想是用 RTT 的值来影响拥塞窗口,而不是通过丢包) 这个算法的论文是《TCP Vegas: End to End Congestion Avoidance on a Global Internet》这篇论文给了 Vegas 和 New Reno 的对比:
关于这个算法实现,你可以参看 Linux 源码:/net/ipv4/tcp_vegas.h, /net/ipv4/tcp_vegas.c
HSTCP(High Speed TCP) 算法
这个算法来自RFC 3649(Wikipedia 词条)。其对最基础的算法进行了更改,他使得 Congestion Window 涨得快,减得慢。其中:
- 拥塞避免时的窗口增长方式: cwnd = cwnd + α(cwnd) / cwnd
- 丢包后窗口下降方式:cwnd = (1- β(cwnd))*cwnd
注:α(cwnd)和 β(cwnd)都是函数,如果你要让他们和标准的 TCP 一样,那么让 α(cwnd)=1,β(cwnd)=0.5 就可以了。 对于 α(cwnd)和 β(cwnd)的值是个动态的变换的东西。 关于这个算法的实现,你可以参看 Linux 源码:/net/ipv4/tcp_highspeed.c
TCP BIC 算法
2004 年,产内出 BIC 算法。现在你还可以查得到相关的新闻《Google:美科学家研发 BIC-TCP 协议 速度是 DSL 六千倍》 BIC 全称Binary Increase Congestion control,在 Linux 2.6.8 中是默认拥塞控制算法。BIC 的发明者发这么多的拥塞控制算法都在努力找一个合适的 cwnd – Congestion Window,而且 BIC-TCP 的提出者们看穿了事情的本质,其实这就是一个搜索的过程,所以 BIC 这个算法主要用的是 Binary Search——二分查找来干这个事。 关于这个算法实现,你可以参看 Linux 源码:/net/ipv4/tcp_bic.c
TCP WestWood 算法
westwood 采用和 Reno 相同的慢启动算法、拥塞避免算法。westwood 的主要改进方面:在发送端做带宽估计,当探测到丢包时,根据带宽值来设置拥塞窗口、慢启动阈值。 那么,这个算法是怎么测量带宽的?每个 RTT 时间,会测量一次带宽,测量带宽的公式很简单,就是这段 RTT 内成功被 ack 了多少字节。因为,这个带宽和用 RTT 计算 RTO 一样,也是需要从每个样本来平滑到一个值的——也是用一个加权移平均的公式。 另外,我们知道,如果一个网络的带宽是每秒可以发送 X 个字节,而 RTT 是一个数据发出去后确认需要的时候,所以,X _ RTT 应该是我们缓冲区大小。所以,在这个算法中,ssthresh 的值就是 est_BD _ min-RTT(最小的 RTT 值),如果丢包是 Duplicated ACKs 引起的,那么如果 cwnd > ssthresh,则 cwin = ssthresh。如果是 RTO 引起的,cwnd = 1,进入慢启动。 关于这个算法实现,你可以参看 Linux 源码: /net/ipv4/tcp_westwood.c
其它
更多的算法,你可以从 Wikipedia 的 TCP Congestion Avoidance Algorithm 词条中找到相关的线索
流量控制
什么是流量控制?流量控制的目的?
如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。流量控制根本目的是防止分组丢失,它是构成 TCP 可靠性的一方面。
如何实现流量控制?
IP 层协议属于不可靠的协议,IP 层并不关系数据是否发送到了对端,TCP 通过确认机制来保证数据传输的可靠性,在比较早的时候使用的是 send–wait–send 的模式,其实这种模式叫做 stop-wait 模式,发送数据方在发送数据之后会启动定时器,但是如果数据或者 ACK 丢失,那么定时器到期之后,收不到 ACK 就认为发送出现状况,要进行重传。这样就会降低了通信的效率,如下图所示,这种方式被称为 positive acknowledgment with retransmission (PAR)
由滑动窗口协议(连续 ARQ 协议)实现。滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。
拥塞控制
拥塞:即对资源的需求超过了可用的资源。若网络中许多资源同时供应不足,网络的性能就要明显变坏,整个网络的吞吐量随之负荷的增大而下降
拥塞控制:防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提:网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及与降低网络传输性能有关的所有因素。
流量控制:指点对点通信量的控制,是端到端正的问题。流量控制所要做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。
拥塞控制代价:需要获得网络内部流量分布的信息。在实施拥塞控制之前,还需要在结点之间交换信息和各种命令,以便选择控制的策略和实施控制。这样就产生了额外的开销。拥塞控制还需要将一些资源分配给各个用户单独使用,使得网络资源不能更好地实现共享。
因特网建议标准 RFC2581 定义了进行拥塞控制的四种算法
- 慢开始(slow start)
- 拥塞避免(congestion avoidance)
- 快重传(fast retransmit)
- 快恢复(fast recovery)
我们假定
- 数据是单方向传送,而另外一个方向只传送确认。
- 接收方总是有足够大的缓存空间,因为发送窗口的大小由网络的拥塞程度来决定。
下图是慢启动和拥塞避免的一个可视化描述。我们以段为单位来显示 cwnd 和 ssthresh,但它们实际上都是以字节为单位进行维护的。
拥塞窗口概念
发送报文段速率的确定,既要根据接收端的接收能力,又要从全局考虑不要使网络发生拥塞,这由接收窗口和拥塞窗口两个状态量确定。
- 接收端窗口(Reciver Window)又称通知窗口(Advertised Window),是接收端根据目前的接收缓存大小所许诺的最新窗口值,是来自接收端的流量控制。
- 拥塞窗口 cwnd(Congestion Window)是发送端根据自己估计的网络拥塞程度而设置的窗口值,是来自发送端的流量控制。
慢启动原理
- 当主机开始发送数据时,如果立即将较大的发送窗口的全部数据字节都注入到网络中,那么由于不清楚网络的情况,有可能引其网络拥塞
- 比较好的方法是试探一下,即从小到达逐渐增大发送端的拥塞控制窗口数值
- 通常在刚刚开始发送报文段时可先将拥塞窗口 cwnd(拥塞窗口)设置为一个最大报文段的 MSS 的数值。在每收到一个对新报文段确认后,将拥塞窗口增加至多一个 MSS 的数值,当 rwind(接收窗口)足够大的时候,为了防止 cwnd 增长过大引起网络拥塞,还需设置一个慢开始门限 ssthresh 状态变量。ssthresh 的用法如下:
当cwnd<ssthresh时,使用慢开始算法。
当cwnd>ssthresh时,改用拥塞避免算法。
当cwnd=ssthresh时,慢开始与拥塞避免算法任意。
拥塞避免
拥塞避免算法思路:让拥塞窗口缓慢增长,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加 1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。
无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为 1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。
乘法减小和加法增大
乘法减小:是指不论在慢开始阶段还是拥塞避免阶段,只要出现超时,就把慢开始门限减半,即设置为当前的拥塞窗口的一半(于此同时,执行慢开始算法)。当网络出现频繁拥塞时,ssthresh 值就下降的很快,以大大将小注入到网络中的分组数。
加法增大:是指执行拥塞避免算法后是拥塞窗口缓慢增大,以防止网络过早出现拥塞。
快重传和快恢复
一条 TCP 连接有时会因等待重传计时器的超时而空闲较长的时间,慢开始和拥塞避免无法很好的解决这类问题,因此提出了快重传和快恢复的拥塞控制方法。快重传算法并非取消了重传机制,只是在某些情况下更早的重传丢失的报文段(如果当发送端接收到三个重复的确认 ACK 时,则断定分组丢失,立即重传丢失的报文段,而不必等待重传计时器超时)。
快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。如下图:
快重传配合使用的还有快恢复算法,有以下两个要点:
- 当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把 ssthresh 门限减半。但是接下去并不执行慢开始算法。
- 考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将 cwnd 设置为 ssthresh 减半后的大小,然后执行拥塞避免算法。如下图:
在采用快恢复算法时,慢开始算法只是在 TCP 连接建立时和网络出现超时时才使用。
接受窗口又称为通知窗口。因此从接收方对发送方的流量控制角度考虑,发送方的发送窗口一定不能超过对方给出的接受窗口的 RWND
也就是说:发送窗口的上限=Min[rwnd,cwnd].
Reference
-
No backlinks found.