ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Linux笔记】TCP协议

【Linux笔记】TCP协议 一、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 × 4Data 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 取走数据串行化后发往物理链路PHY2.2 TCP 数据报传输的核心原理2.2.1 TCP序号简介TCP 序号Sequence Number是一个32位的字段可以理解为一个周长为2^32的环形跑道上的刻度标记。seq next (seq current len) mod 2^320 / 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 1042.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 ↑ ↑ seq1000 最后一个字节1499 发送端计算得出下一个报文段的 Sequence Number 1000 500 1500接收端应答Acknowledgment Number字段存放的是发送端的seq 发送端的有效载荷payload_lenACK 最后一个已确认字节 1即end_seqseqpayload_len接收端应答 假设应答端未捎带有效载荷只做应答处理 TCP Header: .... 其他字段 Acknowledgment Number 1500 .... 其他字段 Payload : 空TCP 头部中没有 Payload Length 字段载荷长度是通过减法从下层IP层推导TCP Payload LengthIP 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 bytes2.2.3 流量控制机制 --滑动窗口滑动窗口是 TCP 实现流量控制和高效传输的核心机制。发送方无需等待每个报文的 ACK可以连续发送一个窗口内的多个报文。窗口大小由两个维度共同决定接收窗口rwnd接收方通告的可用缓冲区大小拥塞窗口cwnd发送方根据网络拥塞程度估算的值窗口大小 windowmin(rwnd,cwnd)滑动窗口模型 |←——— 已发送且已确认 —————→ | ←——— 已发送未确认 ———→ |←— 待发送 —→| |_________________________|______________________________|____________| | ←—————————— 发送窗口 ———————→ | | | 窗口左沿 窗口右沿 snd_una snd_una window snd_una : 窗口的左沿最新一次ACK应答的确认序号。 window : 窗口的大小 snd_una window窗口的右沿滑动窗口的作用位置物理位置作用域发送方的发送方内核的 TCP 发送缓冲区中该缓冲区可以理解为环形跑道不会导致窗口向右一直扩展。逻辑位置在协议栈的发送路径中在每次发送数据之前都需要进行判断数据是否超出窗口。结论滑动窗口作用在发送方内核的发送缓冲区上是发送路径中的一个门控机制。滑动窗口的向右滑动触发收到 ACK最常见的滑动触发收到 ACK5001 → 窗口左沿 从 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 字节以太网 MTU1500 减去 IP/TCP 头。拥塞控制的阶段图阶段一慢启动Slow Start- 触发条件连接刚建立 / RTO超时后 / 长时间空闲后 - 核心机制每收到一个ACKcwnd 1 MSS - 流程如下: 第0轮cwnd1发出1个包 → 收到1个ACK → cwnd 1 → cwnd2 第1轮cwnd2发出2个包 → 收到2个ACK → cwnd 1 × 2 → cwnd4 第2轮cwnd4发出4个包 → 收到4个ACK → cwnd 1 × 4 → cwnd8 第3轮cwnd8发出8个包 → 收到8个ACK → cwnd 1 × 8 → cwnd16 - 效果 一轮内发了 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轮cwnd16发出16个包 → 收到16个ACK → 每个ACK让cwnd 1/16 → 一轮下来 cwnd 16 × (1/16) 1 → cwnd17 第5轮cwnd17 → cwnd18 ...依此类推每轮恰好 1 - 效果 具体实现是每收到一个 ACKcwnd 1/cwnd。 这样一轮内收到 cwnd 个 ACK总共增加 cwnd × (1/cwnd) 1。 - 终止条件发生丢包阶段三超时重传 或 快速重传 快速恢复超时重传触发条件RTO 到期仍未收到 ACK 含义网络可能严重拥塞大量报文丢失 处理 ssthresh max(cwnd / 2, 2 MSS) cwnd 1 MSS 重新进入慢启动快速重传 快速恢复触发条件收到 3 个重复 ACKDupACK 含义有个别报文丢失但后续报文已到达接收方 → 网络没有严重拥塞只是个别丢包 快速重传立即重传丢失的报文不等RTO 快速恢复 ssthresh cwnd / 2 cwnd ssthresh 3 3是因为3个DupACK意味着有3个报文已离开网络 之后每收到一个DupACKcwnd 1允许这些已离网的报文继续传输 收到新数据的ACK后cwnd ssthresh进入拥塞避免2.2.6 重传机制重传机制TCP在应对报文丢包时的处理机制超时重传RTO 到期未收到 ACK发送方发出一个数据包后启动一个重传计时器。如果在计时器耗尽即RTO时间之前还没收到对应的ACK发送方就认定该包已丢失。快速重传收到 3 个重复 ACK不需要等 RTO发送方连续收到 3 个重复的 ACK即 Dup ACK比如收到 ACK2001、ACK2001、ACK2001。丢包的场景一发送端数据在网络传输的过程中没有成功抵达到接收端丢包的场景二发送端数据在网络传输的过程中成功抵达到接收端但是发送端的应答没有成功抵达到到发送端。超时重传机制示意图发送方 接收方 |--- SEQ1, LEN1000 -------------| ✓ 收到 |-- ACK1001 ---------------------| | | |--- SEQ1001, LEN1000 ----------| ✗ 丢失 | [等待 RTO...] | | [RTO 到期] | |--- SEQ1001, LEN1000 ----------| ✓ 收到重传 |-- ACK2001 ---------------------|快速重传机制示意图发送方 接收方 |--- SEQ1, LEN1000 -----------| ✓ | | |-- ACK1001 ---------------------| | | |--- SEQ1001, LEN1000 ----------| ✗ 丢失 | | |--- SEQ2001, LEN1000 ----------| ✓ 但期望1001 | | |-- ACK1001 (dup#1) -------------| | | |--- SEQ3001, LEN1000 ----------| ✓ | | |-- ACK1001 (dup#2) -------------| | | |--- SEQ4001, LEN1000 ----------| ✓ | | |-- ACK1001 (dup#3) -------------| ← 触发快速重传 | | |--- SEQ1001, LEN1000 ----------| 立即重传 | | |-- ACK5001 ---------------------| 累积确认跳过已收到的2.2.7 延时应答机制延时应答的核心思想既然迟早要发 ACK不如等一等看看能不能把 ACK 搭便车捎带在数据包里一起发出去。延时应答的规则 规则1ACK 可以被延迟但延迟时间不得超过 500ms → 强制发送纯 ACK → 这是兜底机制防止 ACK 被无限延迟 规则2对于连续到达的数据段至少每隔一个满尺寸段必须发送一个 ACK → 立即发送 ACK确认这两个段 → 这是至少每隔一个段必须确认规则的体现 规则3如果有反向数据要发送ACK 应该立即捎带在数据包中 → 立即将 ACK 附带在数据包中发送 → 这是最理想的情况零额外开销场景解释延时应答机制收到第1个数据包 → 不急着回ACK启动延时定时器比如40ms - 情况A40ms内应用层有数据要发 → 把ACK捎带在数据包里一起发理想情况 - 情况B40ms内又收到了第2个数据包 → 立即发一个累积ACK确认两个包 - 情况C40ms到了既没有新数据也没收到第2个包 → 定时器到期强制发ACK2.3 TCP的标志位2.3.1 SYN -同步初始序号本质同步初始序列号ISN是 TCP 连接的出生证明。ISNInitial Sequence Number初始时的数据字节的编号关键规则标志位 SYN1 时seq 字段为ISNSYN 和 FIN 不能同时置位语义矛盾既要建连又要断连使用场景仅出现在连接建立阶段客户端 → 服务端: SYN1, seqx 第一次握手 服务端 → 客户端: SYN1, ACK1, seqy, ackx1 第二次握手 客户端 → 服务端: ACK1, seqx1, acky1 第三次握手SYN0 因为SYN只用于同步初始序号所有仅需在前两次握手的时候设置。2.3.2 ACK -确认应答本质声明确认号字段有效是 TCP 可靠传输的反馈通道。关键规则ACK0 时ack 字段无意义接收方应忽略。ACK1 时ack 字段表示期望收到的下一个字节序号。ackN 的含义我已完整收到 N-1 及之前的所有字节 如果发送端收到 ack1000然后重复收到 ack1000说明接收端还在等 seq0~999 如果收到 ack1000然后收到 ack1500说明 1000-1499 已全部到达2.3.3 FIN -结束报文本质优雅地关闭一个方向的传输是 TCP 连接的死亡通知书。关键规则FIN 消耗一个序号空间与 SYN 相同即seq 下一个待发送数据的序号即上一次接收的ACKFIN 表示我不再发送数据了但仍可接收数据半关闭FIN 和 RST 不应同时置位FIN 是优雅关闭RST 是异常中止收到 FIN 后必须回复 ACK否则对方会重传 FIN主动关闭方进入 TIME_WAIT 状态2×MSL使用场景主动关闭方 → 被动关闭方: FIN1, ACK1, sequ 第一次挥手 被动关闭方 → 主动关闭方: ACK1, acku1 第二次挥手 ... 被动关闭方可能继续发送剩余数据 ... 被动关闭方 → 主动关闭方: FIN1, ACK1, seqw 第三次挥手 主动关闭方 → 被动关闭方: ACK1, ackw1 第四次挥手2.3.4 RST -强制终止连接本质强制、立即、无条件地终止连接是 TCP 的紧急刹车。关键规则RST 不消耗序号空间RST 不需要确认收到 RST 的一方不得回复任何报文场景说明端口未监听收到 SYN 但目标端口无进程监听 → 回复 RST连接已关闭收到数据但连接已不存在 → 回复 RST半关闭写数据对端已发 FIN本端仍尝试 write → 收到 RSTSO_LINGER0 close()应用要求立即丢弃未发送数据 → 发 RST 而非 FIN超时/协议错误严重协议违规或长时间无响应拒绝连接服务端 backlog 满且 tcp_abort_on_overflow12.3.5 PSH -建议对端取走缓冲区数据本质提示接收方不要缓冲立即将数据交付给应用层。2.3.6 URG -紧急数据本质标记报文段中包含紧急数据配合 Urgent Pointer 字段指示紧急数据的末尾位置URG1 时 Urgent Pointer 紧急数据最后一个字节的偏移量相对于 seq 例如seq1000, Urgent Pointer5 → 紧急数据为 seq1000~10045字节 → seq1005 起为普通数据2.4 TCP的连接管理机制2.4.1 TCP的连接 -三次握手TCP 需要三次握手的理由为了在不可靠的 IP 网络上以最短的请求方式建立一个全双工可靠连接确认双方均有全双工的能力。同步序列号(ISN)双方交换初始序列号后续数据按序传输。协商参数如 MSS最大报文段长度、窗口大小等A.三次握手确认全双工a. 只进行一次握手能说明什么SYN 客户端 --- 服务端站在客户端的角度客户端无法确认 - 客户端自己的发送能力是否成功到达 - 客户端自己是否能够接收 - 服务端是否能够发送 - 服务端是否能够接收站在服务端的角度若服务端收到了客户端的 SYN服务端可以确认 - 客户端具有发送能力 - 服务端具有接收能力 服务端无法确认 - 客户端是否具有接收能力 - 服务端是否具有发送能力b.只进行二次握手能说明什么?SYN 客户端 --- 服务端 SYNACK 客户端 --- 服务端站在客户端的角度若客户端收到了服务端的 SYNACK客户端可以确认 - 客户端具有发送能力 - 客户端具有接收能力 - 服务端具有接收能力 - 服务端具有发送能力 此时客户端能够确认双方都具备全双工的能力。站在服务端的角度:服务端收到了客户端的 SYN并回复了 SYNACK但还没有收到客户端对 SYNACK 的确认所以服务端只能确认 - 客户端具有发送能力 - 服务端具有接收能力 但服务端无法确认 - 客户端有接收能力因为服务端不知道客户端是否收到了自己的 SYNACK - 服务端自己有发送能力因为服务端不知道自己的 SYNACK 是否成功到达客户端。c.只进行三次握手能说明什么?SYN 客户端 --- 服务端 SYNACK 客户端 --- 服务端 ACK 客户端 --- 服务端站在客户端的角度若客户端收到了服务端的 SYNACK客户端可以确认 - 客户端具有发送能力 - 客户端具有接收能力 - 服务端具有接收能力 - 服务端具有发送能力 此时客户端能够确认双方都具备全双工的能力。站在服务端的角度服务端收到了客户端的 SYN并回复了 SYNACK同时收到客户端对 SYNACK 的确认所以服务端能确认 - 客户端具有发送能力 - 客户端有接收能力因为客户端回了 ACK说明客户端收到了服务端的 SYNACK - 服务端具有接收能力 - 服务端自己有发送能力因为 SYNACK 确实到达了客户端 此时服务端能够确认双方都具备全双工的能力。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 报文行为内核自动回复 ACKack x 1无需应用层参与状态变化ESTABLISHED → CLOSE_WAIT含义对端确认收到你的关闭请求但对端可能还有数据要发送关键点ACK 由内核自动发出但 FIN 必须等应用层调用close()才发送第三次挥手LAST_ACK触发条件被动关闭方应用层调用close()行为发送自己的 FIN 报文段seq y状态变化CLOSE_WAIT → LAST_ACK含义被动方也准备好关闭了第四次挥手TIME_WAIT触发条件主动关闭方收到对端的 FIN行为回复 ACKack y 1状态变化FIN_WAIT_2 → TIME_WAIT含义确认对端也已关闭关键等待进入 TIME_WAIT 后等待2×MSLLinux 中 MSL 30 秒共 60 秒B. 深度解剖TIME_WAIT状态原因解释确保最后一个 ACK 到达如果最后一个 ACK 丢失对端会重发 FIN。TIME_WAIT 期间可以重新回复 ACK让旧连接的残余报文消亡网络中可能还有属于旧连接的延迟报文2×MSL 确保它们全部过期不会干扰新连接保证连接正确终止双方都能可靠地关闭连接不会有一方永远等待为什么 TIME_WAIT 状态需要等待 2×MSL 而不是 1×MSLMSL任何一个 TCP 报文段在网络中存活的最长时间上限。最坏情况 1. 主动方关闭发出最后一个 ACK耗时最多 1×MSL 到达 2. ACK 丢失被动关闭方重发 FIN又耗时最多 1×MSL 到达 3. 主动关闭方需要在这个时间窗口内能收到重发的 FIN 并重发 ACK重置 2×MSL 计时器。 所以总共需要 2×MSL。C. 为什么不是三次挥手三次握手时第二次和第三次可以合并SYNACK 一起发。 但关闭时 主动关闭方发出 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 协议层会根据MSSMaximum Segment Size自行决定如何切分和合并数据。B. MSS 与 MTUMTUMaximum Transmission Unit链路层一帧能承载的最大数据量以太网通常为 1500 字节。MSSMaximum Segment SizeTCP 报文段中数据部分的最大长度。以太网中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() → BBBCCCCB.粘包发生的典型场景核心原因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 场景三机器掉电 / 网线断开
返回列表