时间戳——RTTM与序列号回绕
时间戳格式
时间戳(TimeStamp—TSOPT)选项格式如下:
1 | // Kind: 8 |
TSval
表示发送端发出该报文时的本地时间戳, 而 TSecr
则负责回放 (Echo) 最近一次收到的对端报文中的 TSval
的值。下面是一组典型的时间戳交互过程:
1 | // TCP A TCP B |
启用 Timestamp 选项需要经过双方的协商,协商在三次握手时完成 ,如果协商成功,则在后续的报文中, 除了 RST 之外的所有报文均必须包含 Timestamp 选项,若未协商成功,即使后续报文有 Timestamp 选项也一并忽略。
时间戳作用
在RFC1323中,TSOPT主要有两个用途一个是 RTTM (round-trip time measurement)即根据 ACK 报文中的这个选项测量往返时延,另外一个用途是 PAWS (protect against wrapped sequence numbers),即防止同一个连接的系列号重叠。另外还有一些其他的用途,如 SYN-cookie、 Eifel Detection Algorithm 等等。下面详述前两个用途。
RTTM
对此,时间戳不能走得太慢,这是为了能更准确地测量报文的 RTT。假设这个时钟 10s 才 tick 一下,那么对于 往返时间为 1s 的 TCP 连接,一端发送报文之后,很有可能会发现收到对端的 ACK 报文中的 TSecr 和当前时钟 的值是一样的,这说明 RTT 为 0 ! 显然,这是十分荒谬的。需要说明的是,RTT 的测量不需要时间戳也能进行,只是使用时间戳可以解决 RTTM 中某些棘手的问题(区分重传报),参见另一篇文章:RTT的测量
TSOPT如何测量RTT?
一般而言,客户端和服务端会互发数据和确认,但此处为了更清楚地说明问题,我们只考虑客户端发送数据,服务端回应 ACK 报文。于是有以下过程:
1 | // 客户端 A 服务端 B |
客户端如此测量 RTT :将本端此刻时间戳填入 TSOPT 选项中的 TSval 部分,并发送报文 A ;服务端接收到报文 A 后,将 TSval 部分的值放入 ACK 报中的 TSecr 部分并发送;客户端接收到 ACK 报文后取出 TSecr ,并将此刻时间戳减去 TSecr ,即得到 RTT 。通常情况下,发送 ACK 报文时只需要无脑填入对端上一个报文的 TSval 就行了,但有几种特殊场景需要注意:
-
Delay ACK(延迟确认):
延迟确认有两大好处:1.减少网络中 ACK 包的数量(最多延迟一个包 ),以减小网络拥塞;2.如果接收端刚好也有数据发送,则可以将数据和 ACK一起发送(数据捎带确认),减少了 pure ACK 的数量。
如果启用了 Delay ACK, 并且接收端收到了多个报文,这些报文的 TSval 不同,那么应该 Echo 哪一个报文的 TSval 呢?答案是:需要 Echo 最早收到的那个报文的 TSval , 因为只有这样,发送端测量的 RTT 才更加准确(更保守)。本地会维护一个 TS.recent 变量,其用来保存下一个填入 TSecr 的时间戳,当需要发送报文时,报文的 TSecr 始终从当前 TS.Recent 获得。
1
2
3
4
5
6
7//延迟确认下: TS.Recent
// <A, TSval=1> -------------------> 1
//
// <B, TSval=2> -------------------> 1
//
// <---- <ACK(B), TSecr=1> 1
//可见,echo的是最早发送的报文 -
乱序或丢失:
发送端发送了多个报文,但中间有报文出现了丢失或者乱序,这会使得接收端的窗口产生空洞 (即在未收到序号较小的报文时,先收到序号较大的报文)。这种情况可能预示着链路发生了拥塞,因此,此时也会让接收方 Echo 稍早时候的 TSval ,而不是序号最大报文的 TSval , 这样使得发送端估算的 RTT 能偏大,也就是发送报文更保守,有利于减小拥塞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/*
发送顺序:A->B->C->D->E
接受顺序:
TS.Recent
<A, TSval=1> -------------------> 1
<---- <ACK(A), TSecr=1> 1
<C, TSval=3> -------------------> 1
<---- <ACK(A), TSecr=1> 1
<B, TSval=2> -------------------> 1->2
<---- <ACK(C), TSecr=2> 2
<E, TSval=5> -------------------> 2
<---- <ACK(C), TSecr=2> 2
<D, TSval=4> -------------------> 2->4
<---- <ACK(E), TSecr=4> 4
*/注意第 11 行,ACK 报文中的 TSecr = 1 而不是 3 ,原因是:1.使 RTT 偏大,保守计算;2.如果发送 3 ,则 TS.Recent 改为 3 ,那么接收到 B 包时,会计算
TSecr = 2 < TS.Recent = 3
,从而判断 B 是历史报文而导致错误丢弃。
PAWS
由于序号占 32 位,范围为 ~ ,即最大 4 个 GB 。在一般网络下(假定 1 MB /S),那么循环一圈序列号所花费的时间约为 1.19 小时,而
MSL
(报文最大生存时间)为 30 秒(Linux),所以一般情况下即使是发生序列号回绕也不会接收到历史报文。但在高速网络(千兆网)下,可能循环一圈只需要十几秒甚至几秒,此时网络中的历史报文就有较大概率落入新连接的窗口(窗口最大 1 GB),从而造成数据错乱。由此,还需要引入 时间戳 来解决此问题。
举个例子,假设发送端的序号从 1 开始计数,此时已经发送了 字节的数据,当前接收端的 RCV.NXT = 5000
, TS.Recent = 100
, 如果收到一个报文 Seq = 5000
, LEN = 1000
, TSval=70
如果不看时间戳,那么接收端会认为这个报文正好是预期的报文,但是这个报文实际上却是第 5000 ~ 5999 字节的报文;而有了时间戳,之间根据 TSval < TS.Resent
就可判断此报文已过期。
时间戳的时间部分占32位,不也会用完并回绕吗?
是的,时间戳也会回绕,不过需要时间戳是以时间为驱动进行增长(1ms,实现不同),而序列号是以字节为驱动进行增长。按照 1ms 的时间戳时钟计算,32-bit 的时间戳回绕一次的周期是 24.8 天, 而实际上连接实际不会这么长,所以远不用担心时间戳回绕。
时间戳还有什么作用?
文章参考:TCP 08 ,时间戳那点事 ,SYN Cookie ,RTT ,序列号回环