☰
【Linux笔记】TCP协议
2026/10/7 10:04:04 网站建设 项目流程

一、TCP报文结构

0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | ← 端口号 (16位) +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | ← 序号字段(32位) +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgment Number | ← 确认号(32位) +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | |U|A|P|R|S|F| | | Offset| Reserved |R|C|S|S|Y|I| Window | | | |G|K|H|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | Other options | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | Payload | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

二、TCP 工作原理

2.1 标准问题

2.1.1TCP报头和有效载荷分离

TCP报头分离:基于动态首部长度 + 流式重组

  • 动态首部长度:首部长度(报头长度)可变

  • 流式重组:面向字节流,载荷本身没有天然的消息边界。

A.报头与载荷的分离:首部长度字段

关键字段: TCP 首部第 12 字节的高 4 位是 Data Offset(数据偏移/首部长度) 字段。

  • 单位:该字段单位为4字节

  • 计算方式:Header Size (bytes)=Data Offset×4 Header Size (bytes)=Data Offset × 4

Data Offset 取值范围: 0101 ~ 1111 ​ TCP的报头长度最小为:20 byte 固定长度 ​ TCP的报头长度最大为:15 * 4byte =60byte ​ TCP的报头长度范围为 20 Byte ~ 60 Byte,且只能为4Byte的整数倍。

分离过程:

1. 读取 TCP 报文段的前 20 字节(最小首部)。 ​ 2. 解析 Data Offset 字段的值(例如值为 5,则首部为 20 字节;值为 8,则首部为 32 字节)。 ​ 3. 根据计算出的首部长度,跳过相应字节数。 ​ 4. 剩余部分即为 TCP 有效载荷。

注意:如果 Data Offset 的值小于 5 或大于 15,或者计算出的首部长度超过了实际收到的报文段总长度,该报文段被视为畸形包并丢弃。

B 载荷内部的"分离":字节流语义

TCP 协议:本身并不负责将载荷拆分为应用层消息,它只保证字节流的有序、可靠传输。。

它眼中的"载荷"就是一堆连续的字节,不关心这些字节代表什么含义

应用层消息边界:TCP只负责将字节流进行传输,对于"有效载荷"本身完全由应用层协议自行处理。

固定长度: 每个消息固定 N 字节。 ​ 长度前缀: 消息头部包含一个表示消息体长度的字段(如 HTTP/2 的 Frame Length、自定义协议的 Len-Value 结构)。 ​ 特殊分隔符: 使用特定字符序列标记消息结束(如 HTTP/1.x 的 \r\n\r\n、SMTP 的点号填充)。 ​ 更高层编码: 如 Protobuf、ASN.1 等自描述格式。

2.1.2TCP向上交付

⑥ 应用层:read()/recv() 拷贝纯数据到用户空间 ▲ │ ⑤ 数据交付:仅将 Payload 挂入接收缓冲区 ▲ │ ④ 传输层:tcp_v4_rcv() → 校验 + 剥离 TCP 头 + 提取控制信息 ▲ │ ③ 网络层:ip_rcv() → 校验 + 剥离 IP 头 ▲ │ ② 链路层:netif_receive_skb() → 剥离 Ethernet 头 ▲ │ ① 驱动层:中断处理 → 从 Ring Buffer 取出 skb,构建 skb,记录各层头部位置 ▲ │ ⓪ 网卡硬件层:DMA 将数据写入 Ring Buffer,产生硬中断

2.1.3TCP向下交付

⑥ 应用层: send()/write() → 将纯数据从用户空间拷贝到内核 Socket 发送缓冲区 │ ▼ ⑤ 数据装配: 从发送队列取出 skb,将 Payload 挂载到 skb 数据区 │ ▼ ④ 传输层: tcp_write_xmit() / tcp_transmit_skb() → 构建 TCP 头(端口、Seq、Flags)+ 计算校验和 │ ▼ ③ 网络层: ip_queue_xmit() / ip_local_out() → 构建 IP 头(地址、TTL)+ 校验 + 路由查找(决定出口网卡) │ ▼ ② 链路层: dev_queue_xmit() → 邻居子系统(ARP)填充 Ethernet 头(MAC 地址),压入网卡发送队列 │ ▼ ① 驱动层: ndo_start_xmit() 回调 → 将 skb 数据映射到 DMA 地址,更新 Ring Buffer 描述符(Tx Descriptor) │ ▼ ⓪ 网卡 DMA: 网卡硬件通过 DMA 从 Ring Buffer 取走数据,串行化后发往物理链路(PHY)

2.2 TCP 数据报传输的核心原理

2.2.1 TCP序号简介

TCP 序号:Sequence Number是一个32位的字段,可以理解为一个周长为2^32的环形跑道上的刻度标记。

seq next =(seq current +len) mod 2^32

0 / 2^32 (回绕点) ↗ ↖ / \ 2^32-1 * * 1 | | | ● rcv_nxt | | | 2^32-2 * * 2 \ / ↘ ↙ (2^31 附近) 整个圆环 = 32位序号空间 弧段 [rcv_nxt, rcv_nxt + window) = 当前有效窗口 ​ 窗口之外的序号 = 无效/过期/未来

解决的子问题:在无序、重复的数据流中唯一标识每一个字节。

字节流: H e l l o 序号: 100 101 102 103 104

2.2.2 确认应答机制

TCP 采用累积确认机制:接收方通过32位ACK( Acknowledgment Number)字段,告知发送方传送的字节全部接收。

  • 核心原理:收到 ACK = N,表示 N 之前(0~N-1)的所有字节已全部正确接收。

  • ACK 字段仅在ACK标志位为 1 时有效

发送端传输:Sequence Number 字段存放的是本段数据中第一个字节的序号。

发送端传输: 假设当前已确认到字节 1000,本段携带 500 字节数据: TCP Header: .... 其他字段 Sequence Number = 1000 ← 第一个字节的序号 .... 其他字段 Payload (500 bytes): 字节序号: 1000 1001 1002 ... 1498 1499 ↑ ↑ seq=1000 最后一个字节=1499 发送端计算得出:下一个报文段的 Sequence Number = 1000 + 500 = 1500

接收端应答:Acknowledgment Number字段存放的是发送端的seq+ 发送端的有效载荷payload_len

ACK= 最后一个已确认字节 + 1

即:end_seq=seq+payload_len

接收端应答: 假设应答端未捎带有效载荷,只做应答处理 TCP Header: .... 其他字段 Acknowledgment Number = 1500 .... 其他字段 Payload : 空

TCP 头部中没有 "Payload Length" 字段:载荷长度是通过减法从下层(IP层)推导

TCP Payload Length=IP Total Length−IP Header Length−TCP Header Length

计算示例:

IP Header: Total Length = 1500 bytes ← 整个 IP 数据包的总长度 IHL = 5 ← IP 头长度 = 5×4 = 20 bytes ↑ IP的首部长度 TCP Header: Data Offset = 8 ← TCP 头长度 = 8×4 = 32 bytes (含12字节Options) ↑ TCP的首部长度 TCP Payload Length = 1500 - 20 - 32 = 1448 bytes

2.2.3 流量控制机制 --滑动窗口

滑动窗口:是 TCP 实现"流量控制"和"高效传输"的核心机制。

发送方无需等待每个报文的 ACK,可以连续发送一个"窗口"内的多个报文。

窗口大小由两个维度共同决定:

  • 接收窗口(rwnd):接收方通告的可用缓冲区大小

  • 拥塞窗口(cwnd):发送方根据网络拥塞程度估算的值

  • 窗口大小 :window=min(rwnd,cwnd)

滑动窗口模型: |←——— 已发送且已确认 —————→ | ←——— 已发送未确认 ———→ |←— 待发送 —→| |_________________________|______________________________|____________| | ←—————————— 发送窗口 ———————→ | | | 窗口左沿 窗口右沿 snd_una snd_una + window snd_una : 窗口的左沿,最新一次ACK应答的确认序号。 window : 窗口的大小 snd_una + window:窗口的右沿

滑动窗口的作用位置:

  • 物理位置:作用域发送方的发送方内核的 TCP 发送缓冲区中,该缓冲区可以理解为环形跑道,不会导致窗口向右一直扩展。

  • 逻辑位置:在协议栈的发送路径中,在每次发送数据之前,都需要进行判断数据是否超出窗口。

结论:滑动窗口作用在发送方内核的发送缓冲区上,是发送路径中的一个"门控"机制。

滑动窗口的向右滑动触发:收到 ACK(最常见的滑动触发)

收到 ACK=5001 → 窗口左沿 从 4001 更新为 5001 → 窗口左沿 右移 1000 字节 → 原本被窗口挡住的数据现在可以发送了 → 如果还有未发送数据且 cwnd 允许,立即发送

滑动窗口中数据丢包的三种情况:

  • 窗口的最左侧数据丢包

  • 窗口的中间数据丢包

  • 窗口的最右侧数据丢包

在滑动窗口中丢包的三种情况,都可以转化为最左侧数据丢包,原因如下所示:

  • 核心机制:只有当发送端接收到了ACK,窗口的左沿才会进行向右滑动。

    • 若出现了报文丢失,ACK应答能够定位到缺失报文的序号,滑动窗口的左沿进行滑动到缺失序号处。

    • 此时最滑动窗口的最左端,就是丢失的报文,通过重传机制,发送端可以进行补发缺失的数据。

  • 若中间和最右侧数据丢包,滑动窗口先更新接收的ACK应答,窗口的左沿滑动到了数据丢包的位置,此时问题转换为了最左侧数据丢包。

解析:最左侧数据数据丢包 发送端:发送窗口的四个数据包 1000 ~ 2000 、 2001 ~ 3000 、 3001 ~ 4000 、4001 ~ 5000 其中数据包 1000 ~ 2000 丢包 A. 丢包情况一:发送端没有成功发送 1000 ~ 2000 序号的数据包,在网络中丢失。 发送端对2001 ~ 3000 的ACK应答为 1001 发送端对3001 ~ 4000 的ACK应答为 1001 发送端对4001 ~ 5000 的ACK应答为 1001 此时窗口的左沿仍然为1001,不会进行滑动,丢失的数据包正好为窗口的左沿,通过重传机制进行补发 B. 丢包情况二:接收端收到数据包,1000 ~ 2000 序号 甚至 3001 ~ 4000 的ACK应答在网络中丢失,其他应答成功发送。 在这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认 例如 :发送端成功发送对 4001 ~ 5000 的ACK应答为 5001。 此时,说明接收端此前的所有数据包都成功接收了,滑动窗口正常向右滑动。

2.2.4 流量控制

TCP 流量控制:解决发送方与接收方速度不匹配问题的机制。

它的核心目标只有一个:防止发送方发送过快,导致接收方缓冲区溢出、数据丢失。

流量控制 ≠ 拥塞控制

流量控制:是端到端的(发送方 ↔ 接收方),关注的是接收方的处理能力。

拥塞控制:是全局的(发送方 ↔ 网络),关注的是网络链路的承载能力。

核心机制:接收方通过 TCP 报文头部的 Window 字段,将自己当前可用的接收缓冲区大小通告给发送方。

1.接收方传达可用窗口的方式 在进行应答的时候,接收方通过设定:TCP 头部第 14~15 字节(16位无符号整数) 16 位最大值 = 65535 字节 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Window | ← 接收窗口通告值 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 2. 接收方动态计算接收方可用窗口 rwnd = 接收缓冲区总大小 - 已占用缓冲区大小

零窗口与窗口探测:应对极端情况,接收方的缓冲区满了,发送端进行试探接收端是否处理了数据,接收端的缓冲区是否还有空间。

2.2.5 拥塞控制

TCP 拥塞控制:是专门解决网络链路承载能力不足问题的机制。

它的核心目标是:在不压垮网络的前提下,尽可能高效地利用可用带宽。

拥塞控制的核心变量:拥塞窗口(cwnd)。

  • cwnd是由发送方根据网络状况自行计算

  • cwnd 的单位通常是MSS 个数(而非字节),其中 1 MSS ≈ 1460 字节(以太网 MTU=1500 减去 IP/TCP 头)。

拥塞控制的阶段图:

阶段一:慢启动(Slow Start)

- 触发条件:连接刚建立 / RTO超时后 / 长时间空闲后 - 核心机制:每收到一个ACK,cwnd += 1 MSS - 流程如下: 第0轮:cwnd=1,发出1个包 → 收到1个ACK → cwnd += 1 → cwnd=2 第1轮:cwnd=2,发出2个包 → 收到2个ACK → cwnd += 1 × 2 → cwnd=4 第2轮:cwnd=4,发出4个包 → 收到4个ACK → cwnd += 1 × 4 → cwnd=8 第3轮:cwnd=8,发出8个包 → 收到8个ACK → cwnd += 1 × 8 → cwnd=16 - 效果: 一轮内发了 N 个包,就收到 N 个 ACK,每个 ACK 让 cwnd +1,所以一轮下来 cwnd 从 N 变成 2N。 cwnd 指数增长(1→2→4→8→16...) - 终止条件: 当 cwnd 达到 ssthresh(慢开始阈值)= 16 时结束。ssthresh 是一个人为设定的分界线: cwnd < ssthresh:网络状况未知,用指数增长快速探测 cwnd ≥ ssthresh:已经接近可能的拥塞点,改用线性增长谨慎探测

阶段二:拥塞避免机制

- 触发条件:cwnd ≥ ssthresh - 核心机制:每个 RTT 周期,cwnd += 1 MSS - RTT:数据从发送方出发,到达接收方,再收到接收方发回的确认应答(ACK),这整个过程所耗费的总时间 - 流程如下: 第4轮:cwnd=16,发出16个包 → 收到16个ACK → 每个ACK让cwnd += 1/16 → 一轮下来 cwnd += 16 × (1/16) = 1 → cwnd=17 第5轮:cwnd=17 → cwnd=18 ...依此类推,每轮恰好 +1 - 效果: 具体实现是每收到一个 ACK,cwnd += 1/cwnd。 这样一轮内收到 cwnd 个 ACK,总共增加 cwnd × (1/cwnd) = 1。 - 终止条件:发生丢包

阶段三:"超时重传" 或 "快速重传 + 快速恢复"

"超时重传"

触发条件:RTO 到期仍未收到 ACK 含义:网络可能严重拥塞,大量报文丢失 处理: ssthresh = max(cwnd / 2, 2 MSS) cwnd = 1 MSS 重新进入慢启动

"快速重传 + 快速恢复"

触发条件:收到 3 个重复 ACK(DupACK) 含义:有个别报文丢失,但后续报文已到达接收方 → 网络没有严重拥塞,只是个别丢包 快速重传:立即重传丢失的报文,不等RTO 快速恢复: ssthresh = cwnd / 2 cwnd = ssthresh + 3 (+3是因为3个DupACK意味着有3个报文已离开网络) 之后每收到一个DupACK,cwnd += 1(允许这些已离网的报文继续传输) 收到新数据的ACK后,cwnd = ssthresh,进入拥塞避免

2.2.6 重传机制

重传机制:TCP在应对报文丢包时的处理机制

  • 超时重传:RTO 到期,未收到 ACK

    • 发送方发出一个数据包后,启动一个重传计时器。如果在计时器耗尽(即RTO时间)之前,还没收到对应的ACK,发送方就认定该包已丢失。

  • 快速重传:收到 3 个重复 ACK(不需要等 RTO)

    • 发送方连续收到 3 个重复的 ACK(即 Dup ACK,比如收到 ACK=2001、ACK=2001、ACK=2001)。

丢包的场景一:发送端数据在网络传输的过程中,没有成功抵达到接收端

丢包的场景二:发送端数据在网络传输的过程中,成功抵达到接收端,但是发送端的应答,没有成功抵达到到发送端。

超时重传机制示意图:

发送方 接收方 |--- SEQ=1, LEN=1000 ------------->| ✓ 收到 |<-- ACK=1001 ---------------------| | | |--- SEQ=1001, LEN=1000 ---------->| ✗ 丢失 | [等待 RTO...] | | [RTO 到期!] | |--- SEQ=1001, LEN=1000 ---------->| ✓ 收到(重传) |<-- ACK=2001 ---------------------|

快速重传机制示意图

发送方 接收方 |--- SEQ=1, LEN=1000 ----------->| ✓ | | |<-- ACK=1001 ---------------------| | | |--- SEQ=1001, LEN=1000 ---------->| ✗ 丢失 | | |--- SEQ=2001, LEN=1000 ---------->| ✓ 但期望1001 | | |<-- ACK=1001 (dup#1) -------------| | | |--- SEQ=3001, LEN=1000 ---------->| ✓ | | |<-- ACK=1001 (dup#2) -------------| | | |--- SEQ=4001, LEN=1000 ---------->| ✓ | | |<-- ACK=1001 (dup#3) -------------| ← 触发快速重传! | | |--- SEQ=1001, LEN=1000 ---------->| 立即重传 | | |<-- ACK=5001 ---------------------| 累积确认跳过已收到的

2.2.7 延时应答机制

延时应答的核心思想:既然迟早要发 ACK,不如等一等,看看能不能把 ACK "搭便车"捎带在数据包里一起发出去。

延时应答的规则: 规则1:ACK 可以被延迟,但延迟时间不得超过 500ms → 强制发送纯 ACK → 这是兜底机制,防止 ACK 被无限延迟 规则2:对于连续到达的数据段,至少每隔一个满尺寸段必须发送一个 ACK → 立即发送 ACK,确认这两个段 → 这是"至少每隔一个段必须确认"规则的体现 规则3:如果有反向数据要发送,ACK 应该立即捎带在数据包中 → 立即将 ACK 附带在数据包中发送 → 这是最理想的情况,零额外开销

场景解释:延时应答机制

收到第1个数据包 → 不急着回ACK,启动延时定时器(比如40ms) - 情况A:40ms内应用层有数据要发 → 把ACK捎带在数据包里一起发(理想情况) - 情况B:40ms内又收到了第2个数据包 → 立即发一个累积ACK确认两个包 - 情况C:40ms到了,既没有新数据也没收到第2个包 → 定时器到期,强制发ACK

2.3 TCP的标志位

2.3.1 SYN -同步初始序号

本质:同步初始序列号(ISN),是 TCP 连接的"出生证明"。

ISN(Initial Sequence Number):初始时的数据字节的编号

关键规则:

  • 标志位 SYN=1 时,seq 字段为ISN

  • SYN 和 FIN 不能同时置位(语义矛盾:既要建连又要断连)

使用场景:仅出现在连接建立阶段

客户端 → 服务端: SYN=1, seq=x (第一次握手) 服务端 → 客户端: SYN=1, ACK=1, seq=y, ack=x+1 (第二次握手) 客户端 → 服务端: ACK=1, seq=x+1, ack=y+1 (第三次握手,SYN=0) 因为SYN只用于同步初始序号,所有仅需在前两次握手的时候设置。

2.3.2 ACK -确认应答

本质:声明"确认号字段有效",是 TCP 可靠传输的反馈通道。

关键规则:

  • ACK=0 时,ack 字段无意义,接收方应忽略。

  • ACK=1 时,ack 字段表示"期望收到的下一个字节序号"。

ack=N 的含义:我已完整收到 N-1 及之前的所有字节 如果发送端收到 ack=1000,然后重复收到 ack=1000,说明接收端还在等 seq=0~999 如果收到 ack=1000,然后收到 ack=1500,说明 1000-1499 已全部到达

2.3.3 FIN -结束报文

本质:优雅地关闭一个方向的传输,是 TCP 连接的"死亡通知书"。

关键规则:

  • FIN 消耗一个序号空间(与 SYN 相同),即seq = 下一个待发送数据的序号(即上一次接收的ACK)

  • FIN 表示"我不再发送数据了",但仍可接收数据(半关闭)

  • FIN 和 RST 不应同时置位(FIN 是优雅关闭,RST 是异常中止)

  • 收到 FIN 后必须回复 ACK,否则对方会重传 FIN

  • 主动关闭方进入 TIME_WAIT 状态(2×MSL)

使用场景:

主动关闭方 → 被动关闭方: FIN=1, ACK=1, seq=u (第一次挥手) 被动关闭方 → 主动关闭方: ACK=1, ack=u+1 (第二次挥手) ... 被动关闭方可能继续发送剩余数据 ... 被动关闭方 → 主动关闭方: FIN=1, ACK=1, seq=w (第三次挥手) 主动关闭方 → 被动关闭方: ACK=1, ack=w+1 (第四次挥手)

2.3.4 RST -强制终止连接

本质:强制、立即、无条件地终止连接,是 TCP 的"紧急刹车"。

关键规则

  • RST 不消耗序号空间

  • RST 不需要确认(收到 RST 的一方不得回复任何报文)

场景说明
端口未监听收到 SYN 但目标端口无进程监听 → 回复 RST
连接已关闭收到数据但连接已不存在 → 回复 RST
半关闭写数据对端已发 FIN,本端仍尝试 write → 收到 RST
SO_LINGER=0 + close()应用要求立即丢弃未发送数据 → 发 RST 而非 FIN
超时/协议错误严重协议违规或长时间无响应
拒绝连接服务端 backlog 满且 tcp_abort_on_overflow=1

2.3.5 PSH -建议对端取走缓冲区数据

本质:提示接收方"不要缓冲,立即将数据交付给应用层"。

2.3.6 URG -紧急数据

本质:标记报文段中包含紧急数据,配合 Urgent Pointer 字段指示紧急数据的末尾位置

URG=1 时: Urgent Pointer = 紧急数据最后一个字节的偏移量(相对于 seq) 例如:seq=1000, Urgent Pointer=5 → 紧急数据为 seq=1000~1004(5字节) → seq=1005 起为普通数据

2.4 TCP的连接管理机制

2.4.1 TCP的连接 -三次握手

TCP 需要三次握手的理由:

  • 为了在不可靠的 IP 网络上以最短的请求方式建立一个全双工可靠连接(确认双方均有全双工的能力)。

  • 同步序列号(ISN),双方交换初始序列号,后续数据按序传输。

  • 协商参数,如 MSS(最大报文段长度)、窗口大小等

A.三次握手确认全双工?
a. 只进行一次握手能说明什么?
SYN 客户端 ---> 服务端

站在客户端的角度:

客户端无法确认: - 客户端自己的发送能力是否成功到达; - 客户端自己是否能够接收 - 服务端是否能够发送 - 服务端是否能够接收

站在服务端的角度:

若服务端收到了客户端的 SYN,服务端可以确认: - 客户端具有发送能力 - 服务端具有接收能力 服务端无法确认: - 客户端是否具有接收能力 - 服务端是否具有发送能力
b.只进行二次握手能说明什么?
SYN 客户端 ---> 服务端 SYN+ACK 客户端 <--- 服务端

站在客户端的角度:

若客户端收到了服务端的 SYN+ACK,客户端可以确认: - 客户端具有发送能力 - 客户端具有接收能力 - 服务端具有接收能力 - 服务端具有发送能力 此时客户端能够确认双方都具备全双工的能力。

站在服务端的角度:

服务端收到了客户端的 SYN,并回复了 SYN+ACK,但还没有收到客户端对 SYN+ACK 的确认,所以服务端只能确认: - 客户端具有发送能力 - 服务端具有接收能力 但服务端无法确认: - 客户端有接收能力:因为服务端不知道客户端是否收到了自己的 SYN+ACK; - 服务端自己有发送能力:因为服务端不知道自己的 SYN+ACK 是否成功到达客户端。
c.只进行三次握手能说明什么?
SYN 客户端 ---> 服务端 SYN+ACK 客户端 <--- 服务端 ACK 客户端 ---> 服务端

站在客户端的角度:

若客户端收到了服务端的 SYN+ACK,客户端可以确认: - 客户端具有发送能力 - 客户端具有接收能力 - 服务端具有接收能力 - 服务端具有发送能力 此时客户端能够确认双方都具备全双工的能力。

站在服务端的角度:

服务端收到了客户端的 SYN,并回复了 SYN+ACK,同时收到客户端对 SYN+ACK 的确认,所以服务端能确认: - 客户端具有发送能力 - 客户端有接收能力:因为客户端回了 ACK,说明客户端收到了服务端的 SYN+ACK; - 服务端具有接收能力 - 服务端自己有发送能力:因为 SYN+ACK 确实到达了客户端 此时服务端能够确认双方都具备全双工的能力。

2.4.2 TCP的断开连接 -四次挥手

A. 四次挥手的状态细节

第一次挥手:FIN_WAIT_1 → FIN_WAIT_2

  • 触发条件:主动关闭端在应用层调用close()或shutdown(fd, SHUT_WR)

  • 行为:发送一个 FIN 报文段,携带当前序列号(seq = x)

  • 状态变化:ESTABLISHED → FIN_WAIT_1

  • 含义:本端不再发送数据,但仍然可以接收数据(半关闭状态 )

第二次挥手:CLOSE_WAIT

  • 触发条件:被动关闭端收到 FIN 报文

  • 行为:内核自动回复 ACK(ack = x + 1),无需应用层参与

  • 状态变化:ESTABLISHED → CLOSE_WAIT

  • 含义:对端确认收到你的关闭请求,但对端可能还有数据要发送

  • 关键点:ACK 由内核自动发出,但 FIN 必须等应用层调用close()才发送

第三次挥手:LAST_ACK

  • 触发条件:被动关闭方应用层调用close()

  • 行为:发送自己的 FIN 报文段(seq = y)

  • 状态变化:CLOSE_WAIT → LAST_ACK

  • 含义:被动方也准备好关闭了

第四次挥手:TIME_WAIT

  • 触发条件:主动关闭方收到对端的 FIN

  • 行为:回复 ACK(ack = y + 1)

  • 状态变化:FIN_WAIT_2 → TIME_WAIT

  • 含义:确认对端也已关闭

  • 关键等待:进入 TIME_WAIT 后等待2×MSL(Linux 中 MSL = 30 秒,共 60 秒)

B. 深度解剖TIME_WAIT状态
原因解释
确保最后一个 ACK 到达如果最后一个 ACK 丢失,对端会重发 FIN。TIME_WAIT 期间可以重新回复 ACK
让旧连接的残余报文消亡网络中可能还有属于旧连接的延迟报文,2×MSL 确保它们全部过期,不会干扰新连接
保证连接正确终止双方都能可靠地关闭连接,不会有一方永远等待

为什么 TIME_WAIT 状态:需要等待 2×MSL 而不是 1×MSL

MSL:任何一个 TCP 报文段在网络中存活的最长时间上限。

最坏情况: 1. 主动方关闭发出最后一个 ACK(耗时最多 1×MSL 到达) 2. ACK 丢失,被动关闭方重发 FIN(又耗时最多 1×MSL 到达) 3. 主动关闭方需要在这个时间窗口内能收到重发的 FIN 并重发 ACK,重置 2×MSL 计时器。 所以总共需要 2×MSL。
C. 为什么不是三次挥手
三次握手时,第二次和第三次可以合并(SYN+ACK 一起发)。 但关闭时: 主动关闭方发出 FIN 报文后,被动关闭端立即回复 ACK 被动关闭端的应用层可能还有数据要发送(比如正在传输大文件),FIN 不能同 ACK 进行捎带应答立即发出。 所以 ACK 和 FIN 必须分开发送 → 多了一次交互 特殊情况:如果被动方没有剩余数据要发,内核可以将 ACK 和 FIN 合并为一个报文,此时退化为"三次挥手"。

2.5 TCP"面向字节流"与"粘包问题"

2.5.1 TCP 面向字节流

TCP:工作在传输层,它为应用层提供的是一个无边界的、连续的字节序列通道。

面向字节流的核心含义:

  • 发送端通过write()/send()写入时,字节序列会被 TCP 当作一条连续的河流来处理。

  • 接收端通过read()/recv()读取时,TCP 不保证每一次读取的字节数恰好等于发送端某一次写入的字节数。

  • 结论:字节流中没有消息边界的概念——TCP 不知道"一个消息"从哪里开始、到哪里结束。

A. TCP 数据在协议栈中的流转
应用层(用户数据) │ write()/send() ▼ ┌─────────────────────────────────┐ │ TCP 发送缓冲区 │ ← 内核空间 │ (Send Buffer, SO_SNDBUF) │ └─────────────────────────────────┘ │ TCP 根据 MSS 分段、加头部 ▼ ┌─────────────────────────────────┐ │ IP 层(可能再分片) │ └─────────────────────────────────┘ │ ▼ 网络传输 ... │ ▼ ┌─────────────────────────────────┐ │ TCP 接收缓冲区 │ ← 内核空间 │ (Recv Buffer, SO_RCVBUF) │ └─────────────────────────────────┘ │ read()/recv() ▼ 应用层(用户取走数据)

关键点:

  • send()只是把数据拷贝到内核发送缓冲区,并不代表数据已经发出。

  • recv()只是从内核接收缓冲区拷贝数据到用户空间,并不代表取走的是"一条完整消息"。

  • TCP 协议层会根据MSS(Maximum Segment Size)自行决定如何切分和合并数据。

B. MSS 与 MTU

MTU(Maximum Transmission Unit):链路层一帧能承载的最大数据量,以太网通常为 1500 字节。

MSS(Maximum Segment Size):TCP 报文段中数据部分的最大长度。

  • 以太网中:MSS = MTU - IP头(20) - TCP头(20) = 1460 字节

如果应用层一次写入超过 MSS 的数据,TCP 会将其拆分成多个报文段发送。

如果应用层多次写入少量数据,TCP 可能会合并它们到同一个报文段中发送。

2.3 发送缓冲区与接收缓冲区

发送缓冲区:应用write()后数据暂存于此,TCP 协议栈异步发送。

接收缓冲区:网络到达的数据暂存于此,应用read()时取走。

缓冲区是连续的字节空间,不记录应用层写入的"次数"或"边界"

2.5.2 TCP 粘包问题

A. 什么是粘包

粘包 是指:

  • 粘:发送端分多次发送的数据,在接收端被一次read()全部读到(多条消息粘在一起)。

  • 拆(也叫拆包):发送端一次发送的数据,在接收端被分多次read()才读完(一条消息被拆开)。

发送端: send("AAAA") → 4字节 send("BBBB") → 4字节 send("CCCC") → 4字节 接收端可能遇到的情况: 情况1(粘包): read() → "AAAABBBBCCCC" 一次读到12字节 情况2(拆包): read() → "AA" read() → "AABBBB" read() → "CCCC" 情况3(混合): read() → "AAAAB" read() → "BBBCCCC"
B.粘包发生的典型场景

核心原因:TCP 传输的是一串连续的、无结构的字节流,不保留应用层send()的边界信息。

例如:发送端调用了三次send(),但 TCP 只看到一串字节,接收端也只收到一串字节,至于哪些字节属于哪条消息,TCP 完全不知道,也不会帮你区分。

发送端因素:多次send()的数据可能被合并

如果发送缓冲区中已有数据,而新数据又很快写入,TCP 可能把缓冲区的多个数据块合并到一个 TCP 段中发送。

接收端因素:接收端调用read()时,如果内核接收缓冲区中已经积累了多个消息的字节,那么read()就会把这些字节一次性返回。

例如:

  • 例如接收缓冲区中已经有:AAAABBBBCCCC ,发送端调用了三次`send(AAAA),send(BBBB),send(CCCC )

  • 调用read(buf, 1024),它就会把 12 个字节全部读出,表现为“粘包”,这是因为read()并不按消息边界返回,只要缓冲区有数据就返回。

网络层因素:TCP 数据再ip层被分段与重组,接收端再按进行序号重组,应用层发送的消息被分段在各个TCP数据端中。

例如:

  • 发送端第一条消息"AAAA"可能和第二条消息的前半部分"BB"被封装在同一个 TCP 段中。

  • 接收端收到后放入缓冲区,应用层read()可能一次读走"AAAABB",这也是“粘包”的一种表现。

解决粘包的思路:

解决方案就是在应用层协议中加入边界信息(长度头、分隔符或固定长度),并在接收端严格按协议解析,配合循环读写和缓冲区管理,即可彻底解决。

解决粘包 │ ├── 消息长度是否固定? │ └── 是 → 定长协议 │ ├── 是否为纯文本协议? │ └── 是 → 分隔符协议(\r\n) │ └── 其他所有场景 └── 长度前缀协议(推荐) │ ├── 阻塞 IO → readn/writen 封装 └── 非阻塞 IO (epoll) → 每连接缓冲区 + 状态机解析

2.6 TCP异常处理

2.6.1 场景一:进程终止

2.6.2 场景二:机器重启

2.6.3 场景三:机器掉电 / 网线断开

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询