4
4
5
5
** 为什么 TCP 每次建立连接时,初始化序列号都要不一样呢?**
6
6
7
- 接下来,我一步一步给大家讲明白,我觉得应该有不少人会有类似的问题,所以今天在肝一篇 !
7
+ 接下来,我一步一步给大家讲明白,我觉得应该有不少人会有类似的问题,所以今天再肝一篇 !
8
8
9
9
> 为什么 TCP 每次建立连接时,初始化序列号都要不一样呢?
10
10
11
11
主要原因是为了防止历史报文被下一个相同四元组的连接接收。
12
12
13
13
> TCP 四次挥手中的 TIME_WAIT 状态不是会持续 2 MSL 时长,历史报文不是早就在网络中消失了吗?
14
14
15
- 是的,如果能正常四次挥手,由于 TIME_WAIT 状态会持续 2 MSL 时长,历史报文会在下一个连接之前就会自然消失。
15
+ 是的,如果能正常四次挥手,由于 TIME_WAIT 状态会持续 2 MSL 时长,历史报文会在下一个连接之前就会自然消失。
16
16
17
17
但是来了,我们并不能保证每次连接都能通过四次挥手来正常关闭连接。
18
18
44
44
45
45
> 那客户端和服务端的初始化序列号都是随机的,那还是有可能随机成一样的呀?
46
46
47
- RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。
47
+ RFC793 提到初始化序列号 ISN 随机生成算法:` ISN = M + F(localhost, localport, remotehost, remoteport) ` 。
48
48
49
49
- M 是一个计时器,这个计时器每隔 4 微秒加 1。
50
- - F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值,要保证 hash 算法不能被外部轻易推算得出。
50
+ - F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值,要保证 Hash 算法不能被外部轻易推算得出。
51
51
52
52
可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。
53
53
@@ -66,11 +66,11 @@ RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost,
66
66
67
67
通过前面我们知道,** 序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据** 。
68
68
69
- 不要以为序列号的上限值是 4GB ,就以为很大,很难发生回绕。在一个速度足够快的网络中传输大量数据时,序列号的回绕时间就会变短。如果序列号回绕的时间极短,我们就会再次面临之前延迟的报文抵达后序列号依然有效的问题。
69
+ 不要以为序列号的上限值是 4 GB ,就以为很大,很难发生回绕。在一个速度足够快的网络中传输大量数据时,序列号的回绕时间就会变短。如果序列号回绕的时间极短,我们就会再次面临之前延迟的报文抵达后序列号依然有效的问题。
70
70
71
- 为了解决这个问题,就需要有 TCP 时间戳。tcp_timestamps 参数是默认开启的,开启了 tcp_timestamps 参数,TCP 头部就会使用时间戳选项,它有两个好处,** 一个是便于精确计算 RTT,另一个是能防止序列号回绕(PAWS)** 。
71
+ 为了解决这个问题,就需要有 TCP 时间戳。` tcp_timestamps ` 参数是默认开启的,开启了 ` tcp_timestamps ` 参数,TCP 头部就会使用时间戳选项,它有两个好处,** 一个是便于精确计算 RTT,另一个是能防止序列号回绕(PAWS)** 。
72
72
73
- 试看下面的示例,假设 TCP 的发送窗口是 1 GB,并且使用了时间戳选项,发送方会为每个 TCP 报文分配时间戳数值,我们假设每个报文时间加 1,然后使用这个连接传输一个 6GB 大小的数据流。
73
+ 试看下面的示例,假设 TCP 的发送窗口是 1 GB,并且使用了时间戳选项,发送方会为每个 TCP 报文分配时间戳数值,我们假设每个报文时间加 1,然后使用这个连接传输一个 6 GB 大小的数据流。
74
74
75
75
![ 图片] ( https://img-blog.csdnimg.cn/img_convert/1d497c38621ebc44ee3d8763fd03da67.png )
76
76
@@ -92,8 +92,8 @@ RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost,
92
92
93
93
Linux 以本地时钟计数(jiffies)作为时间戳的值,不同的增长时间会有不同的问题:
94
94
95
- - 如果时钟计数加 1 需要 1ms ,则需要约 24.8 天才能回绕一半,只要报文的生存时间小于这个值的话判断新旧数据就不会出错。
96
- - 如果时钟计数提高到 1us 加 1,则回绕需要约 71.58 分钟才能回绕,这时问题也不大,因为网络中旧报文几乎不可能生存超过 70 分钟,只是如果 70 分钟没有报文收发则会有一个包越过 PAWS(这种情况会比较多见,相比之下 24 天没有数据传输的 TCP 连接少之又少),但除非这个包碰巧是序列号回绕的旧数据包而被放入接收队列(太巧了吧),否则也不会有问题;
95
+ - 如果时钟计数加 1 需要 1 ms ,则需要约 24.8 天才能回绕一半,只要报文的生存时间小于这个值的话判断新旧数据就不会出错。
96
+ - 如果时钟计数提高到 1 us 加 1,则回绕需要约 71.58 分钟才能回绕,这时问题也不大,因为网络中旧报文几乎不可能生存超过 70 分钟,只是如果 70 分钟没有报文收发则会有一个包越过 PAWS(这种情况会比较多见,相比之下 24 天没有数据传输的 TCP 连接少之又少),但除非这个包碰巧是序列号回绕的旧数据包而被放入接收队列(太巧了吧),否则也不会有问题;
97
97
- 如果时钟计数提高到 0.1 us 加 1 回绕需要 7 分钟多一点,这时就可能会有问题了,连接如果 7 分钟没有数据收发就会有一个报文越过 PAWS,对于 TCP 连接而言这么短的时间内没有数据交互太常见了吧!这样的话会频繁有包越过 PAWS 检查,从而使得旧包混入数据中的概率大大增加;
98
98
99
99
Linux 在 PAWS 检查做了一个特殊处理,如果一个 TCP 连接连续 24 天不收发数据则在接收第一个包时基于时间戳的 PAWS 会失效,也就是可以 PAWS 函数会放过这个特殊的情况,认为是合法的,可以接收该数据包。
@@ -104,7 +104,7 @@ static inline bool tcp_paws_check(const struct tcp_options_received *rx_opt, int
104
104
{
105
105
......
106
106
107
- //从上次收到包到现在经历的时间多于 24 天,返回 true
107
+ // 从上次收到包到现在经历的时间多于 24 天,返回 true
108
108
if (unlikely(get_seconds() >= rx_opt->ts_recent_stamp + TCP_PAWS_24DAYS))
109
109
return true;
110
110
@@ -115,7 +115,7 @@ static inline bool tcp_paws_check(const struct tcp_options_received *rx_opt, int
115
115
116
116
要解决时间戳回绕的问题,可以考虑以下解决方案:
117
117
118
- 1)增加时间戳的大小,由 32 bit 扩大到 64bit
118
+ 1)增加时间戳的大小,由 32 bit 扩大到 64 bit
119
119
120
120
这样虽然可以在能够预见的未来解决时间戳回绕的问题,但会导致新旧协议兼容性问题,像现在的 IPv4 与 IPv6 一样
121
121
0 commit comments