传输层协议(10):滑动窗口(4)

5.3.1.5 Zero Window(0窗口)

Zero Window(0窗口)指的是 TCP 发送方的滑动窗口大小为0,其本质上是因为 TCP 接收方的接收缓存已经满了,没有空间再接收数据。

当处于 Zero Window 时,TCP 发送方是不能向对方发送数据的——即使发送也会被对方给怼回来,如图5-107所示:

 

图5-107 Zero Window 示意

 

图5-107,T0 时刻,B 的接收缓存 = 100(个字节),A 的滑动窗口 = 100。T1 时刻,A 给 B 发送了1个包含100字节数据的报文,B 给 A 回以 ACK 报文。在这个 ACK 报文中,B 告诉 A,它的接收缓存为0(Window = 0)。

收到 B 的 ACK 报文以后,A 将自己的滑动窗口修改为0:SND.NXT = 201、SND.WND = 0。正常来说,滑动窗口为0,A 不应该再发送数据给 B。假设 A 不遵守这个规则,给 B 发送了数据,正如图5-107中的 T2 时刻所示:A 给 B 发送了1个包含10字节数据的报文,假设 B 的接收缓存还是满的,没有空间接收数据,那么 B 就会给 A 回以1个 ACK 报文,这个报文表达了两个含义:

(1)AKN = 201,跟其所接收的报文的 SEQ 相同,表示其根本就没有接收数据

(2)Window = 0,表明了自己的接收缓存为0,不能接收数据

不过接收缓存为0,只是表明 TCP 不能接收数据,不代表它不能接收报文——只要这个报文的数据长度为0,比如:ACK 报文、FIN 报文、URG = 1 但是数据长度为0 的报文。

ACK、FIN 报文比较好理解,尤其是 FIN 报文,它是要告诉接收方:就算你的接收缓存为0,你不能接收数据,那又有什么了不起,我还不想发送数据了呢,我已经没有数据要发送了,我想关闭连接。

URG = 1 但是长度为0的报文,通过“5.3.1.4 Urgent(紧急数据)”的介绍,我们也知道,TCP 想发这样的报文就发送吧,表面上很“紧急”的样子,实际上也没什么特别用处。

TCP 接收方能接收这样的报文,也就意味着,在 Zero Window 的情形下,TCP 发送方能发送这样的报文。除此之外,TCP 发送方还能发送另外一种包含数据的报文,这些数据是“已发送未确认”的数据,如图5-108所示:

 

图5-108 Zero Window 场景下可以发送的数据

 

图5-108,既表达了一个事实(Zero Window 的时候,“已发送未确认”的数据可以发送),同时也带来了一个困惑,这样的数据为什么可以发送,而其他数据为什么不可以发送?

“已发送未确认”的数据,发送过去,会被怼回来吗?会,如果当时接收方的接收缓存依然为0的话。

既然会被怼回来,那为什么还要发送呢?有什么意义呢?另外为什么它可以发送,其他数据却不能发送,这些数据又有什么特别吗?

让我们暂时平复一下心中的疑问,先看另外一个问题:就算“已发送未确认”的数据可以发送,那么如果这些数据为空呢?那么 TCP 发送方发送什么?永远不发送数据吗?

这个问题,换一种问法,就是:Zero Window,TCP 发送方的滑动窗口何时会变为大于0?

何当共剪西窗烛,却话巴山夜雨时。何时才能大于0?当然是接收方的接收缓存腾挪出了接收空间(数据提交了给应用层),TCP 发送方的滑动窗口才能大于0,如图5-109所示:

 

 图5-109 通知接收窗口大于0

 

图5-109中,当接收方(B)向应用层提交了100字节的数据以后,它就会向对方(A)方1个 ACK 报文,告知对方自己的接收缓存变为100了。A 收到该 ACK 报文以后,就会将自己的滑动窗口(SND.WND)调整为100。

这个机制可以总结为:TCP 的接收换从0变成大于0时,会给对方发送1个 ACK 报文,通知对方自己的接收缓存的大小。

这个机制似乎一点毛病都没有,但是......ACK 报文并不会收到对方针对该 ACK 报文的 ACK 报文,如图5-110所示:

 

图5-110 ACK 报文没有 ACK

 

图5-110中的“报文2”是不存在的,因为 TCP 协议就是如此规定的。即使不考虑 TCP 协议,我们脑补一下这个逻辑:假设1个 ACK 报文还需要另外1个 ACK 报文对它进行 ACK,那么就会“ACK 复 ACK,ACK 何时了”——ACK 报文就会没完没了。所以 ACK 报文没有 ACK!

然而,只要是报文就有可能丢失,无论它是不是 ACK 报文。假设图5-101的 ACK 报文(报文1)丢失了,那么 B 是无法知道该报文丢失了,它也就不会再重传这个报文。这就有点像:爱就一个字,我只说一次。 

 

 

我们现在来看看发生了什么事情:

(1)B 向 A发送了接收缓存由0变成大于0的通知(ACK 报文),但是通知报文丢了,而 B 并不知道,也不会再重发

(2)A 没有收到 B 发送的通知报文,它还一直以为滑动窗口为0,所以也就一直不再发送数据。

这就造成了死锁!“有的连接活着,它却已经死了”,说的大概就是这种故事吧。

TCP 当然不能让这样的故事发生!于是就产生了 Zero Probe 报文(零窗口探测报文)。广义的 Zero Probe 报文有两种。

第1种报文是广义 Zero Probe 报文:

如果 TCP 发送方还有“已发送未确认”数据,那么此时它就会发送该数据。现在我们知道,TCP 发送此数据的目的:是当作 Zero Probe 来使用的。这样的报文也称为“广义 Zero Probe 报文”。

TCP 发送广义 Zero Probe 报文,如果对方恰好腾挪出了接收缓存,那么就回以1个 ACK 报文,告知自己的接收缓存大小。如此一来,打破死锁,TCP 双方又可以愉快地收发报文了。

如果对方没有收到报文(广义 Zero Probe 报文丢失),或者对方的接收缓存还是为0,将这个报文给怼了回来,或者回怼的报文丢失了,这都没关系,对于 TCP 发送方来说,它认为都是一样的:滑动窗口还是为0,还是需要发送广义 Zero Probe 报文。处理这种情况,TCP 是周期发送该报文,RFC 793 建议的周期是2分钟。

第二种报文是狭义的 Zero Probe 报文。TCP 的术语中“Zero Probe”报文,指的就是这种:

如果 TCP 发送方没有“已发送未确认”数据,但是它同时还有数据需要发送。那么它就会发送 Zero Probe 报文。

Zero Probe 报文是 ACK 报文,它的数据长度为0,它的 SEQ 等于 SND.NXT - 1。正是由于 SEQ 的取值,使得 Zero Probe 报文虽然也是 ACK 报文,但是它与普通的 ACK 报文不同,如图5-111所示:

 

 

图5-111 Zero Probe 报文

 

图5-111中,因为发送方没有“已发送未确认”报文,所以 SND.UNA = SND.NXT,同时对于接收方而言,其 RCV.NXT 也应该等于 SND.NXT。

一个正常的报文,其 SEQ 应该等于 SND.NXT,但是 Zero Probe 报文的 SEQ 却等于 SND.NXT - 1 = RCV.NXT - 1,这一点 TCP 接收方是能感知到的。

所以 TCP 接收方收到这个报文以后,就会把它当作 Zero Probe 报文,然后回以1个 ACK 报文,以告知对方此时自己的接收窗口大小。

如果 Zero Probe 报文丢失了,如果 TCP 接收方回应的 ACK 报文同样告知接收缓存为0,如果 TCP 接收方回应的 ACK 报文丢失了,对于 TCP 发送方来说,都是一样的:它需要继续发送 Zero Probe 报文。

为此,TCP 启动了一个 Zero Probe Timer(零窗口探测定时器),也叫“坚持定时器”。只要定时器时间到, TCP 发送方就会发送 Zero Probe 报文,直到收到对方的 ACK 报文告知自己其接收缓存大于0。

TCP的坚持定时器使用1、2、4、8、16……64秒这样的普通指数退避序列来作为每一次的溢出时间。也就是说,定时器的第1个周期是1秒,第2个周期是2秒,第n个周期是2n-1秒,直到最大值64秒,以后的周期都是64秒。

以上讲述了两种 Zero Probe 报文,对应了两种场景。对于第3种场景:既没有“已发送待确认”的数据,也没有“需要发送”的数据,TCP 的策略是:不进行零窗口探测(不发送 Zero Probe 报文),当然也不会启动坚持定时器。

 

5.3.1.6 Keep Alive(保活)

本来 Keep Alive 应该放到“5.2 TCP 连接”那一章节讲述,但是由于 Keep Alive Probe报文跟 Zero Probe 报文格式是一样的,所以我们就将其放到这里。

Keep Alive(保活),可不是如它名字所讲述的那样,能够“保证活着”,它只是为了探测是否活着。这句话有点故弄玄虚,那么它到底是什么意思呢?我们先看两个名词:TCP Client、TCP Server。

在“5.2 TCP 连接”我们讲过:其实,所谓的 Client/Server,指的是 TCP 连接创建过程中,谁是主动发起的一方:谁主动发起,谁就是 TCP Client;谁被动接受,谁就是 TCP Server。

在实际的应用中,我们也会根据应用层的特点来标识 TCP Client、TCP Server,如图5-112所示:

 

 图5-112 TCP Client 与 TCP Server

 

图5-112中,DB Server 对应的就是 TCP Server,DB Client 对应的就是 TCP Client。这种区分方式,与根据谁是主动发起 TCP 连接请求的区分方式,两者本质上是一样的,只不过前者似乎更直观一点。

图5-112同时也表明了1个 TCP Server 会连接多个 TCP Client,也就是说在 TCP Server 端,会创建多个 TCP 连接。我们知道 TCP 连接本质上是一块内存,那么多个 TCP连接对于 TCP Server 来说,其实是一种负担。

每一个 TCP Server 心里都有一种释放 TCP 连接的冲动,但是它又不敢释放——错误的释放,会造成业务中断,这一点 TCP 可承担不起。

考虑到这样一种场景,TCP Server 发现某一个(或多个)TCP 连接上已经很久没有数据传输了(也就没有报文传送了),它是不是可以动点心思:这个连接是不是已经断了?TCP Client 是不是已经 Crash(崩溃)了?如果连接事实上已经断了,那么这个连接是不是就可以释放了?是不是能自己减少一点负担?

于是 TCP Sever 发起了 Keep Alive Probe(保活探测)报文——说是保活,实际上就是看看对方是不是还活着,它可真的不能保证对方不死,永远活着。

保活探测报文跟 Zero Probe 报文格式是一样的,都是1个 ACK 报文,而且最关键的,它的 SEQ = SND.NXT - 1。可以参考图5-111,对于一个长时间没有数据传输的连接来说,其 RCV.NXT = SND.NXT。所以,对于 TCP Client 来说,它收到这样1个 ACK 报文,同样知道这是1个探测报文。至于这个报文叫 Zero Probe 还是叫 Keep Alive Probe,这都没关系,这只是我们人类给这个报文所取得名字(场景不同,名字不同)而已,对于 TCP 来说都是一样的:收到这个报文,回以1个 ACK。

当 TCP Server 收到 TCP Client 的 ACK 以后,它发现对方还活着,那么就不能释放连接。那就继续再等待一段时间,如果这段时间内连接上仍然是空的,仍然是没有数据传输,TCP Server 就继续发送 Keep Alive Probe 报文。RFC 1122 推荐的这个等待时间是不小于2小时。

TCP Server 如果没有收到 TCP Client 的 ACK 报文,会有几种情形:

(1)网络中断,TCP Client 根本就收不到 TCP Server 的报文,当然也谈不上回以1个 ACK 报文

(2)TCP Client 已经 Crash(崩溃)或者正在重启的过程中,此时也谈不上回以1个 ACK 报文

(3)TCP Client 的 ACK 报文,再网络上丢失了

无论哪种情形,TCP Server 也不能贸然释放连接,它还得多发几次探测报文,以确保 TCP Client 确实是没有“活着”。

如果 TCP Client Crash 以后又重启了,那么它收到 TCP Server 发送过来的探测报文以后,会回以1个 RST 报文。这样也好,双方重新建立新的连接,开始新的生活,^_^。

需要说明一下,以上关于 TCP Server 主动发起保活探测,是基于实际的应用场景来表达的。理论上讲,无论是 TCP Client 还是 TCP Server,都可以发起保活探测。当然,由 TCP Client 发起的保活探测,基本没啥意义。

总的来说,RFC 1122 似乎并不太赞同 Keep Alive 机制,所以它建议:

(1)Keep Alive 机制应该设置一个开关选项,允许用户关闭该机制,并且这个选项默认必须是关闭的

(2)空闲时间也须交于用户设置,并且默认值是不小于2小时。当在这段时间内,连接上没有数据传输,TCP 才能发送 Keep Alive Probe 报文。

RFC 1122 不太建议采用 Keep Alive 机制的理由是:

(1)可能误伤 TCP 连接。如果网络只是中断一点时间,恰好被保活探测赶上了,那就会“错误”地释放连接

(2)保活探测报文占用带宽

(3)保活探测报文需要花钱(因为带宽不是免费的)

无论 RFC 1122 多么不喜欢 Keep Alive,基本上很多 TCP 的具体实现,都支持了 Keep Alive 机制,比如 Linux。在Linux的 /etc/sysctl.conf 文件中有如下配置(值可以修改):

 

# 空闲时常(7200秒,2小时):如果这段时间内连接上没有数据传输,

# 那就发送保活探测报文

net.ipv4.tcp_keepalive_time=7200

 

# 探测周期(75秒):如果第1次发送保活探测报文没有收到对方的 ACK 报文

#(也没有收到 RST 报文),那就继续周期探测,探测周期为75秒。

# 与之对应的定时器,也称为保活定时器

net.ipv4.tcp_keepalive_intvl=75

 

# 探测次数(9)次:如果探测了9次,对方都没有回应,那么就释放连接

net.ipv4.tcp_keepalive_probes=9

 

最后要补充一点,前文我们一直说,Keep Alive Probe 报文与 Zero Probe 报文一样,那也就意味着这个 ACK 报文的数据长度为0。但是历史上有一些 TCP 的错误实现,它们要求 Keep Alive Probe 报文必须包含1个字节的垃圾数据(这个数据没啥用,但是它就是需要发送1个字节):it MAY be configurable to send a keep-alive segment containing one garbage octet, for compatibility with erroneous TCP implementations.

 

5.3.7 Window Scale Option

Window Scale 是 TCP 的1个选项(Option),在讲述这个选项之前,我们先看1个问题:

A 和 B 之间相距1500km(公里),两者之间运行 TCP,那么从 A 传输到 B的最大数据带宽是多少(1秒钟最多能传输多少字节的数据)?

如果您不知道答案,笔者建议您先思考5分钟再往下阅读,这样更能加深理解。

要回答这个问题,我们需要明确两点:

(1)TCP 发送方的滑动窗口最大是216(64K),因为标识滑动窗口大小的 Window 字段,其占位16个 bits。我们把这个最大值(64K)记为 MWS(max window size)

(2)TCP 发送方在连续发送 MWS 个字节后,至少要停顿1次,等待对方的 ACK 报文,才能发送下一批数据。

图5-113表达了以上两点:

 

 图5-113 滑动窗口最大数据带宽示意图

 

图5-113中,为了计算最大数据带宽,我们假设:(1)TCP 1个报文可以发送 64K 个字节;(2)网络传输时延只有光速所引发的时延;(3)TCP 接收方处理时间为0,即图5-113中的 Δtr = 0。

T0 时刻,A 的滑动窗口为64K。T1 时刻,A 发送了64K 数据给 B,此时它的滑动窗口变为0,不能再发送数据。T2 时刻,A 收到了 B 的 ACK 报文,滑动窗口又变为了64K(然后它又可以继续发送报文)。

那么 T2 - T1,就是光速在 A 和 B 之间1个来回所经历的时间,也就是0.01秒。由此,我们可以计算出 A 传输到 B 的最大数据带宽是:

64K ÷ 0.01 = 6.4M Bps = 64M bps

以上的计算是假设 TCP 1个报文能包含64K字节的数据,稍微有点不真实,如果我们考虑到 MSS(max segment size),TCP 最大数据带宽计算的示意,如图5-114所示:

 

 图5-114 滑动窗口最大数据带宽示意图(2)

 

图5-114中,为了计算最大带宽,我们假设从 T1 到 T64,TCP 连续发送64个报文(每个报文1K字节),并且假设从 T1 到 T64之间的时间间隔 Δts = 0,当然我们也假设 Δtr = 0。但是从 T64 与 T65 之间,TCP 发送方必须要等待对方的 ACK 报文,才能发送下一批数据。

图5-113的假设,有点像发炮,图5-114的假设有点像机枪扫射。不过无论是哪种比喻,我们都是假设发射的时间为0,只是计算了换炮/子弹的时间。所以,基于图5-114,我们仍然可以计算出 TCP 最大数据带宽是64Mbps。

 

 

 

 

由于数据长度是1K,即使加上40字节的报文头(IP 报文头 + TCP 报文头),其影响也是微乎其微。

在两端相距1500km的情况下,TCP 理论最大带宽是64Mbps。在两端相距150km的情形下,TCP 理论最大带宽是640Mbps。无论是64M还是640M,在当今这样的年代,这样的理论最大带宽无论如何都可以说是 TCP 的缺点,因为它无法有效利用物理层、数据链路层的技术发展成果。

这样的话,增大TCP 发送方的滑动窗口(接收方的接收缓存)就非常有必要。但是从协议的角度,TCP 该如何增大它的 Window 字段呢?将 Window 字段从16 bits 扩大到更多的 bits(比如32 bits)?这不是不可以,但是兼容性有问题。

为此,TCP(RFC 7323)给出的解决方案是 TCP Window Scale Option。

 

5.3.7.1 Window Scale Option 概述

TCP(RFC 1323)给出的解决方案是 TCP Window Scale Option,如图5-115所示:

  

图5-115 TCP Window Scale Option

 

TCP Window Scale Option,Kind = 3,一共占用3个 bytes,其中数据部分占用1个 byte,其含义是 shift.cnt,我们不必在意其英语含义,直奔主题,假设 shift.cnt = 10,那么:

(1)假设原来的 Window = W(比如 64K)

(2)新的 Window = 2shift.cn * W = 210 * 64K = 64M

也就是说,shift.cn(TCP Window Scale Option)将原来的 Window 放大了 2shift.cn倍。因为shift.cn 最大可以等于255,那么原来的 Window 最大可以放大2255倍。这是一个非常大的数字:2255约等于5.79*1076,再加上 Window 本身就有 216,约等于6.55*104,那么Window 的 size 一共约等于3.79*1081。而根据目前的科学知识估算,宇宙中的粒子总数是3.28*1080个。也就是说,TCP 的滑动窗口,经过 Window Scale 的放大以后,可以装的下10个宇宙的粒子。

IT 发展史上,由于当时客观条件或者主观认识的限制,发生过几起著名的对未来估计不足的案例,比如千年虫事件、IPv4 地址不足的烦恼。而 TCP Window Scale,笔者以为就算到将来 TCP 被抛弃的那一天,甚至宇宙崩溃的那一天,shift.cn 也是够用的,虽然它只占用了1个字节。

 

另外,Window Scale Option 既然是 TCP 的一个选项,那么它就需要协商。其实与其说是协商,不如说是通知。

对于 A、B 来说,假设 A 是 TCP 连接创建的主动发起方,那么 A 可以在它的 SYN 报文中带上 Window Scale Option,那意思就是通知 B:我的接收窗口将扩大 2shift.cnt倍,希望你心理能有点数。当然 B 心理肯定会有数——也仅仅是心理有数即可,B 并不需要给 B 回应什么特别的东西——B所需要做的就是将自己的滑动窗口默默放大2shift.cnt倍即可。

TCP 是全双工,同时两个方向也是解耦的。A 通知了 B 它的接收窗口放大了2shift.cnt倍,但是不代表 B 也需要将自己的接收窗口也放大,它完全可以保持原状,或者即使放大,它也不需要跟 A 一样放大同样的倍数。不过如果 B 需要放大,那么它就得在自己的 <SYN/ACK>报文中带上自己的 Window Scale Option,并且赋上自己的 shift.cnt 值。

通过以上描述,我们知道,Window Scale Option 必须在 SYN 或者 SYN/ACK 报文中协商(通知),其他报文中如果携带有 Window Scale Option,TCP 将会忽略这个选项。

需要注意的是,虽然 TCP 通过 SYN、SYN/ACK 报文协商,但是 Window Scale Option 在这两个报文中并不会生效,也就是说这两个报文的 Window Size 并不会放大。这也比较好理解:这个报文仅仅是打个招呼(协商/通知),下一个报文才会生效。

 

5.3.7.2 shift.cnt 的大小与 SEQ 的绕接

前文说过,经过 Window Scale 放大以后,TCP Window 的 size 最大约等于3.79*1081,是当前已知的宇宙中的粒子数的10倍。但是,这么大的数字能用得完吗?还真没法用完,不要说计算机没有那么大的内存,仅仅是 TCP 协议本身就限制了 shift.cn 的取值范围。我们回忆一下 TCP 的 Sequence Number 字段,它占位32字节,也就说最大值是232(4G),考虑到一种极端的情况,如图5-116所示:

 

图5-116 Sequence Number 的限制

 

图5-116说的是一个比较极端的情况,A 对 B 来了一个连续点射,1次只发送1个字节,从T1 时刻开始,一直到 T3 时刻,一共发送了232个字节,同时 B 在 T5 时刻回应1个 ACK 报文。但是在 T3 时刻,问题已经出现了:由于 SEQ 只有32 bits,它的最大值是 232-1,也就说说在 T3 时刻,SEQ 必须要绕接,其值只能等于0。这时候,我们再看 T4 时刻,B一共收到了2个 SEQ = 0 的报文,它必须要抛弃其中1个报文,这显然是不符合 A 的本意。

这个例子虽然极端,但是 TCP 从逻辑自洽的角度考虑,必须限制 Window 的大小,其最大值必须要小于 232。因为 Window 原来就占有16 bits,这也就推导出 shift.cnt 最大值只能是 16(216 * 2shift.cnt = 216 * 216 = 232)。

而实际上,shift.cnt 最大值仅仅是14。这是为什么呢?这还要从 TCP Sequence Number(SEQ)的绕接说起。

我们知道,SEQ 只有32 bits,它的最大值是232-1,然后又得绕接到0,从0开始逐步增加——一直加到232-1,然后再绕接到0......

但是问题来了:在以前的叙述中,我们为了易于表达和易于理解,都特意回避了绕接这个话题,两个序列号 SEQ1、SEQ2,如果 SEQ1 < SEQ2,我们就认为是 SEQ1 先发送、SEQ2 后发送。可是如果叠加上绕接的概念,那么谁先谁后呢?如图5-117所示:

 

图5-117 SEQ 绕接所带来的困惑

 

图5-117中,SEQ1 = 100,SEQ2 = 110,如果不考虑绕接,SEQ1(所对应的报文)显然比 SEQ2(所对应的报文)先发送。但是,如果考虑绕接呢?我们怎么能确认:为什么不是 SEQ2 先发送,然后过了一段时间 SEQ 绕接之后,再发送的 SEQ1?

 

 

真叫人头大!应该说,在绕接的情况下,没有什么好方法来确定谁先谁后,除非加上一定的约束。我们先来看看 Linux 相应的代码——这个算法不仅仅是 Linux 的算法,也是 TCP 标准的算法:

 

# __u32: unsigned int 32

# __s32:signed int 32

static inline int before(__u32 seq1, __u32 seq2)

{

return (__s32)(seq1-seq2) < 0;

}

 

这段代码非常简单,两个序列号 seq1、seq2 都是无符号整数(__u32),函数名“before”的意思,就是判断谁在前谁在后,而它的判断算法就是:

(1)__u32 a = seq1-seq2;

(2)将 a 转换为有符号整数:__s32 b = (__s32) a;

(3)如果 b < 0,那么 seq1 就在 seq2 之前;否则,seq1 就在 seq2 之后

这个算法是什么意思?为了易于理解,我们简化描述,假设 SEQ 只有4 bits,也就是说 SEQ大于15就需要绕接,如图5-118所示:

 

 图5-118 before 函数的具体示例

 

图5-118中,SEQ1 = 12,SEQ2 = 2,那么 SEQ1 与 SEQ2 谁先谁后呢?按照 Linux 的那段代码,我们计算 SEQ1 - SEQ2,如图5-119所示:

 

图5-119 SEQ1 - SEQ2

 

通过图5-119可以看到,如果只考虑无符号整数,a = 1100 - 0010 = 1010(10),但是如果将 a 从无符号整数转变为有符号整数 b = (__s32) a = -2。

因为 b < 0,所以认为 SEQ1 在 SEQ2 前面,也就是说 SEQ2 是绕接后的结果:虽然单独看 SEQ2 的数字(2)它比 SEQ1(12)小,但是它却是是 SEQ1 后发送。

这有什么道理?这没有什么道理!Linux 就是这么规定的,TCP 也是这么规定的!与其说这是一个算法,不如说它是一个规定。

那么,这个规定是不是很霸道,是不是就是完全不讲道理呢?我们继续看1个算式:SEQ1 - 8,如图5-120所示:

 

图5-120 SEQ1 - 8

 

图5-120中,SEQ1(12)- 8 = 4,无论是无符号整数(a)还是有符号整数(b),它们的值都是4,都大于0。回到 Linux 算法,因为 b > 0,所以 SEQ1 在 SEQ2 之后,也就是说是先发送的 SEQ2,后发送的 SEQ1。

那么,问题来了,我们为什么要让 SEQ1 减去8呢?8有什么特殊?8确实很特殊,前面我们假设 SEQ 只有4 bits,而8等于23,即8是 SEQ 空间(24 = 16,0~15)的一半。

可是,这个所谓的一半,又有什么特别含义呢?我们知道,在 SEQ 的空间是 0~15的场景下,最容易发生绕接的 SEQ 的值是15,所以,我们就以15这个值,让它分别减去7、8、9,如图5-121所示:

 

图5-121 15 - 7、8、9

 

从图5-121可以看到(您也可以自己计算15减去其他值),8,SEQ 空间的一半,恰好是以一个分水岭。

这说明什么?这恰恰说明了一个大白话,如图5-122所示:

 

图5-122 TCP 绕接判断算法的大白话

 

 图5-122中有3个序列号:s1、s2、t,要判断 t1、t2 相对 s 来说是不是绕接,就是看它们之间的差值:(x = t - s)

(1)x1 = t - s1

(2)x2 = t - s2

(3)x1、x2 都是无符号整数(无符号整数、无符号整数、无符号整数),

关键点来了:只要 x(x1、x2)小于 SEQ 空间的一半(2n-1),那么就没有绕接,否则就是绕接了。

用大白话说就是:如果两者相差不大(小于最大值的一半),就没有绕接;否则,就是绕接。

当然,我说这个是大白话,RFC 可能会不开心,它们认为我把安吉拉说成了翠花。RFC 7323 是这么表述的:

 

s < t  if 0 < (t - s) < 2^31,

computed in unsigned 32-bit arithmetic

 

这句话的意思是:(1)都当作32位无符号整数进行计算;(2)如果 t - s 大于0并且小于最大值的一半(最大值是232),那么就认为 s 在 t 的前面(没有绕接)。 

 

春节回到家,安吉拉变翠花

 

是安吉拉也好,是翠花也罢,TCP 关于 SEQ 是否绕接的算法,都需要发送方的配合。如果发送方不配合,TCP 这个算法就是错的,如图5-123所示:

 

图5-123 如果发送方不配合

 

图5-123中,A 一股脑发送了一堆报文,在 T1 时刻,其 SEQ_T1 = 0,在 T2 时刻,其 SEQ_T2 = 232 - 1。显然,SEG_T2 - SEG_T1 > 231,但是我们不能说 SEQ_T1 是绕接的,是在 SEQ_T2 之后发送的。

那么,TCP 的发送方该如何配合 TCP 的绕接判断算法呢?没办法,为了能正确判断 SEQ是否绕接,TCP 只能牺牲发送效率,硬性规定:接收窗口(也就是发送方的滑动窗口)的 size,其最大值只能是 SEQ 空间的一半,即231。如此一来,图5-123所描述的故事就不会发生。

然而,故事还没有结束。回忆一下“5.2.12.2 通用处理思路”所介绍的 TCP 接收到报文以后,关于报文 SEQ 的判断:

 

RCV.NXT <= SEG.SEQ < RCV.NXT + RCV.WND ----公式(1)

or

RCV.NXT <= SEG.SEQ + SEG.LEN - 1 < RCV.NXT + RCV.WND ----公式(2)

 

这两个公式只要有1个满足,那么所接收报文的 SEQ 就是合法的。关于这两个公式的细节,建议您参阅“5.2.12.2”节,这里只说结论:虽然 TCP 规定了 Window 的 size,但是对于1个报文来说,它所包含的数据的长度,最大可能是 Window 的 size 的2倍。RFC 7323 的描述是:the sender and receiver windows can be out of phase by at most the window size。 

 

自己挖的坑就要自己填

 

由于有这么一个“坑”,TCP 只能自己填上。于是,TCP 又将 Window 的 size 减少一半。如此一来,TCP Window 的 size 的最大值只能是230。

因为 TCP Window 原来的最大值是216(占有16个 bits),那么也就推导出 Window Scale Option 中的 shift.cnt 的最大值只能是14。

既然 TCP Window 的size 的最大值只能是230,那么 TCP 理论上最大的传输带宽是多少呢?我们可以按照本节一开始所描述的方法计算如下(假设两地相距1500公里):

TCP 理论最大带宽 = 230 / 0.01 秒 = 100GBps = 1000Gbps = 1Tbps

可以看到,即使在即将到来的5G时代,TCP 也不会过时。

 

我们感谢美国引发了互联网革命,但是同时我们也看到,5G时代,美国却在扮演着一个非常不光彩的角色。

没有强大的祖国,这世界上又哪里能安放一张安静书桌?!

为中华之崛起而读书!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值