最近对 TCP 协议做了一次系统性的学习,种巨复杂的知识,只有系统性的总结归纳并且不断的实践才能够真正的掌握。后续会分为几篇文章来对 TCP 协议进行系统性的总结,帮助自己更好的理解 TCP 协议,也希望能够帮助到和我一样被 TCP 弄得头大的同学。
本篇 blog 主要对 TCP 协议的一些基本概念进行简单的介绍,包括 TCP 包格式、TCP 连接管理、TCP 状态机等。
TCP 协议简介
TCP 协议是面向连接的、可靠的、面向字节流的传输层协议,起源于 ARPANet 使用的 Network Control Protocol (NCP) 协议,为了改善该协议的一些问题工程师们又开发出了 Internet Transmission Control Program 程序来进行数据的传输。这是 TCP/IP 协议的前身,该程序的问题在于将网络层和传输层的功能合在了一起,其强制要求所有要使用网络层功能的应用必须使用传输层的功能,违反了分层、模块化设计的原则。
为了改善 TCTP 存在的问题,经过 3 个版本的演进,终于在第 4 个版本将网络层和传输层彻底分开,从而形成了 IPv4 协议和 TCP 协议。TCP 协议在 RFC 791 ( 2022 年 8 月发布了最新的 RFC9293 已经取代了 RFC791。)中进行了定义,不过在 RFC791 中并不包含所有的细节,因此后来有有了众多补充性的 RFC。
图片来自 TCP/IP Guide
TCP Segment 格式
首先我们先对 TCP 包进行一个简单的介绍,TCP 数据包也叫 Segment,因为 TCP 是面向字节流的传输,在 TCP 协议看来,应用发过来的数据就是一个个的字节,TCP 协议会将这些字节切分成一个个的 Segment 然后进行传输。
下图是 TCP 包的数据格式
图片来自 TCP/IP Guide
Segment 中各个字段含义:
字段 | 含义 | 简介 |
---|---|---|
Source Port | 源端口 | 发送端口 |
Destination Port | 目标端口 | 接收端口 |
Sequence Number | 序列号 | TCP 协议会为被传输的每个字节标注序列号,用于接收端进行确认和数据重组 |
Acknowledgment | 确认号 | |
Data Offset | 头部长度 | TCP 根据该字段来区分 Segment 包的头部和数据部分 |
Reserved | 保留位 | |
Control Bits | 控制位 | 标识信息的类型,比如 ACK 表示确认信息,SYN 表示建立连接信息等 |
Window Size | 窗口大小 | 表示发送端的接收窗口大小,用于流量控制 |
Checksum | 校验和 | 用于校验 TCP 头部和数据部分是否有错误 |
Urgent Pointer | 紧急指针 | 用于指示紧急数据的位置 |
Options | 可选项 | |
Padding | 填充字段 | 用于保证 TCP 头部长度为 32 位的整数倍 |
Data | 应用数据 |
- TCP 使用源端口与目标端口以及 IP 层的源 IP 和目标 IP 一起组成的四元组(sourceIP,sourcePort,destIP,destPort )来唯一标识一个连接,该四元组也被称为 Socket。
- Reserved 最开始为 6 位,但是后来只有 4 位了,有两位被用作了 Control Bits 标识 CWR 和 ECE 两个控制位。
对于 Control Bits,该字段有 6 bit,每个 bit 代表一种消息类型,当该 bit 设置为 1 时表示启用,6 种消息类型如下:
Bit | Name | Description |
---|---|---|
0 | FIN | 结束连接 |
1 | SYN | 发起一个连接 |
2 | RST | 重置连接 |
3 | PSH | 接收方应该尽快将数据交给应用 |
4 | ACK | 确认号有效 |
5 | URG | 紧急指针有效 |
CWR | 拥塞窗口减小 | |
ECE | 拥塞窗口减小 |
除了上述固定的字段外,TCP 还支持若干可选项用来实现特定的功能,可选项大致分为两类:
- 不携带数据的可选项:option 仅包含 kind 字段,占 1 字节
- 携带数据的可选项:格式固定为三部分:
- Kind:可选项类型
- Length: 可选项长度,单位字节
- Data: 可选项的字段值
以下是一些常用的可选项,如果对于其他可选项感兴趣可以参考 Transmission Control Protocol (TCP) Parameters,该文章列出了所有的 Options 及其 RFC。
Kind | Length | Meaning | Description |
---|---|---|---|
0 | - | End of Option List | 当 Option 列表结尾 TCP Header 结尾不重合时,会在 option 列表最后添加该 option 表示 Option 列表结束 |
1 | - | No-Operation | |
2 | 4 | Maximum Segment Size | 确定 Segment 最大携带的数据大小 |
3 | 3 | Window Scale | 窗口缩放因子,用于扩大窗口大小,用于高速网络 |
4 | 2 | SACK Permitted | 用于启用 SACK 选项 |
5 | variable | SACK | 用于启用 SACK 选项 |
14 | 3 | TCP Alternate Checksum Request (obsolete) | 用用于启用 TCP 备用校验和算法 |
15 | variable | TCP Alternate Checksum Data (obsolete) | 用于启用 TCP 备用校验和算法 |
以上是对 TCP 包的介绍,对于各个字段先大致了解即可,后面会对这些字段进行详细的介绍。
TCP 连接 & 状态机
了解了 TCP 包的格式后,就可以对 TCP 的各种工作机制进行分析了。首先来看看 TCP 的连接建立和终止过程。
我们知道 TCP 是面向连接的传输协议,在传输数据之前收发双方必须先建立连接。所谓的连接并不是通信双方之间真有一条线路将双方连起来,而是双方维护了一系列的状态数据。比如序列号、发送窗口、SACK/CheckSum 算法等选项。通过这些状态,收发双发来判断数据包是否被接收到,是否需要重传,是否需要进行拥塞控制。而建立连接的过程就是同步这些状态信息,为数据发送做准备的过程。
TCP 通过 FSM(finite state machine) 有限状态机来管理连接的状态,其状态转换图如下。
图片来自 《TCP/IP Guide》
TCP 连接建立和终止过程以及数据传输过程如图:
图片来自 CoolShell
下面我们详细分析下 TCP 连接的建立和终止过程。
TCP 建立连接
1. 连接建立流程
TCP 采用“三次握手”协议来建立连接,所谓三次握手就是收发双方需要进行三次通信。其通信过程如图:
图片来自《TCP/IP Guide》
建立过程如下:
- Server 启动进程,监听端口,进入 Listen 状态
- Client 发送 SYN 信息,表示请求建立连接,进入 SYN_SENT 状态
- Server 收到 SYN 信息,并将自己的 SYN 和 ACK 一同发送给 Client,进入 SYN_RECEIVED 状态
- Client 收到 SYN&ACK 信息,并向 Server 发送 ACK 信息,进入 Established 状态
- 服务端收到 ACK 信息,进入 Established 状态
- 连接建立成功
除了正常的三次握手,还会存在收发双方同时发起请求的情况,其连接建立过程如图:
图片来自 《TCP/IP Guide》
2. 为什么需要三次握手
正常情况下之所以需要三次握手,主要原因在于:
-
收发双方同步数据:同步序列号、MSS 等信息。尤其是序列号,收发双发都需要向对方发送自己的序列号并确认收到了对方的序列号。因此理论上需要如图所示的 4 次通信才能完成数据同步,但中间的两次通信可以合并为一次,因此只需要三次通信即可完成数据同步。
-
防止重复连接: 根据 RFC9293 中的内容,三次握手最重要的目的是避免历史旧连接重复建立,防止建立错误连接,为此 TCP 提供了一个 RST 的控制位,表示中止连接。
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
— RFC9293
下面是 RFC 9293 中给出的一个三次握手时历史连接导致重置连接的例子:
图片来自 RFC 9293
我们来分析下上图中的过程:
A 向 B 发送了 SYN 消息,与此同时,之前丢失的一条 SYN 消息也发送给了 B。B 收到历史消息后返回 ACK,值为 SEQ + 1,A 会判断 B 返回的 ACK 是否正确:
- 如果 B 返回的 ACK 不正确,则 A 知晓 B 响应的是历史连接,A 会直接发送 RST 消息来中止连接。
- 如果 B 返回的 ACK 正确,则 A 向 B 发送 ACK 消息,双方连接建立成功。
因此除了 A 的第一次 SYN 请求外,发送方 A 还需要根据 B 的 ACK 消息来确认 B 是否接收到了正确的 SYN 信息并返回处理结果是中止连接还是建立连接,该过程最少需要三次通信,这是 TCP 使用三次握手的最主要原因。
TCP 终止连接
TCP 连接的的终止过程是一个双向的过程,即双方都可以主动发起终止连接的请求,因此 TCP 的连接终止过程是一个四次挥手的过程。
图片来自 《TCP/IP Guide》
连接终止过程为:
- Client 发送 FIN 消息,表示不再发送数据,请求关闭连接,进入 FIN_WAIT_1 状态。此时 TCP 连接处于半关闭状态,即 Client 可以接收数据但是不能发送数据。
- Server 收到 FIN 消息后,发送 ACK 消息,表示收到了 Client 的关闭请求,进入 CLOSE_WAIT 状态。此时 TCP 连接处于半关闭状态,即 Server 可以发送数据但是不能接收数据。
- Client 收到 ACK 消息后,进入 FIN_WAIT_2 状态。
- Server 端应用执行完毕,发送 FIN 消息,请求关闭连接,进入 LAST_ACK 状态。
- Client 收到 FIN 消息后,发送 ACK 消息,表示收到了 Server 的关闭请求,进入 TIME_WAIT 状态。此时 TCP 连接处于半关闭状态,即 Client 可以接收数据但是不能发送数据。
- Server 收到 ACK 消息后,进入 CLOSED 状态,此时 TCP 连接关闭。
如果是双方同时断开连接,过程如图:
了解了 TCP 建立连接的建立和终止过程,我们再来看下一些细节问题。
其他相关处理
1.选择初始序列号
TCP 通过序列号来标识数据包,序列号的初始值是随机的,三次握手的过程中,双方会交换自己的初始序列号(ISN,Initial Sequence Number),以便后续数据包的发送。
RFC9293 规定序列号是一个 32 位的定时计数器,每隔 4ms 会对计数器加 1,因此 ISN 的增长速度为 1/250s,即每秒 250 个。ISN 的范围为 0~2^32-1,即 0~4294967295,超过这个范围后会重新从 0 开始计数,大约 4.55 小时会重复一次。
之所以采用定时计数器的原因是避免来自不同连接的包发生混乱。如果每次建立连接时都从 1 开始计数,假设一个连接在断开后,又建立了一个新的连接,那么新连接的序列号可能会与之前的连接的序列号重复,这样就会导致数据包的混乱。如果是采用定时计数器,对于不同连接,每 4.55 小时才会重复一次,远远超过了段存活时间(Maximum Segment Lifetime)因此不会出现不同连接出现重复 ISN 的情况。。
但是基于定时计数器的 ISN 有两个问题:
- 序列号可预测:由于基于定时计数器的序列号是每 4ms 加 1,攻击者可以通过分析 ISN 来伪造数据包,从而进行攻击。
- 可快速耗尽:序列号范围是 0~4294967295,并且 1 个字节占用 1 个序列号,在高速网络下序列号会快速耗尽。在 1Gb/s 网速下序列号的耗尽时间为 34s ,而在 10Gb/s 网速下序列号的耗尽时间为 3s,100Gb 下只有 1/3s。
对于序列号可预测的问题,可以通过随机函数来初始化序列号。RFC6528 给出了如下随机函数:
ISN = M + F(localip, localport, remoteip, remoteport, secretkey)
其中 M 是一个计数器,F 是一个随机函数,localip、localport、remoteip、remoteport 是本地和远程的 IP 和端口,secretkey 是一个密钥,该密钥只有本地和远程双方知道,通过上述随机函数计算出的 ISN 是不可预测的。
对于序列号耗尽的问题,RFC7323 提出了TimeStamp Options 来解决。
2.确定 MSS
建立连接时,在 SYN 消息中还会在 Options 的携带 MSS(Max Segment Size) 信息,表示 TCP 连接双方能够接收的最大数据包大小(仅表示数据大小,不包含 header)。
设置该字段的主要目的是防止 Segment 过大导致 IP 包分片,从而影响传输性能,同时为了提高 TCP 的传输效率,一般是在不超过 MTU 的情况下越大越好。
MSS 只能在建立连接时通过 SYN 消息传递,因为 TCP 的数据包是有大小限制的,如果在连接建立后,双方想要改变 MSS,那么只能重新建立连接。
如果 SYN 消息中没有 MSS 信息,则一般采用默认值 536 byte(默认 MTU 576 - IP Header 20 - TCP Header 20)。