目录
[一、 TCP 协议是什么](#一、 TCP 协议是什么)
[二、 TCP 报文格式](#二、 TCP 报文格式)
[16 位源端口号](#16 位源端口号)
[4 位首部长度和 16 位目的端口号](#4 位首部长度和 16 位目的端口号)
[32 位序号和 32 位确认序号](#32 位序号和 32 位确认序号)
[16 位窗口大小](#16 位窗口大小)
[ACK 标志位](#ACK 标志位)
[SYN 标志位](#SYN 标志位)
[FIN 标志位](#FIN 标志位)
[RST 标志位](#RST 标志位)
[PSH 标志位](#PSH 标志位)
[URG 标志位](#URG 标志位)
[16 位紧急指针](#16 位紧急指针)
[常见 TCP 选项](#常见 TCP 选项)
[1. MSS(Maximum Segment Size)](#1. MSS(Maximum Segment Size))
[2. 窗口扩大选项(Window Scale)](#2. 窗口扩大选项(Window Scale))
[3. 时间戳(Timestamp)](#3. 时间戳(Timestamp))
[4. SACK(Selective ACK)](#4. SACK(Selective ACK))
一、 TCP 协议是什么
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接、具有可靠性、面向字节流的传输层 通信协议,主要负责网络中两个应用程序之间传输数据。
TCP 协议位于 OSI 七层协议模型的传输层,它主要负责:进程之间的数据传输,数据可靠到达,数据到达顺序正确,它不负责定位和寻找主机(网络层 IP 协议负责)。
TCP 协议的特点
(1)面向连接
TCP 在通信之前,需要先建立连接。
cpp
客户端 服务器
SYN ------->
<------- SYN+ACK
ACK ------->
建立连接
连接建立后,双方维护通信状态,从而进行数据通信。
其中该过程被称为 TCP 三次握手,下一章节详细讲。
(2)可靠性
TCP 协议保证数据传输的可靠性,即无论底层 IP 网络如何丢包、乱序,TCP 都尽可能保证应用程序收到是数据完整、有序。
实现依靠:
序号、确认应答机制、超时重传机制、滑动窗口、拥塞控制
(3)面向字节流
TCP 不关心应用层一次发送多少数据,应用层将数据拷贝到发送端的发送缓冲区中,数据什么时候发、数据一次发多少、数据丢失怎么办,全是操作系统基于 TCP 协议来做出确定。当对端收到数据时,对端的接收缓冲区存放的全是有效数据,对端的操作系统基于 TCP 协议只负责维护接收缓冲区中连续的字节流,因此 TCP 没有消息边界,需要应用层协议自主决定。这就是 TCP 粘包而需要拆包的根本原因。
(4)全双工通信

TCP 连接建立后:双方可以同时发送数据
cpp
客户端 ========> 服务器
客户端 <======== 服务器
两个方向独立的本质是 TCP 套接字具有发送缓冲区和接收缓冲区。
这也是为什么:TCP 关闭连接需要四次挥手。
二、 TCP 报文格式

16 位源端口号
用于表明发送端主机网络进程绑定的端口号,当对端处理完请求,给予响应时,根据 16 位源端口号将数据交付给发送请求的网络进程。
4 位首部长度和 16 位目的端口号
在网络协议栈中,我们知道对于应用层数据向下交互到网卡时,每一层协议都会对上层协议进行封装,对于网卡中的数据向上交互到应用层时,每一层协议也必须要做到解包和分用。
对于 TCP 协议是如何做到解包和分用的呢?
分用:操作系统如何将数据交给本地主机的哪一个进程?
根据 16 位目的端口号,操作系统就能找到绑定对应端口号的目标进程,将应用层数据交付给指定进程,完成分用的过程。
解包:操作系统如何将 TCP 报文完成报头和有效载荷的分离?在 TCP 报文格式中,存在着一个字段:4 位首部长度,它表明了 TCP 报头的长度,操作系统可以根据它的大小来做到报头和有效载荷的分离。
4 位首部长度,它的取值范围是 0, 15 ,而 TCP 报头的长度抛去选项字段,最低大小也得为 20 字节。那这就不对了啊!其实不然,因为 TCP 是一种协议,协议也是一种约定,而 TCP 协议明确表明该 4 位首部长度的大小 * 4 表明 TCP 报头的长度,那么它的取值范围就是 0, 60 就可以正确的表明 TCP 报头的大小,完成报头和有效载荷的分离。
32 位序号和 32 位确认序号
前置知识:理解可靠性的本质 ---- 确认应答机制

当客户端发送的数据被服务器成功接收后,服务器会通过应答报文告知客户端已经收到哪些数据。

注:不对应答做出应答,如果对应答做出应答就会出现无限套娃的情况。那如果应答丢失了,怎么办?TCP 保证可靠性?
TCP 还有一种机制叫做:超时重传机制,它可以解决应答丢失应该如何去做?---- 下一章节讲
准确理解可靠性:
1. 客户端收到应答,可以保证服务器一定收到了客户端向服务器发送的TCP报文
2. 对于客户端没有收到应答的TCP报文,无法保证该TCP报文被服务器收到
3. TCP 并不保证每一个应答报文一定能够成功到达对端,但是 TCP 通过序号和确认序号记录数据传输状态,即使应答报文丢失,也可以通过重传机制保证数据最终可靠到达
对于服务器向客户端发送TCP报文,亦是如此。
上述传输TCP报文的过程,并不是真实的过程(客户端发送一个数据,服务器应答后,客户端才能发送下一个数据)。
TCP报文传递的真实过程

客户端和服务器之间的 TCP 通信并不是简单的"一问一答"的串行过程,而是全双工通信,双方可以同时发送数据。由于 TCP 基于 IP 协议,而 IP 协议无法保证数据传输顺序,因此 TCP 报文到达对端主机由于网速等原因不一定先发送的先到达,可能出现乱序问题。数据乱序问题也是不可靠性的一种体现,需要得到解决。而对于客户端,它发送了如此多的TCP报文,假如某些数据没有得到确认,它如何知道哪些数据需要重新发送?对于服务器,它接受了如此多的TCP报文,它是如何做到让客户端知道哪一个TCP报文接受的呢? 在 TCP 报头格式中,存在着两个特别重要的字段:序号和确认序号,它们可以对 TCP 传输的数据进行编号,并记录双方已经接收的数据状态,它不仅能解决报文乱序的问题,还能解决部分应答报文丢失不需要重传的问题。
在下一章节中,我们将在滑动窗口中,重点介绍序号和确认序号。但我们需要先了解一下:
确认序号表示:接收方已经收到指定序号之前的所以数据,下一次希望接收的数据序号。
举例:
序号 = 100
数据长度 = 500
那么:确认序号 = 600
理解为什么要有两个序号?一个序号不可以吗?
TCP 是一种全双工通信协议,客户端和服务器之间可以同时进行数据发送。
例如:
客户端向服务器发送数据:
客户端:
序号 = 100
数据 = hello
与此同时,服务器也可能向客户端发送数据:
服务器:
序号 = 500
数据 = world
对于双方来说,都需要记录自己发送的数据编号,同时也需要知道对方发送的数据是否已经被成功接收。
因此,TCP 首部设计了两个字段:
- 序号:表示自己发送的数据编号。
- 确认序号:表示已经成功接收到对方的数据编号。
通过这两个字段,TCP 可以同时维护两个方向的数据传输状态。
可能有人会思考:如果 TCP 只有一个序号字段,能不能通过这个序号同时完成数据发送和确认?
答案是不可以。
因为 TCP 通信存在两个独立的数据方向:
客户端 ---> 服务器
服务器 ---> 客户端
一个序号无法同时记录:
- 当前发送方发送的数据编号
- 当前发送方已经收到的数据编号
所以需要存在序号和确认序号来完成全双工的特性。
对于一些聪明的人,可能会想:如果只存在一个序号,当客户端向服务器发送TCP报文时,服务器不能知道自己收到的是应答报文还是数据报文。所以需要序号和确认序号来表明报文的类型,这样做确实可以,但没有必要,因为TCP报文是存在许多类型的,TCP报头中存在专门的标志位来表示报文的类型。
所以序号和确认序号的作用并不是区分 TCP 报文类型。
而两个序号存在的根本原因是:
TCP 需要同时维护双方的数据传输状态,从而实现可靠的全双工通信。
序号和确认序号附加优势 ---- 捎带确认机制例如:
客户端发送:
你今天晚上吃饭了吗?
服务器回复:
我今天晚上吃了,你呢?
服务器在回答客户端问题的同时,也发送了自己的数据。
对应到 TCP 中:
服务器发送的 TCP 报文中同时包含:
- 序号:服务器发送的数据编号
- 确认序号:对客户端数据的确认编号
也就是说,一个 TCP 报文可以同时完成:
- 发送自己的数据
- 确认收到的数据
这样可以减少额外 ACK 报文的发送,提高网络通信效率。
该机制被称为捎带确认机制,对于同时包含有效序号和有效确认序号的 TCP 报文被称为捎带应答
16 位窗口大小
对于应用层调用 read/recv 系统调用获取网络数据,并不是直接从网卡中获取,而是操作系统接收到网络数据后,通过协议栈进行解包和分用,得到 TCP 报文,并将 TCP 报文中的有效载荷拷贝到内核级 TCP 接收缓冲区中。应用层调用 read/recv 时,实际获取的是接收缓冲区中的数据。
但是,计算机内存资源是有限的,因此 TCP 接收缓冲区也存在大小限制。
假设客户端向服务器请求资源,服务器不断向客户端发送数据。如果服务器发送速度为 100MB/s,而客户端应用程序处理速度只有 10MB/s,那么大量数据会不断堆积在客户端 TCP 接收缓冲区中。
当接收缓冲区空间不足时,如果发送方仍然继续发送数据,接收方对其进行丢弃,就会导致大量无效的数据传输和资源浪费。虽然 TCP 可以通过重传机制保证数据最终可靠到达,但是这种方式会严重降低网络通信效率。
因此,TCP 协议引入了流量控制机制,通过 TCP 报头中的 16 位窗口大小字段解决上述问题。
16 位窗口大小:表示接收方当前允许发送方发送的数据量,即 TCP 接收缓冲区中剩余空间的大小。在 TCP 通信过程中,接收方会通过 TCP 报文中的窗口大小字段,将自己当前接收缓冲区剩余可用空间告知发送方。
由于 TCP 是面向连接的协议,在建立连接时需要进行三次握手,双方会交换初始窗口大小信息。
在数据传输过程中,接收方也会持续通过应答报文携带窗口大小,动态调整发送方的数据发送速度。
该机制称为:流量控制机制。
(关于窗口大小、滑动窗口以及发送窗口和接收窗口之间的关系,将在后续章节详细介绍。)
标志位
TCP 首部中存在多个标志位字段,用于表示当前 TCP 报文的类型以及 TCP 当前所处的状态。
标志位的本质就是比特位,当比特位为 1 时,表明它是有效的,否则是无效的。
前面我们介绍了:
- 序号:表示发送数据的位置
- 确认序号:表示已经接收的数据
- 窗口大小:表示接收方当前能够接收的数据量
但是 TCP 通信过程中还存在一些特殊情况:
例如:
- 如何表示我要建立连接?
- 如何表示我要关闭连接?
- 如何表示这个报文是确认报文?
- 如何表示连接出现异常?
这些信息无法通过序号、确认序号等字段表示,因此 TCP 在报头中设计了多个标志位,用于描述当前 TCP 报文的作用。
常见标志位如下:
| 标志位 | 含义 |
|---|---|
| SYN | 建立连接 |
| ACK | 确认应答 |
| FIN | 关闭连接 |
| RST | 重置连接 |
| PSH | 提醒接收方尽快交付数据 |
| URG | 紧急数据标志 |
其中:
SYN、ACK、FIN 是 TCP 中最重要的三个标志位。
ACK 标志位
ACK(Acknowledgment)表示:
当前 TCP 报文中的确认序号字段是否有效。
例如:
ACK = 1
ack = 1000
表示:
确认序号有效,已经收到对方 1000 之前的数据。
TCP 通信过程中,大部分数据报文都会携带 ACK。
例如:
客户端:
序号 = 100
data = hello
服务器:
ACK = 1
ack = 105
表示:
hello 已经收到,希望下一次从105开始发送。
SYN 标志位
SYN(Synchronize)表示:
请求建立 TCP 连接。
TCP 建立连接时:
客户端发送:
SYN = 1
表示:
我要和你建立连接。
服务器收到后回复:
SYN = 1
ACK = 1
表示:
我同意建立连接,并确认收到你的请求。
(三次握手会详细介绍)
FIN 标志位
FIN(Finish)表示:
请求关闭 TCP 连接。
例如:
客户端发送:
FIN = 1
表示:
我的数据发送完毕,希望关闭连接。
服务器收到后:
ACK = 1
确认收到关闭请求。
之后双方分别关闭自己的发送方向。
(四次挥手详细介绍)
RST 标志位
RST(Reset)表示:
重置 TCP 连接。
当 TCP 连接出现异常情况,例如:
- 连接不存在
- 端口没有监听
- 收到无法处理的数据
TCP 可以发送 RST 报文,直接终止连接。
例如:
客户端连接一个不存在的端口:
connect()
服务器返回:
RST = 1
表示:
该连接不存在。
PSH 标志位
PSH(Push)表示:
希望接收方立即将数据交付给应用层。
正常情况下:
TCP 数据到达后,会先存放在接收缓冲区。
当 PSH 标志被设置:
TCP 会提示接收方:
这部分数据应该尽快交给应用程序。
不过现代操作系统中,PSH 的实际影响并不像早期网络中那么明显。
URG 标志位
URG(Urgent)表示:
当前 TCP 报文中包含紧急数据。
当 URG=1 时:
紧急指针字段有效。
用于告诉接收方:
有部分数据需要优先处理。
不过现在实际应用非常少。
16 位紧急指针
TCP 首部中存在一个 16 位紧急指针字段 ,它用于表示 TCP 报文中紧急数据的位置。
但是需要注意:
紧急指针字段只有在 TCP 首部中的 URG 标志位被设置为 1 时才有效。
为什么需要紧急指针?
正常情况下,TCP 是按照字节流顺序传输数据的:
例如:
A B C D E F G
接收方会按照:
A → B → C → D → E → F → G
顺序读取数据。
但是在某些特殊情况下,发送方希望告诉接收方:
某一部分数据需要优先处理。
例如:
用户正在通过远程连接操作服务器:
普通输入:
ls
cd /home
紧急输入:
Ctrl+C
此时 Ctrl+C 需要被优先处理,不能等待前面的普通数据全部处理完成。
因此 TCP 提供了紧急数据机制。
紧急指针如何工作?假设:
发送的数据:
A B C D E F
其中:
D
是紧急数据。
TCP 报文:
URG = 1
Urgent Pointer = 3
表示:
从当前序号开始偏移 3 个字节的位置存在紧急数据。
接收方根据紧急指针找到紧急数据位置,并优先处理。
16 位紧急指针:表示紧急数据在 TCP 字节流中的偏移位置,只有当 URG 标志位被设置为 1 时才有效。该字段占 16 位,可以表示较大的偏移范围,用于帮助接收方快速定位紧急数据。
在 Linux 等现代操作系统中,TCP 紧急数据通常按照 1 字节的带外数据(OOB Data)进行处理,但 TCP 协议本身没有规定紧急数据的大小。应用层可以利用这 1 字节数据自行定义不同的含义,例如通过约定不同数值表示不同的紧急事件,从而实现简单的优先级通知机制。
为什么现在很少使用?虽然 TCP 协议设计了紧急数据机制,但是在现代网络编程中使用非常少。
原因:
- 应用层通常自己设计优先级机制。
例如:
- 消息类型字段
- 优先级字段
- 控制消息
- TCP 紧急数据机制不同操作系统实现存在差异。
因此实际开发中:
更多使用:
应用层协议 + 消息优先级设计
来实现类似功能。
16位校验和
它的主要作用是用于检测 TCP 报文在传输过程中是否发生了数据损坏
补充:TCP 校验和相关理解
发送端会根据 TCP 首部、数据以及相关的 IP 信息计算出一个16位检验和值,接收端收到报文后,会按照同样的规则重新计算校验和,然后进行比较。如果计算结果不一致,说明报文在传输过程中很可能发送了数据损坏,接收端就将该 TCP 报文直接丢弃。
选项字段
TCP 首部中存在一个可变长度的选项字段,用于扩展 TCP 的功能。
TCP 首部最小长度为:
20 字节
但是 TCP 在设计时考虑到未来可能需要增加新的功能,因此预留了选项字段。
由于选项字段长度不固定,因此 TCP 首部中设计了:
数据偏移(首部长度)字段,用于表示 TCP 首部实际长度。
例如:
没有选项:
TCP首部 = 20字节
存在选项:
TCP首部 > 20字节
接收方通过数据偏移字段知道 TCP 数据从哪里开始。
常见 TCP 选项
1. MSS(Maximum Segment Size)
最大报文段长度。
表示:TCP 通信双方能够接收的最大 TCP 数据长度。
例如:
客户端发送 SYN:
MSS = 1460
表示:我希望你发送给我的 TCP 数据部分最大为 1460 字节。
MSS 可以避免 TCP 分段过大。
2. 窗口扩大选项(Window Scale)
前面讲窗口大小时提到:
TCP 首部窗口字段只有 16 位:
最大65535字节
但是现代网络带宽很高,65535 字节可能不够。
因此 TCP 通过窗口扩大选项扩展窗口大小。
3. 时间戳(Timestamp)
用于:
- 计算 RTT(往返时间)
- 防止旧数据包影响新连接
4. SACK(Selective ACK)
选择确认。
默认 TCP ACK 是累计确认:
例如:
收到:
1 2 3 5
缺少:
4
只能告诉发送方:
ack=4
SACK 可以告诉发送方:
1~3 收到
5 收到
4缺失
减少不必要的重传。
TCP 选项字段用于扩展 TCP 功能,使 TCP 协议能够适应不同网络环境和需求。由于选项字段长度不固定,因此 TCP 通过数据偏移字段确定 TCP 首部长度。常见选项包括 MSS、窗口扩大、时间戳以及 SACK 等。