skaiuijing

引言

前面我们查阅了RFC文档,现在让我们从连接开始讲TCP模型。

后面的章节中,源码采用的是4.4FreeBSD代码,笔者参考的书籍是TCP详解卷二。

书上已经讲得非常详细了,但是笔者为什么还要重复讲一遍呢?

1.因为书上太详细了,甚至包括了很多操作系统的细节,但是对于TCP的原理来说,理解这些细节不是必要的。

2.书上几乎都是一行行代码讲解,但是我们的目标是理解TCP的整体框架

所以,笔者的讲解是在TCP卷一和TCP卷二的源码实现中寻找一个平衡点,既能把理论讲解清楚,又能让读者明白TCP的源码实现框架。

也就是说,笔者专注于的是TCP的整体运行框架。

为了达到这一点,必须要以动态的视角看待TCP源码实现。

我们必须要从数据包的视角看待TCP协议栈的源码处理,所以,我们还是要与三次握手、四次挥手联系起来。

什么是有连接

我们经常说TCP是有连接的协议,到底什么是有链接?

确认

我们需要确保对方收到了我们之前的数据报。

一个很直接的思想就是进行一个确认:

A:访问发了一个数据报,你收到了吗?

B:我收到了

A:好,那我就继续发了。

其实这就是确认机制。

三次握手和四次挥手

TCP是面向连接的协议,现在我们需要先建立连接,

可能有很多人疑问,为什么三次握手和四次挥手呢?

其实很简单,就算确保双方都能互相确认:

A->B,B知道了A可以向他发送消息,但是A还不知道这一点。

B->A,A知道B可以向他发送消息,但是B还不知道。

A->B,B知道A知道B可以向他发送消息。

好了,现在双方都知道了可以互相发送消息了。

这就是三次握手。

至于四次挥手,

A->B:B知道A要关闭了,但目前A还不知道B知道A要关闭,所以B要发消息给A。

B->A:A知道B知道他要关闭了,所以A可以直接关闭了。但是B还不知道A知不知道,所以A最后还要发消息给B

因为四次挥手本质上也是与握手一样,都是双方的确认,所以在Linux等操作系统中,B->A的两次操作可能会被合并为一次。

也就是说,其实握手和挥手都可以是三次。

三次是双方互相确认的最小次数。

最后A->B:现在双方都确认了,可以安心关闭了。

明白了这一点后,让我们看看完整的握手与挥手:

TCP数据包的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
         主动开启者               被动开启者
LISTEN
被动打开
SYN_SENT ------SYN K-----------> SYN_RCVD
/
ESTABLISHED <---SYN L, ACK K + 1--
----ACK K + 1--------> ESTABLISHED
数据传输发生在ESTABLISHED状态
FIN_WAIT_1 -------FIN M + ACK ------> CLOSED_WAIT
/
FIN_WAIT_2 <-----ACK M + 1---------
LAST_ACK 其实这两次操作往往也都会被合并为一次
/
TIME_WAIT <------FIN N---------
| -------ACK N +1 --------> \
|2MSL计时器 CLOSED
CLOSED

接下来看看TCP首部,我们的重点是seq和ack,一个是发送号,另一个是确认号。

TCP结构体字段

首部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct tcphdr {
unsigned short th_sport;
unsigned short th_dport;
unsigned int th_seq;
unsigned int th_ack;
//little ENDIAN!!!
unsigned char th_x2:4;
unsigned char th_off:4;
unsigned char th_flags;
unsigned short th_win;
unsigned short th_sum;
unsigned short th_urp;
}__attribute__((packed));

这里我们先不介绍tcp控制块。

序号与确认号

初始化序列号ISN

协议栈中有一个变量ISN,记录的是初始化序列号,参考RFC9293:

3.4.1 初始序列号(ISN)的选择

  • 每个连接的实例(化身)必须有唯一的初始序列号,以避免旧段被误认为新段。
  • ISN 由一个 单调递增的 32 位计数器生成,通常每 4 微秒加 1,约 4.55 小时循环一次,远大于 MSL(最大报文段生存期)。
  • 为防止攻击者预测 ISN,ISN = **计数器值 M + 伪随机函数 F(…)**,其中 F 结合了本地/远程 IP、端口和一个秘密密钥。

所以,在连接建立时,双方会交换序列号。

首部中的seq和ack,在单向传输过程中一个发送,一个确认。

  • **序号 (seq)**:标记报文段中第一个字节在整个字节流中的位置。
  • **确认号 (ack)**:累计确认,表示“到这个序号之前的字节我都收到了”。
    • A 的 snd_una ← B 的 ack
    • B 的 rcv_nxt ← A 的 seq + len

为了详细说明,还是先抓个包吧:

TCP通信示例

用tcp简单抓包:

简约:

1
2
3
4
5
6
7
8
23:36:05.107559 IP skaiuijing-VMware-Virtual-Platform.59746 > DESKTOP-JIO0OM2.9090: Flags [S], seq 2574454702, win 64240, options [mss 1460,sackOK,TS val 2467129094 ecr 0,nop,wscale 7], length 0
23:36:05.108077 IP DESKTOP-JIO0OM2.9090 > skaiuijing-VMware-Virtual-Platform.59746: Flags [S.], seq 2010931041, ack 2574454703, win 64240, options [mss 1460], length 0
23:36:05.108118 IP skaiuijing-VMware-Virtual-Platform.59746 > DESKTOP-JIO0OM2.9090: Flags [.], ack 1, win 64240, length 0
23:36:05.108900 IP DESKTOP-JIO0OM2.9090 > skaiuijing-VMware-Virtual-Platform.59746: Flags [P.], seq 1:1431, ack 1, win 64240, length 1430
23:36:05.108922 IP skaiuijing-VMware-Virtual-Platform.59746 > DESKTOP-JIO0OM2.9090: Flags [.], ack 1431, win 65535, length 0
23:36:05.109153 IP DESKTOP-JIO0OM2.9090 > skaiuijing-VMware-Virtual-Platform.59746: Flags [FP.], seq 1431, ack 1, win 64240, length 0
23:36:05.109245 IP skaiuijing-VMware-Virtual-Platform.59746 > DESKTOP-JIO0OM2.9090: Flags [F.], seq 1, ack 1432, win 65535, length 0
23:36:05.109371 IP DESKTOP-JIO0OM2.9090 > skaiuijing-VMware-Virtual-Platform.59746: Flags [.], ack 2, win 64239, length 0

由于不能很好的看出双方的ACK和SEQ变化,再抓一份详细的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    192.168.91.129.37790 > 192.168.31.190.9090: Flags [S], cksum 0xfcbe (incorrect -> 0x08d8), seq 3651959169, win 64240, options [mss 1460,sackOK,TS val 2467211849 ecr 0,nop,wscale 7], length 0
0x0000: 0050 56f1 7990 000c 29a8 3f54 0800 4500 .PV.y...).?T..E.
0x0010: 003c 7e6e 4000 4006 bfbd c0a8 5b81 c0a8 .<~n@.@.....[...
0x0020: 1fbe 939e 2382 d9ac 7981 0000 0000 a002 ....#...y.......
0x0030: faf0 fcbe 0000 0204 05b4 0402 080a 930e ................
0x0040: aa49 0000 0000 0103 0307 .I........
23:37:27.863131 IP (tos 0x0, ttl 128, id 46358, offset 0, flags [none], proto TCP (6), length 44)
192.168.31.190.9090 > 192.168.91.129.37790: Flags [S.], cksum 0xe08e (correct), seq 1067742738, ack 3651959170, win 64240, options [mss 1460], length 0
0x0000: 000c 29a8 3f54 0050 56f1 7990 0800 4500 ..).?T.PV.y...E.
0x0010: 002c b516 0000 8006 8925 c0a8 1fbe c0a8 .,.......%......
0x0020: 5b81 2382 939e 3fa4 7612 d9ac 7982 6012 [.#...?.v...y.`.
0x0030: faf0 e08e 0000 0204 05b4 0000 ............
23:37:27.863197 IP (tos 0x0, ttl 64, id 32367, offset 0, flags [DF], proto TCP (6), length 40)
192.168.91.129.37790 > 192.168.31.190.9090: Flags [.], cksum 0xfcaa (incorrect -> 0xf84b), seq 1, ack 1, win 64240, length 0
0x0000: 0050 56f1 7990 000c 29a8 3f54 0800 4500 .PV.y...).?T..E.
0x0010: 0028 7e6f 4000 4006 bfd0 c0a8 5b81 c0a8 .(~o@.@.....[...
0x0020: 1fbe 939e 2382 d9ac 7982 3fa4 7613 5010 ....#...y.?.v.P.
0x0030: faf0 fcaa 0000 ......
23:37:27.864539 IP (tos 0x0, ttl 128, id 46359, offset 0, flags [none], proto TCP (6), length 826)
192.168.31.190.9090 > 192.168.91.129.37790: Flags [P.], cksum 0x20ae (correct), seq 1:787, ack 1, win 64240, length 786
0x0000: 000c 29a8 3f54 0050 56f1 7990 0800 4500 ..).?T.PV.y...E.
0x0010: 033a b517 0000 8006 8616 c0a8 1fbe c0a8 .:..............
0x0020: 5b81 2382 939e 3fa4 7613 d9ac 7982 5018 [.#...?.v...y.P.
0x0030: faf0 20ae 0000 4b4f 4d53 4d59 474b 4d53 ......KOMSMYGKMS
0x0040: 5949 434c 4154 4348 4447 434a 4e49 485a YICLATCHDGCJNIHZ
0x0050: 4754 5251 5542 444b 5048 5843 4a49 4a4b GTRQUBDKPHXCJIJK
0x0060: 4858 4a52 4e42 414e 444f 4152 5456 4b42 HXJRNBANDOARTVKB
0x0070: 454a 4457 424c 424e 4555 485a 4e42 554c EJDWBLBNEUHZNBUL
0x0080: 4b52 4a41 4f41 4a47 4c46 5948 4359 4b48 KRJAOAJGLFYHCYKH
0x0090: 4a53 4257 4844 524c 5447 5345 4d5a 5647 JSBWHDRLTGSEMZVG
0x00a0: 5548 4358 5153 4450 4c4d 4941 5657 4548 UHCXQSDPLMIAVWEH
\\省略一堆数据
23:37:27.864559 IP (tos 0x0, ttl 64, id 32368, offset 0, flags [DF], proto TCP (6), length 40)
192.168.91.129.37790 > 192.168.31.190.9090: Flags [.], cksum 0xfcaa (incorrect -> 0xf84b), seq 1, ack 787, win 63454, length 0
0x0000: 0050 56f1 7990 000c 29a8 3f54 0800 4500 .PV.y...).?T..E.
0x0010: 0028 7e70 4000 4006 bfcf c0a8 5b81 c0a8 .(~p@.@.....[...
0x0020: 1fbe 939e 2382 d9ac 7982 3fa4 7925 5010 ....#...y.?.y%P.
0x0030: f7de fcaa 0000 ......
23:37:27.864694 IP (tos 0x0, ttl 128, id 46360, offset 0, flags [none], proto TCP (6), length 40)
192.168.31.190.9090 > 192.168.91.129.37790: Flags [FP.], cksum 0xf530 (correct), seq 787, ack 1, win 64240, length 0
0x0000: 000c 29a8 3f54 0050 56f1 7990 0800 4500 ..).?T.PV.y...E.
0x0010: 0028 b518 0000 8006 8927 c0a8 1fbe c0a8 .(.......'......
0x0020: 5b81 2382 939e 3fa4 7925 d9ac 7982 5019 [.#...?.y%..y.P.
0x0030: faf0 f530 0000 0000 0000 0000 ...0........
23:37:27.864796 IP (tos 0x0, ttl 64, id 32369, offset 0, flags [DF], proto TCP (6), length 40)
192.168.91.129.37790 > 192.168.31.190.9090: Flags [F.], cksum 0xfcaa (incorrect -> 0xf84a), seq 1, ack 788, win 63453, length 0
0x0000: 0050 56f1 7990 000c 29a8 3f54 0800 4500 .PV.y...).?T..E.
0x0010: 0028 7e71 4000 4006 bfce c0a8 5b81 c0a8 .(~q@.@.....[...
0x0020: 1fbe 939e 2382 d9ac 7982 3fa4 7926 5011 ....#...y.?.y&P.
0x0030: f7dd fcaa 0000 ......
23:37:27.864958 IP (tos 0x0, ttl 128, id 46361, offset 0, flags [none], proto TCP (6), length 40)
192.168.31.190.9090 > 192.168.91.129.37790: Flags [.], cksum 0xf538 (correct), seq 788, ack 2, win 64239, length 0
0x0000: 000c 29a8 3f54 0050 56f1 7990 0800 4500 ..).?T.PV.y...E.
0x0010: 0028 b519 0000 8006 8926 c0a8 1fbe c0a8 .(.......&......
0x0020: 5b81 2382 939e 3fa4 7926 d9ac 7983 5010 [.#...?.y&..y.P.
0x0030: faef f538 0000 0000 0000 0000 ...8........
^C

抓包内容表

阶段 报文方向 Flags Seq / Ack (tcpdump显示) 长度 状态机含义
1 192.168.91.129:37790 → 192.168.31.190:9090 [S] seq=3651959169, ack=0 0 A 发 SYN,请求建立连接,ISN_A=3651959169
2 192.168.31.190:9090 → 192.168.91.129:37790 [S.] seq=1067742738, ack=3651959170 0 B 回 SYN+ACK,ISN_B=1067742738,确认 A 的 SYN
3 192.168.91.129:37790 → 192.168.31.190:9090 [.] seq=1, ack=1 0 A 回 ACK,确认 B 的 SYN,三次握手完成,进入 ESTABLISHED
4 192.168.31.190:9090 → 192.168.91.129:37790 [P.] seq=1:787, ack=1 786 B 发送数据 786 字节,PSH 表示尽快交付应用
5 192.168.91.129:37790 → 192.168.31.190:9090 [.] seq=1, ack=787 0 A 确认收到 B 的 786 字节,累计确认 ack=787
6 192.168.31.190:9090 → 192.168.91.129:37790 [FP.] seq=787, ack=1 0 B 发送 FIN+PSH,表示数据发送完毕并请求关闭
7 192.168.91.129:37790 → 192.168.31.190:9090 [F.] seq=1, ack=788 0 A 回 ACK 并发送 FIN,表示自己也准备关闭
8 192.168.31.190:9090 → 192.168.91.129:37790 [.] seq=788, ack=2 0 B 确认 A 的 FIN,连接完全关闭

解析

  1. 三次握手
    • A 发 SYN (ISN=3651959169),B 回 SYN+ACK (ISN=1067742738, ack=3651959170),A 回 ACK (ack=1067742739)。
    • 双方进入 ESTABLISHED。
  2. 数据传输
    • B 发了 786 字节数据(seq=1:787),A 回 ACK=787。
    • 注意这里的 seq=1 是因为 tcpdump 默认把初始序号归一化显示为 0/1,方便阅读。
  3. 四次挥手
    • B 先发 FIN(seq=787, ack=1),进入 FIN-WAIT-1。
    • A 回 ACK=788,并同时发 FIN(seq=1, ack=788)。
    • B 回 ACK=2,确认 A 的 FIN,连接关闭。

A.th_ack 是 A 对 B 的数据流的累计确认。单向场景中,B 不发数据,因此 A.th_ack 固定不变。

B.th_seq同样保持,不随时间变化,除非 B 开始发送数据(此时才会随 B 的发送推进)。

例如,我们可以随便抓一个包:

整理

时间戳 源 → 目的 Flags Seq 区间 Ack Win Len
07:44:29.219344 api.snapcraft.io.https → ubuntu.47266 [P.] 28193:41153 8363 64240 12960
07:44:29.219376 ubuntu.47266 → api.snapcraft.io.https [.] 41153 55480 0
07:44:29.410105 api.snapcraft.io.https → ubuntu.47266 [P.] 41153:45473 8363 64240 4320
07:44:29.410169 ubuntu.47266 → api.snapcraft.io.https [.] 45473 61320 0
07:44:29.412027 api.snapcraft.io.https → ubuntu.47266 [P.] 45473:59873 8363 64240 14400
07:44:29.412047 ubuntu.47266 → api.snapcraft.io.https [.] 59873 55480 0
07:44:29.467687 api.snapcraft.io.https → ubuntu.47266 [P.] 59873:67073 8363 64240 7200
07:44:29.467729 ubuntu.47266 → api.snapcraft.io.https [.] 67073 59860 0

可以看出,其实发送方的ACK没什么变化,接收方的seq也同理,当然,因为不重要,所以抓包直接不显示接收方的seq。

总结

TCP数据报的连接机制其实依赖于序列号与确认号。在连接建立时,双方会交换初始序列号建立连接,在建立连接后,双方一个使用seq表示发送数据范围,一个使用ack进行确认,从而确保数据包被交付给对方。

在结束时,双方也要确保告知对方关闭,因此要进行四次挥手,确保关闭完成。

现在让我们再看看TCP的状态机吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
                            +---------+ ---------\      active OPEN
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
rcv RST (note 1) +---------+ CLOSE | \
-------------------->| LISTEN | ---------- | |
/ +---------+ delete TCB | |
/ rcv SYN | | SEND | |
/ ----------- | | ------- | V
+--------+ snd SYN,ACK / \ snd SYN +--------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd SYN,ACK | |
| |------------------ -------------------| |
+--------+ rcv ACK of SYN \ / rcv SYN,ACK +--------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<---------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +---------+
-------------------->|TIME-WAIT|------------------->| CLOSED |
+---------+ +---------+