网络通信之TCP协议
目录
一、TCP协议概述
1.1 TCP 协议简介
1.2 传输控制协议
1.2.1 控制逻辑源自内核协议栈
1.2.2 为什么说 TCP 在“控制传输”?
1.3 TCP 的字节流传输模型
1.3.1 什么叫“面向字节流”?
1.3.2 为什么 TCP 设计为字节流?
1.3.3 两次 send() 会发生什么?
1.3.4 粘包与拆包问题的根本原因
1.3.5 如何解决粘包/拆包?
二、 TCP 报文段结构解析
2.1 TCP首部长度(HLEN)
2.2 如何解析 TCP 报文中的数据?
2.3 TCP 的窗口机制与流量控制
2.4 TCP 的确认应答机制
2.5 TCP 为什么需要确认应答?
2.6 ACK 应答机制的三大特性
2.7 序列号与确认号的作用
2.8 TCP 报文中的 6 个控制标志位
三、TCP 的可靠传输策略
3.1 TCP 的超时重传与快速重传机制
3.2 TCP 的连接建立与断开机制
3.2.1 TCP 三次握手建立连接的过程
3.2.2 TCP 四次挥手断开连接的过程
一、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 分钟)。