目录
一、TCP协议概述
1.1 TCP 协议简介
TCP(Transmission Control Protocol,传输控制协议) 是一种面向连接、可靠传输、基于字节流的传输层协议。它运行在 IP 协议之上,构成了现代网络通信的核心组成—TCP/IP 协议栈。
CP 主要用于对数据完整性、顺序和传输可靠性要求较高的应用场景,如:
Web 应用:HTTP / HTTPS
邮件服务:SMTP、POP3、IMAP
远程登录:SSH、Telnet
文件传输:FTP(命令通道)
特点包括:
可靠传输: 有确认应答、超时重传、数据校验等机制。
面向连接: 通过“三次握手”建立连接,“四次挥手”断开连接。
顺序保证: 接收方按发送顺序组装数据(通过序列号)。
全双工通信: 双方可同时发送数据。
流量控制和拥塞控制: 动态调节发送速率,防止丢包与拥塞。
1.2 传输控制协议
1.2.1 控制逻辑源自内核协议栈
当我们在程序中通过
socket()
创建一个 TCP 套接字时,操作系统会为其分配一个内部的数据结构,称为 Socket File Control Block(socket 文件控制块)。该控制块中维护了多个关键信息,其中就包括:
发送缓冲区
接收缓冲区
连接状态、序列号、窗口、重传定时器等控制字段
用户层的数据发送和接收,其实只是完成了数据拷贝的动作,真正的数据传输过程完全由 TCP 协议在内核中自动控制。
✅ 数据的“发送”其实只是拷贝
调用
send()
/write()
时,数据被从 应用层用户空间 拷贝到 内核 TCP 发送缓冲区;TCP 是否立即发送该数据,由协议栈根据网络状态自行决定(例如可能启用 Nagle 算法合并发送)。
✅ 数据的“接收”也是拷贝
调用
recv()
/read()
,是从 内核的 TCP 接收缓冲区 拷贝数据到 应用层用户空间 ;实际的数据接收早已在内核中完成,用户只是“取用”。
1.2.2 为什么说 TCP 在“控制传输”?
因为用户只负责把数据交给 TCP,而不是直接操作网络。以下这些核心问题都由 TCP 控制协议自动完成:
何时发送数据?(可能立即,也可能合并多个包延迟发送)
发送多少字节?(受窗口大小、拥塞控制等限制)
如果丢包怎么办?(是否重传,何时重传,采用何种策略)
数据是否按顺序交付?
是否需要通知对端 ACK?是否需要快速重传?
所有这些传输细节都不是应用层代码能控制的,而是由 TCP 协议栈(即传输控制逻辑)自动完成的。这正是 “Transmission Control Protocol” 名称的核心含义——TCP 不只是传输协议,更是一个智能调度数据传输的“控制器”。
1.3 TCP 的字节流传输模型
1.3.1 什么叫“面向字节流”?
✅ 定义说明
所谓面向字节流(byte-stream-oriented),指的是:TCP 将应用层传来的数据视为一个连续、无结构的字节序列进行可靠传输,不保留每次 send()/write() 的边界信息。
与之相对的是 UDP是面向报文(message-oriented)的协议,天然保留每次
sendto()
发送的“消息边界”。
✅ 面向字节流的具体表现
1)发送端:
多次
send()
/write()
的数据可能被合并成一个或多个 TCP 报文段;- 合并行为受 TCP 堆栈算法(如 Nagle 算法)和拥塞控制机制影响。
TCP 并不会在每次
send()
/write()
调用之间插入边界标识。2)接收端:
每次
recv()
或read()
接收到的数据长度和边界无法预测;一次
recv()
可能读取不到一个完整的消息,也可能包含多个消息。
1.3.2 为什么 TCP 设计为字节流?
TCP 被设计为面向字节流的传输协议,是为了适应复杂和动态的网络环境,实现高效、可靠的传输。
其本质是:仅负责传输一个无边界的、有序的字节流,如何切分消息由应用层决定。
✅ 技术动机
1)动态调整数据发送粒度
TCP 根据 发送窗口 和 拥塞窗口 实时调整发送速率;
发送数据的分段行为是基于网络状态控制的,而非应用层调用的边界。
2)合并应用层数据(如 Nagle 算法)
TCP 可能将多个小块
send()
的数据缓冲合并,减少报文数量,提高带宽利用率。这意味着多次
send()
调用可能合并进一个 TCP 报文中发送。3)底层网络层支持分片与重组
IP 层支持对数据包进行分片;
TCP 在传输层也能将大段应用数据拆成多个报文段发送,再在接收端重新组装。
4)接收端读取粒度灵活
recv()
/read()
的读取长度是由接收应用决定的;与发送端
send()
的边界没有任何必然关系;
1.3.3 两次 send() 会发生什么?
假设发送端代码如下:
send(sockfd, "Hi.", 3, 0); send(sockfd, "I am XiaoHua", 13, 0);
应用层的两条消息为:
[Hi.] 和 [I am XiaoHua]
实际网络中的 TCP 报文可能出现以下几种情况:
✅ 情况一:两条消息被合并到一个 TCP 报文中(粘包)
[TCP segment 1] → "Hi.I am Xiaolin"
✅ 情况二:第一条完整,第二条部分被发送(拆包)
[TCP segment 1] → "Hi.I am " [TCP segment 2] → "Xiaolin"
✅ 情况三:第一条也被拆了,第二条拼接进来(粘 + 拆)
[TCP segment 1] → "Hi" [TCP segment 2] → ".I am Xiaolin"
✅ 情况四:极端情况,多个 TCP 报文包含碎片数据
[TCP segment 1] → "H" [TCP segment 2] → "i." [TCP segment 3] → "I am X" [TCP segment 4] → "iaolin"
1.3.4 粘包与拆包问题的根本原因
由于 TCP 是面向字节流的协议,数据在传输过程中没有天然边界,再加上传输受下列因素影响:
网络拥塞、IP 分片;
接收缓冲区限制;
Nagle 算法合并;
系统调度延迟等。
因此,TCP 不能保证每次发送和接收是一一对应的消息单位,从而引发两个典型问题:
✅ 粘包
多个应用层消息在发送端被合并为一个 TCP 报文段;
接收端调用一次
recv()
就读到了多个消息的拼接内容;导致消息边界丢失。
✅ 拆包
一个完整的应用层消息在网络层或 TCP 层被拆分为多个报文段;
接收端需要多次调用
recv()
才能拼出完整消息。
1.3.5 如何解决粘包/拆包?
因为 TCP 不提供边界,消息划分必须由应用层协议自行定义。
常见策略包括:
方法 描述 固定长度消息 每条消息固定 N 字节 特殊分隔符 用特殊字符标识消息结尾(如 \n
,\0
,\r\n
)前置长度字段 在消息前添加长度字段(如 4 字节) TLV 格式协议 使用 Type-Length-Value 格式描述数据结构
二、 TCP 报文段结构解析
TCP 报文段由固定报头(20字节)+ 可选字段 + 数据部分组成。TCP 报文头要求按 32 位对齐(与 IP 层协同)
字段名 | 长度 | 说明 |
---|---|---|
源端口 | 16位 | 发送方进程绑定的端口号 |
目的端口 | 16位 | 接收方进程绑定的端口号(用于上层交付) |
序列号 | 32位 | 本段数据第一个字节的编号 |
确认号 | 32位 | 期望接收的下一个字节序号(仅在 ACK 位=1 时有效) |
首部长度 HL | 4位 | 单位为 4 字节,用于解析 TCP 报头长度(含选项) |
保留位 | 6位 | 全部为 0,保留给以后使用 |
标志位 Flags | 6位 | SYN/ACK/FIN/RST/PSH/URG 控制连接行为 |
窗口大小 | 16位 | 告诉对端:我还能接收多少字节(流量控制) |
校验和 | 16位 | TCP 的完整性验证(伪首部 + TCP 头 + 数据) |
紧急指针 | 16位 | 标记紧急数据(仅当 URG=1 时有效) |
选项字段 | 可变 | 如 MSS、窗口扩大因子、时间戳等(32位对齐) |
数据部分 | 可变 | 实际发送的应用层数据 |
2.1 TCP首部长度(HLEN)
HLEN 字段占 TCP 头部的 4 位,单位为 4 字节。所以TCP 报头的长度范围为 0~15,即报头长度范围是 0~60 字节。
具体说明:
标准 TCP 报头固定长度为 20 字节,对应 HLEN = 5。
如果报头中包含选项字段,报头长度会大于 20 字节。
接收方通过 HLEN 字段确定 TCP 报头结束的位置,即数据部分的起始偏移。
举例:[若 HLEN = 6] → [报头 = 6 * 4 = 24 字节 ] → [数据部分从偏移 24 字节开始]
2.2 如何解析 TCP 报文中的数据?
1) 读取首部长度字段(HLEN)
- 读取此字段即可得到 TCP 报头的总长度:报头长度=HLEN×4 字
2) 提取固定报头部分(20 字节)
- 包含源端口、目的端口、序列号、确认号、标志位、窗口大小等核心信息。
- 这部分是 TCP 报文的基本结构,必须解析。
3) 提取选项字段(如果有)
选项字段长度为:选项长度=(HLEN×4)−20 字节
4) 提取有效载荷数据
剩余部分即为 TCP 数据段的有效载荷(Payload),传递给上层应用或协议处理。
2.3 TCP 的窗口机制与流量控制
TCP 为了防止接收方被过快的数据流压垮,引入了基于窗口的流量控制机制。
✅ 核心概念
接收窗口(Receive Window,rwnd):由接收方通告给发送方,表示接收方当前还能接收的缓冲区大小(以字节为单位)。
发送窗口(Send Window):发送方根据接收方的窗口值决定最大发送字节的范围。
滑动窗口:随着数据的确认接收,窗口向前滑动,允许新的数据继续发送。
✅ 工作机制
每个 TCP 报文头中包含一个 16 位的窗口字段
Window Size,
用来传递接收方的接收窗口大小;发送方只能发送序号在区间
[SND.UNA, SND.UNA + rwnd)
内的数据,其中SND.UNA
表示“未确认的最小序号”。接收方根据自身接收缓存的使用情况动态调整窗口大小,并反馈给发送方,防止接收方因缓存不足而被数据流压垮。
2.4 TCP 的确认应答机制
确认应答机制是 TCP 实现可靠传输的核心机制之一。
✅ 机制概述
每个 TCP 报文都可以携带 ACK 标志位 和 确认号字段;
确认号表示期望接收的下一个字节序列号,即接收方已成功收到的上一次数据。
TCP 默认使用累计确认机制,确认号表示“连续收到的最后字节的下一个序号”。
✅ 举例说明
发送方发送序列号范围为 100~199 的数据段;
接收方成功接收后,发送确认号为 200,表示:“我已成功收到序列号小于 200 的所有字节,请从 200 开始发送下一批数据”。
2.5 TCP 为什么需要确认应答?
✅ 原因一:确保可靠传输
IP 协议本身是不可靠的,数据包可能会出现丢失、乱序、重复等情况。
TCP 通过确认应答机制,能够检测数据是否成功到达对端,保证数据传输的可靠性。
✅ 原因二:驱动数据传输窗口前进
TCP 发送方依赖收到的确认号(ACK)反馈,来滑动发送窗口,继续发送后续数据。
如果没有确认反馈,发送窗口将停滞,导致数据无法继续发送,影响传输效率。
✅ 原因三:支持重传机制
通过确认号,发送方能够判断哪些数据段未被接收方确认。
在超时或收到重复 ACK 的情况下,发送方能够触发重传,保障数据不丢失。
2.6 ACK 应答机制的三大特性
✅ 特性一:累计确认(Cumulative ACK)
含义:TCP 默认使用 累计确认机制:
只确认序号连续的数据段的最后一个字节。
举例:
若接收方收到字节序列号为
0~499
,则发回ACK = 500
;如果
500~599
丢失,接收方收到600~699
也不会确认,只会继续发送ACK = 500
。注:可选 SACK(Selective ACK)
TCP 的扩展机制,允许接收方告诉发送方具体哪些数据段已经成功接收。
提升在网络拥堵或乱序环境中的重传效率,避免不必要的重复发送
✅ 特性二:延迟确认(Delayed ACK)
含义:允许 不立即发送 ACK,而是略微延迟(典型值如 40~200ms)后再发送,以期望:
- 在延迟窗口内有更多的数据可一并确认,从而减少 ACK 报文数量,提高链路利用率。
举例:
TCP 可能会等待一小段时间,看是否可以合并多个 ACK 或数据与 ACK 一起发送;
若超时无数据到达,也会强制发送 ACK。
注:为防止死锁,RFC 要求: 延迟确认时间不得超过 500ms。
✅ 特性三:快速重复确认(用于快速重传)
含义:当接收方收到乱序数据段,即前面的数据段丢失,后续却已到达,会连续发送相同的 ACK 确认号:
- 表示“我还在等待之前那个序列号的数据”。
举例:
已收到
0~499
,丢了500~599
,却先收到了600~699
;接收方会反复发送
ACK = 500
;发送方一旦收到 3 个重复 ACK,就会触发 快速重传机制,无需等待超时。
2.7 序列号与确认号的作用
✅ 序列号(Sequence Number)
表示 TCP 数据流中每个字节的唯一编号。
每个 TCP 报文段携带其首字节的序列号,用于标识数据的位置。
在 TCP 连接建立时,客户端和服务端各自生成一个随机的初始序列号,用于防止旧连接报文的干扰。
✅ 确认号(Acknowledgment Number)
表示接收方期望收到的下一个字节序号;
用于接收方向发送方确认已收到数据;
只有当 TCP 报文中的 ACK 标志位为 1 时,确认号字段才有效。
2.8 TCP 报文中的 6 个控制标志位
标志位 | 含义 | 用途 |
---|---|---|
SYN | 建立连接请求 | 三次握手中使用 |
ACK | 确认应答标志 | 大多数数据包都设置此位 |
FIN | 主动关闭连接请求 | 四次挥手中使用 |
RST | 复位连接 | 非法请求或异常时终止连接 |
PSH | 推送功能 | 告知对方立即处理数据 |
URG | 紧急指针有效 | 基本已废弃,极少使用 |
三、TCP 的可靠传输策略
TCP 为了实现“面向连接、可靠传输”的目标,设计了一系列机制,保障数据在复杂网络环境下依然能顺序到达、不重复、不丢失、不乱序。
3.1 TCP 的超时重传与快速重传机制
✅ 超时重传(Timeout Retransmission)
TCP 为每个已发送但未被确认的数据段设置一个重传定时器;
如果定时器超时仍未收到 ACK,则该段会被重新发送。
📌 关键参数
RTO(Retransmission Timeout):重传超时时间,动态计算:
RTO = SRTT + 4 * RTTVAR
其中:
SRTT:平滑往返时间,对最近 RTT 进行指数加权平均
RTTVAR:RTT(往返时间)偏差,估计 RTT 的变异程度
✅ 快速重传(Fast Retransmit)
当接收端收到乱序数据段时,会发送对最后一个连续正确接收数据段的确认,即重复 ACK。
发送端一旦接收到连续 3 个相同的重复 ACK,即认为数据段丢失,无需等待超时,立即重传该丢失的数据段。
为什么重新发送数据段N+1,确认号ACK=N+3,而不是+1变为N+2?
TCP 确认号始终指示“已经连续收到的数据的下一个字节”,而不是“最后一个收到数据的序号”。
当接收方收到丢失的 N+1 数据段后,现在,接收方已经收到 N、N+1、和之前乱序收到的 N+2。数据已经是连续的,从 N 到 N+2 都收到完整。因此,接收方确认下一个期待字节序号是 N+3。
3.2 TCP 的连接建立与断开机制
TCP 是面向连接的协议,通信前需要建立连接,结束后需显式关闭连接。
3.2.1 TCP 三次握手建立连接的过程
✅ 目的
双方协商初始序列号(ISN);
确保双向通路可达;
建立连接状态。
✅ 三次握手流程
第一次握手:客户端发送 SYN
✦ 客户端应用层动作
调用:
connect(sockfd, ...)
结果:
触发内核创建一个带有
SYN
标志的 TCP 报文段。该报文段包含客户端初始序列号(
client_isn
)。客户端进入
SYN_SENT
状态。✦ 服务端内核行为
TCP 层监听 socket(通过
listen()
进入监听状态)。收到 SYN 报文后:
自动生成半连接(SYN队列)。
发送带有
SYN+ACK
标志的响应(第二次握手)。
第二次握手:服务端回应 SYN+ACK
✦ 服务端应用层动作
调用顺序:
socket()
创建监听 socket;
bind()
绑定地址;
listen()
启用监听(此时服务端进入 LISTEN 状态);
accept()
等待连接(阻塞,直到三次握手完成)。✦ 内核行为
收到客户端的 SYN 后,回应 SYN+ACK 报文;
服务端进入
SYN_RCVD
状态;报文中携带服务端自己的初始序列号
server_isn
,并 ACK 客户端的client_isn + 1
。
第三次握手:客户端回应 ACK
✦ 客户端内核行为
收到服务端的 SYN+ACK 后,回复一个 ACK 报文:
确认号为
server_isn + 1
;此时客户端进入
ESTABLISHED
状态;
connect()
系统调用 返回成功(0),应用层正式感知连接建立完成。✦ 服务端内核行为
收到客户端 ACK 报文后,连接完成,状态变为
ESTABLISHED
;将连接从半连接队列转到全连接队列,进入全连接队列(
accept()
会从此队列中取出连接);
accept()
系统调用返回一个新的连接 socket,用于与客户端通信。✅ 特性
第三次握手可以携带数据(如 HTTP 请求),提高效率;
如果第三次 ACK 丢失,服务端重发 SYN+ACK,客户端重传 ACK。
3.2.2 TCP 四次挥手断开连接的过程
✅ 原因:全双工连接,必须双方独立关闭方向
FIN 表示“我已经没有数据可发了”,但仍可接收对方数据;
TCP 使用四次挥手确保双方都关闭。
✅ 四次挥手流程
第一次挥手:主动关闭方发送 FIN
✦ 主动关闭方应用层动作
调用:
close(sockfd)
结果:
内核发送一个带有 FIN 标志的 TCP 报文段;
主动关闭方进入
FIN_WAIT_1
状态。✦ 被动关闭方内核行为
收到 FIN 报文;
发送 ACK 报文确认;
被动关闭方进入
CLOSE_WAIT
状态;应用层被通知连接关闭请求(
read()
会返回 0 或触发关闭事件)。
第二次挥手:被动关闭方回应 ACK
✦ 被动关闭方应用层动作
应用程序在收到关闭通知后,调用
close(sockfd)
(或其他关闭方式);准备关闭连接。
✦ 被动关闭方内核行为
发送带有 FIN 标志的 TCP 报文段;
被动关闭方进入
LAST_ACK
状态。
第三次挥手:主动关闭方收到 FIN,回应 ACK
✦ 主动关闭方内核行为
收到被动关闭方的 FIN 报文;
回复 ACK 报文确认;
进入
TIME_WAIT
状态,等待足够时间确保对方收到确认。✦ 主动关闭方应用层动作
等待 2 倍最大报文生存时间(2MSL)后,内核释放连接资源;
连接最终关闭,进入
CLOSED
状态。
第四次挥手:被动关闭方收到 ACK,连接关闭
✦ 被动关闭方内核行为
收到主动关闭方的 ACK 报文;
连接关闭,进入
CLOSED
状态;释放资源。
✅ TIME_WAIT 的作用
保证最后一个 ACK 能到达对方;
防止旧连接残留包干扰新连接(使用相同四元组);
持续 2 倍最大报文生存时间(MSL,通常为 2 分钟)。