TCP协议:数据包视角
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 | 主动开启者 被动开启者 |
接下来看看TCP首部,我们的重点是seq和ack,一个是发送号,另一个是确认号。
TCP结构体字段
首部:
1 | struct tcphdr { |
这里我们先不介绍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
- A 的
为了详细说明,还是先抓个包吧:
TCP通信示例
用tcp简单抓包:
简约:
1 | 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 |
由于不能很好的看出双方的ACK和SEQ变化,再抓一份详细的:
1 | 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 |
抓包内容表
| 阶段 | 报文方向 | 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,连接完全关闭 |
解析
- 三次握手
- A 发 SYN (ISN=3651959169),B 回 SYN+ACK (ISN=1067742738, ack=3651959170),A 回 ACK (ack=1067742739)。
- 双方进入 ESTABLISHED。
- 数据传输
- B 发了 786 字节数据(seq=1:787),A 回 ACK=787。
- 注意这里的 seq=1 是因为 tcpdump 默认把初始序号归一化显示为 0/1,方便阅读。
- 四次挥手
- 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 | +---------+ ---------\ active OPEN |