鲁迅曾今说过,TCP 是一个可靠的(reliable)、面向连接的(connection-oriented)、基于字节流(byte-stream)、全双工的(full-duplex)协议。

下文中使用 wireshark 抓包来观察 TCP 数据包

1 TCP 数据在 IP 数据报中的封装

TCP 数据在 IP 数据报中的封装

2 TCP 首部的数据格式

TCP 首部的数据格式

TCP 首部的数据结构,如果不计选项字段,首部是 20 个字节。

也可以参考下面的图片,0、1、2、3 分别为第一个字节、第二个字节、第三个字节、第四个字节

TCP 首部的数据格式

2.1 源端口号和目标端口号

源端口号和目标端口号

以下是在 wireshark 抓包查看这两个数值,源 IP、源端口、目标 IP、目标端口构成 TCP 连接的四元祖,一个四元祖可以唯一标识一个连接。

源端口号和目标端口号

2.2 序列号 32 位(Sequence number)

序列号是 32 位无符号数,序号范围从 0 到 \(2^{32} - 1\),当需要达到 \(2^{32} - 1\) 之后会从 0 开始。

序列号指的是本报文段第一个字节的序列号,在 SYN 报文中,序列号用于交换初始的序列号,在其他报文中,序列号用于保证包的顺序。

因为网络层(IP 层)不保证包的顺序,TCP 协议利用序列号来解决网络包的乱序、重复的问题,以保证数据包以正确的顺序组装传递给上层应用。

如果发送方发送的是四个报文顺序分别是 1、2、3、4,但到达接收方的顺序是 2、4、3、1,接收方就可以通过序列号的大小顺序组装出原始的数据。

2.2.1 初始序列号(Initial Sequence Number, ISN)

在建立连接之初,通信双方都会各自选择一个序列号,称之为初始序列号。在建立连接时,通信双方通过 SYN 报文交换彼此的 ISN,如下图所示

此时 SYN 字段置为 1,如下图抓包所示

2.2.2 确认号(Acknowledgment number, ACK)

TCP 使用确认号(Acknowledgment number, ACK)来告知对方下一个期望接收的序列号,小于此确认号的所有字节都已经收到。

关于确认号有几个注意点:

  1. 不是所有的包都需要确认的
  2. 不是收到了数据包就立马需要确认的,可以延迟一会再确认
  3. ACK 包本身不需要被确认,否则就会无穷无尽死循环了
  4. 确认号永远是表示小于此确认号的字节都已经收到

2.2.3 三次握手流程

以下便是一个三次握手的流程

其中第 2 步和第 3 步可以合并一起,这就是三次握手的过程

下面三张图是一个 HTTPS 建立 TCP 连接的三次握手的过程:

首先从本地的 61559 端口向服务器的 443 端口发送 SYN 数据包,序列号为 0。

服务器接收到 SYN 数据包后返回了一个 SYN+ACK 的数据包,ACK number 为 0 + 1 即 1。同时发送服务端的序列号从 0 开始

客户端接收到 SYN+ACK 数据包后再对服务端的 SYN 做一次确认,ACK number 为 0 + 1 即 1,完成三次握手

2.3 首部长度 4 位

首部长度 4 位,单位为 4 字节,首部的最大长度为 \(15 * 4 = 60\) 字节,其中首部的固定长度为 20 字节,选项的最大长度为 40 字节。

2.4 保留位 6 位

暂时没有作用,全部置为 0。关于这里的保留位为什么不放到选项中,我的理解是如果放到选项中,那么只要别人使用了选项位,那么就没法平滑升级到更新过后的 TCP 协议,因为选项位在不同的场景下可能有不同的含义。

2.5 六个标志位

我们通常所说的 SYN、ACK、FIN、RST 其实只是把 flags 对应的 bit 位置为 1 而已,这些标记可以组合使用,比如 SYN+ACK,FIN+ACK 等

2.5.1 URG

当 URG = 1 时,表示有紧急指针有效。

2.5.2 ACK

当 ACK = 1 时,确认序号字段才有效。当 ACK = 0 时,确认序号字段无效。TCP 规定,在连接建立后所有传输的报文段都必须把 ACK 置 1。

2.5.3 PSH

当 PSH = 1 时,接收方应该尽快将这个报文段交给应用层,而不会等整个缓存填满后再交付。

2.5.4 RST

当 RST = 1 时,这个标记用来强制断开连接,通常是之前建立的连接已经不在了、包不合法、或者实在无法处理

2.5.5 SYN

在建立连接时使用,当 SYN = 1,ACK = 0 时,表示这是一个请求连接的报文段。若对方统一建立连接,则在响应报文段中使得 SYN = 1,ACK = 1。

2.5.6 FIN

用来释放一个连接,当 FIN = 1,表示此报文段发送方的数据发送完毕,并要求释放连接。

2.6 窗口大小 16 位

窗口大小

// todo 窗口大小指的是什么?

窗口大小只有 16 位,可能 TCP 协议设计者们认为 16 位的窗口大小已经够用了,也就是 65536 字节(64KB),如果出现不够用的情况,TCP 协议引入了窗口缩放选项作为窗口缩放的比例因子,比例因子的范围是 [0, 14],其中最小值 0 表示不缩放,最大值 14.比例因子可以讲窗口扩大到原来的 2 的 n 次方,比如窗口缩放前位 1050,缩放因子位 7,则真正的窗口大小位 1050 * 128 = 134400

如下图抓包所示

窗口大小

值得注意的是,窗口缩放值在三次握手的时候指定,如果抓包的时候没有抓到 SYN 包,wireshark 是不知道真正的窗口缩放值是多少的。

2.7 校验和 16 位

校验和

校验和的计算包含首部和数据这两个部分,计算校验和的时候要在 TCP 报文段的时候要在前面加上 12 字节的伪首部。 // todo

1
2
1. 加的这 12 个字节的伪首部是什么东西?
2. 这个伪首部和 UDP 的位首部是什么关系?

2.8 紧急指针 16 位

当 URG = 1 时,紧急指针才有效,指出本报文段中的紧急数据的字节数。当窗口大小为 0 时,依然可以发送紧急数据。

2.9 选项

可选项的格式如下图所示

这里要注意的是 Length = Kind 的一个字节 + Length 的一个字节 + value 的字节数

下图展示的便是 TCP 的选项字段 MSS,总共四个字节,类型占一个字节,长度占一个字节,value 占两个字节

常用的选项有一下几个:

  1. MSS:最大段大小选项,是 TCP 允许的从对方接收的最大报文段
  2. SACK:选择确认选项
  3. Window Scale:窗口缩放选项

3 TCP 状态转换图

TCP 状态转换图

4 思考题

4.1 题一

如果一个 TCP 连接正在传送 5000 字节的数据,第一个字节的序号是 10001,数据被分为 5 段,每个段携带 1000 字节,请问每个段的序号是什么?

答案:10001、11001、12001、13001、14001

4.2 题二

A B 两个主机之间建立了一个 TCP 连接,A 主机发给 B 主机两个 TCP 报文,大小分别是 500 和 300,第一个报文的序列号是 200,那么 B 主机接收两个报文后,返回的确认号是()

A、200 B、700 C、800 D、1000

答案:D

第一个包是:[200, 699] len=500 ACK=700 第二个包是:[700, 999] len=300 ACK=1000

4.3 题三

客户端的使用 ISN=2000 打开一个连接,服务器端使用 ISN=3000 打开一个连接,经过 3 次握手建立连接。连接建立起来以后,假定客户端向服务器发送一段数据 Welcome the server!(长度 20 Bytes),而服务器的回答数据 Thank you!(长度 10 Bytes ),试画出三次握手和数据传输阶段报文段序列号、确认号的情况。

C: 客户端 S: 服务器
首先进行三次握手:
C->S Seq=2000
S->C Seq=3000, ACK=2001
C->S Seq=2001, ACK=3001
下面开始传输数据:
C->S Seq=2001, LEN=20
S->C Seq=2001, ACK=2021
S->C Seq=3001, LEN=10
C->S Seq=3001, ACK=3011

参考资料

  1. TCP-IP 详解:第17 章TCP :传输控制协议
  2. 详解 TCP 和 UDP 数据段的首部格式
  3. 深入理解 TCP 协议:从原理到实战
    • 很棒的一本小册