ARTICLE DETAIL

资讯详情

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

数据封装揭秘:每一层只关心自己的头部

数据封装揭秘:每一层只关心自己的头部 核心机制一句话:每一层把上层交来的东西整体当作数据,前面加上自己的头部,再交给下一层。接收端反向逐层剥离。这个设计让每一层只需要认识自己的头部,不用关心上层在传什么,也不用关心下层怎么送——这是分层模型能成立的全部基础。一、整体过程图发送端 接收端 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 应用层 ┌──────────────┐ ┌──────────────┐ │ 用户数据 │ Message / Data │ 用户数据 │ └──────┬───────┘ └──────▲───────┘ │ 加 TCP/UDP 头 │ 剥 TCP/UDP 头 传输层 ┌──────▼───────────────┐ ┌──────────┴───────┐ │TCP头│ 用户数据 │ Segment │TCP头│ 用户数据 │ └──────┬───────────────┘ └──────▲───────────┘ │ 加 IP 头 │ 剥 IP 头 网络层 ┌──────▼──────────────────────┐ ┌───────────┴──────────┐ │IP头│TCP头│ 用户数据 │ Packet │IP头│TCP头│ 用户数据 │ └──────┬──────────────────────┘ └───────────▲──────────┘ │ 加帧头 帧尾(FCS) │ 剥帧头帧尾 链路层 ┌──────▼─────────────────────────────┐ ┌──────────┴──────────┐ │帧头│IP头│TCP头│ 用户数据 │FCS│ Frame │帧头│IP头│...│FCS│ │ └──────┬─────────────────────────────┘ └──────────▲──────────┘ │ 编码为信号 │ 信号还原 物理层 └──► 1010110100101110101001011101010 ────────┘ Bits每层的名称和产物:层产物名称(PDU)加的头部关心什么应用层Message / Data应用自定义业务语义传输层Segment(TCP)/ Datagram(UDP)端口、序列号进程到进程网络层Packet / Datagram源/目的 IP主机到主机,跨网路由链路层Frame源/目的 MAC同一网段内相邻节点物理层Bits / Symbols无比特如何变成信号二、每层头部的具体内容2.1 以太网帧(链路层)0 14 末尾 ├──────────┬──────────┬──────┬────────────────────────────┬──────┤ │ 目的 MAC │ 源 MAC │ 类型 │ Payload │ FCS │ │ 6 字节 │ 6 字节 │2 字节│ 46 ~ 1500 字节 │4 字节│ └──────────┴──────────┴──────┴────────────────────────────┴──────┘ 类型 (EtherType): 0x0800 IPv4 0x86DD IPv6 0x0806 ARP 0x8100 802.1Q VLAN 标签要点:最小帧长 64 字节(含 FCS,不含前导码)。Payload 不足 46 字节会被填充(Padding),这是 CSMA/CD 冲突检测的历史要求。FCS 是 CRC-32,只做检错不做纠错。错了直接丢弃,不通知上层——重传是传输层的事。帧头前面物理层还会加7 字节前导码 1 字节 SFD,用于时钟同步,通常不算进帧长。2.2 IPv4 头部(网络层)0 8 16 31 ┌───────┬───────┬───────────────┬───────────────────────────────────────┐ │Version│ IHL │ TOS │ Total Length │ │ (4) │ (4) │ (8) │ (16) │ ├───────┴───────┴───────────────┼─────┬─────────────────────────────────┤ │ Identification │Flags│ Fragment Offset │ │ (16) │ (3) │ (13) │ ├───────────────┬───────────────┼─────┴─────────────────────────────────┤ │ TTL │ Protocol │ Header Checksum │ │ (8) │ (8) │ (16) │ ├───────────────┴───────────────┴───────────────────────────────────────┤ │ Source IP Address (32) │ ├───────────────────────────────────────────────────────────────────────┤ │ Destination IP Address (32) │ ├───────────────────────────────────────────────────────────────────────┤ │ Options (可选,0 ~ 40 字节) │ └───────────────────────────────────────────────────────────────────────┘ 最小 20 字节,最大 60 字节关键字段:字段作用IHL头部长度,单位是 4 字节。值为 5 表示 20 字节(无 Options)Total Length头部 数据的总长,所以 IP 包理论上限 65535 字节Protocol上层是谁:6TCP,17UDP,1ICMP,41IPv6-in-IPv4TTL每过一个路由器减 1,到 0 丢弃并回 ICMP 超时。traceroute就靠它工作Identification / Flags / Offset分片用,见下文Header Checksum只校验头部,不校验数据。因为 TTL 每跳都变,每个路由器都得重算IPv6 把头部固定为 40 字节,去掉了校验和与分片字段(分片改由扩展头处理),目的是让路由器转发更快。2.3 TCP 头部(传输层)0 8 16 31 ┌───────────────────────────────┬───────────────────────────────────────┐ │ Source Port (16) │ Destination Port (16) │ ├───────────────────────────────┴───────────────────────────────────────┤ │ Sequence Number (32) │ ├───────────────────────────────────────────────────────────────────────┤ │ Acknowledgment Number (32) │ ├───────┬───────────┬───────────┬───────────────────────────────────────┤ │Offset │ Reserved │ Flags │ Window Size (16) │ │ (4) │ (4) │ (8) │ │ ├───────┴───────────┴───────────┼───────────────────────────────────────┤ │ Checksum (16) │ Urgent Pointer (16) │ ├───────────────────────────────┴───────────────────────────────────────┤ │ Options (0 ~ 40 字节) │ └───────────────────────────────────────────────────────────────────────┘ 最小 20 字节,最大 60 字节 Flags: CWR ECE URG ACK PSH RST SYN FIN常见 Options:MSS(最大段大小)、Window Scale(窗口扩大因子)、SACK(选择性确认)、Timestamps。TCP Checksum 覆盖:伪头部 TCP 头 数据。伪头部包含源/目的 IP 和协议号,这样能顺便验证这个包确实是发给我的。这是层间少见的一处耦合。2.4 UDP 头部(传输层)0 8 16 31 ┌───────────────────────────────┬───────────────────────────────────────┐ │ Source Port (16) │ Destination Port (16) │ ├───────────────────────────────┼───────────────────────────────────────┤ │ Length (16) │ Checksum (16) │ └───────────────────────────────┴───────────────────────────────────────┘ 固定 8 字节只有 8 字节。没有序列号、没有确认、没有窗口——所以可靠性、顺序、拥塞控制全部要应用层自己做。实时游戏选 UDP 就是为了这个什么都不做,从而避免 TCP 的队头阻塞和重传延迟。三、开销计算:你的 1 字节实际跑了多少发送 1 字节应用数据,走 TCP over Ethernet: 以太网前导码 SFD 8 字节 (物理层,通常不计入帧) 以太网帧头 14 字节 IP 头 20 字节 TCP 头 20 字节 应用数据 1 字节 以太网填充 (补到最小 46) 45 字节 ← 46-145 FCS 4 字节 帧间隔 IFG 12 字节 (物理层) ────────────────────────────────── 线路实际占用 124 字节 有效载荷比 1 / 124 ≈ 0.8%对比满载的情况:以太网帧头 IP TCP FCS 58 字节 应用数据 1460 字节 ────────────────────────────────── 有效载荷比 ≈ 96%这就是为什么高频小包是网络设计的大忌。实时游戏每帧发位置更新,如果每个实体一个包,带宽几乎全被头部吃掉。解法是包合并:❌ 10 个实体 → 10 个包 → 580 字节头部开销 ✅ 10 个实体 → 1 个包 → 58 字节头部开销四、MTU 与分片4.1 MTU 的层级应用层可用 ←─ MSS MTU - IP头 - TCP头 1500 - 20 - 20 1460 │ 链路层 MTU ←─ 1500 字节(以太网标准) │ 实际路径 ←─ Path MTU 路径上所有链路 MTU 的最小值常见 MTU 值:链路MTU以太网1500PPPoE(很多家宽)1492IPv6 最小要求1280VPN / IPsec 隧道典型 1400 左右Jumbo Frame(数据中心)90004.2 IP 分片过程一个 4000 字节的 IP 包要过 MTU1500 的链路:原始包: [IP头 20][ 数据 3980 ] Total Length 4000 ↓ 分片 片 1: [IP头 20][ 数据 1480 ] IDx Offset0 MF1 片 2: [IP头 20][ 数据 1480 ] IDx Offset185 MF1 片 3: [IP头 20][ 数据 1020 ] IDx Offset370 MF0 ← 最后一片 ↑ Offset 单位是 8 字节: 1480/8 185 重组条件:同一 Identification 同一源/目的 IP 同一协议号分片的问题:⚠️ 任何一片丢失 → 整个原始包都要重传 ⚠️ 重组消耗接收端内存和 CPU ⚠️ 很多防火墙/NAT 直接丢弃非首片(没有端口信息,无法做策略) ⚠️ IPv6 中间路由器不允许分片,只能由源端做所以实践中要避免分片。TCP 通过 MSS 协商天然避免;UDP 则要应用层自己控制包大小。4.3 Path MTU Discovery发送端设置 IP 头的 DF (Dont Fragment) 1 ↓ 路径上某路由器发现包太大且不能分片 ↓ 丢弃该包,回送 ICMP Type 3 Code 4 Fragmentation Needed,并告知自己的 MTU ↓ 发送端调小包大小重试现实中的陷阱:大量网络会过滤 ICMP,导致 PMTUD 失效。症状非常典型:小包能通(ping 正常、TCP 握手成功) 大包全丢(网页打开一半卡住、下载卡死)这就是所谓的ICMP 黑洞。所以 UDP 游戏协议通常把包大小保守地定在1200 字节以内,直接绕开整个问题。五、关键认知:端口号不在 IP 层初学最容易混的一点:┌─────────────────────────────────────────────────────┐ │ MAC 地址 → 链路层 → 同一网段内找到相邻网卡 │ │ IP 地址 → 网络层 → 跨网络找到目标主机 │ │ 端口号 → 传输层 → 在主机内找到具体进程 │ └─────────────────────────────────────────────────────┘一次完整的定位: 192.168.1.5 : 54321 ──► 203.0.113.8 : 443 └─ 哪台机 ─┘ └ 哪个进程┘ └─ 哪台机 ─┘ └ 哪个进程┘ IP 层 TCP 层 IP 层 TCP 层这个四元组(加协议号成五元组)唯一标识一条连接,也是 NAT 和防火墙做策略的依据。端口是传输层的概念,IP 头里没有端口字段——这也是为什么 IP 分片的非首片会被防火墙丢弃:端口信息只在第一片里。六、MAC 地址逐跳改变,IP 地址端到端不变这是封装机制里最容易被忽略、但面试和排障时最关键的一点。主机 A 路由器 R1 路由器 R2 主机 B 10.0.0.2 10.0.0.1 192.168.5.9 MAC: AA MAC: BB MAC: CC MAC: DD │ │ │ │ │ ① │ ② │ ③ │ ├──────────────────────┤───────────────────────┤────────────────────┤ ① A → R1 [帧: 目的BB 源AA][IP: 源10.0.0.2 目的192.168.5.9][TCP][数据] ② R1 → R2 [帧: 目的CC 源BB][IP: 源10.0.0.2 目的192.168.5.9][TCP][数据] ↑ MAC 换了 ↑ IP 完全没变 TTL 减 1 ③ R2 → B [帧: 目的DD 源CC][IP: 源10.0.0.2 目的192.168.5.9][TCP][数据] ↑ MAC 又换了 ↑ IP 还是没变 TTL 再减 1┌───────────────────────────────────────────────────────┐ │ IP 地址 端到端不变(除非经过 NAT) │ │ MAC 地址 每一跳都重写 │ │ TTL 每一跳减 1 │ │ IP 校验和 每一跳重算(因为 TTL 变了) │ └───────────────────────────────────────────────────────┘路由器的工作就是:剥掉旧帧头 → 查路由表 → 用 ARP 找下一跳 MAC → 封装新帧头 → 转发。它从不修改 IP 层以上的内容(NAT 除外,NAT 是刻意打破分层的)。七、隧道与 VPN:封装的嵌套隧道技术就是把一个完整的包当成数据,再封装一次。普通 IP 包: [以太网][IP][TCP][数据] VXLAN 封装(数据中心常见): [外层以太网][外层IP][UDP][VXLAN头][内层以太网][内层IP][TCP][数据] └────────── 底层网络只看这部分 ──────────┘└──── 当作纯数据 ────┘ IPsec 隧道模式: [以太网][新IP][ESP头][原IP][TCP][数据][ESP尾][ESP认证] └──── 加密部分 ────┘ WireGuard: [以太网][IP][UDP][WG头][ 加密后的整个内层 IP 包 ]隧道带来的直接后果是可用 MTU 变小:物理 MTU 1500 减 WireGuard 开销 -60 (IP 20 UDP 8 WG 32) ───── 隧道内可用 MTU 1440 再减内层 IP TCP -40 ───── 实际应用层可用 1400没正确设置隧道 MTU,症状和前面的 ICMP 黑洞一样:小包通、大包死。这是 VPN 环境下排障的第一个检查点。八、应用层要自己做封装:TCP 粘包TCP 提供的是字节流,不是消息流。发送方的send()边界在接收方完全不保留。发送方: send(HELLO) 5 字节 send(WORLD) 5 字节 接收方可能的结果(全都合法): ① recv() → HELLOWORLD 一次收完(粘包) ② recv() → HELLO WORLD 刚好分开 ③ recv() → HEL LOWOR LD 任意切分(拆包)成因:Nagle 算法合并小包、接收缓冲区累积、MSS 切分、网络重组。三种解决方案① 固定长度 [────── 64 字节 ──────] 简单,但浪费空间,只适合定长协议 ② 分隔符 HELLO\r\n WORLD\r\n HTTP 头部用这个。缺点是数据里出现分隔符要转义 ③ 长度前缀 ← 推荐 [长度 4][消息ID 2][ 消息体 ] 二进制友好,无需转义,解析高效长度前缀的实现publicstaticclassPacketCodec{privateconstintLenFieldSize4;privateconstintMsgIdSize2;privateconstintMaxBodySize64*1024;publicstaticbyte[]Encode(ushortmsgId,ReadOnlySpanbytebody){varbufnewbyte[LenFieldSizeMsgIdSizebody.Length];// 长度字段统计「msgId body」,不含自己。这个约定必须写进协议文档BinaryPrimitives.WriteInt32BigEndian(buf.AsSpan(0),MsgIdSizebody.Length);BinaryPrimitives.WriteUInt16BigEndian(buf.AsSpan(4),msgId);body.CopyTo(buf.AsSpan(6));returnbuf;}/// summary从累积缓冲中取出一个完整包;不足则返回 false 等更多数据/summarypublicstaticboolTryDecode(refReadOnlySpanbytebuffer,outushortmsgId,outReadOnlySpanbytebody){msgId0;bodydefault;if(buffer.LengthLenFieldSize)returnfalse;// 长度字段都没收全intpayloadLenBinaryPrimitives.ReadInt32BigEndian(buffer);// 必须校验!不校验等于把 OOM 开关交给对端,这是真实的 DoS 入口if(payloadLenMsgIdSize||payloadLenMaxBodySize)thrownewInvalidDataException($illegal length:{payloadLen});if(buffer.LengthLenFieldSizepayloadLen)returnfalse;// 包体没收全msgIdBinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(4));bodybuffer.Slice(6,payloadLen-MsgIdSize);bufferbuffer.Slice(LenFieldSizepayloadLen);// 前移,处理下一包returntrue;}}三个必须做对的细节:细节说明字节序网络协议统一大端(Big-Endian)。x86 是小端,混用会让数值完全错乱长度上限校验对端发一个 20 亿的长度,你就 OOM 了。这是漏洞,不是优化长度是否含自身定义清楚并写进文档。两端理解不一致是经典联调地狱UDP 不粘包,但要自己做可靠性TCP → 字节流 → 会粘包 → 需要长度前缀 → 但顺序/送达/去重由 TCP 保证 UDP → 数据报 → 不粘包 → 收到就是完整一包 → 但可能丢、可能乱序、可能重复、可能超 MTU所以实时游戏的 UDP 协议头通常自己带:[序列号 2][确认号 2][确认位图 4][频道 1][标志 1][ 消息体 ]这正是 KCP、ENet、QUIC、Unreal NetDriver 在做的事。不要自己从零写可靠 UDP,用成熟实现。九、QUIC:把封装重新组织了一遍HTTP/3 底下的 QUIC 值得单独提一句,因为它改变了封装结构:传统 HTTPS: [IP][TCP][TLS记录][HTTP/2 帧][数据] └ 内核 ┘└──── 用户态 ────┘ 问题:TCP 层的一个丢包会阻塞所有 HTTP/2 流(队头阻塞) QUIC / HTTP/3: [IP][UDP][QUIC 包头(明文)][ 加密载荷: QUIC 帧 HTTP/3 帧 数据 ] └ 内核 ┘└──────────────── 全在用户态 ────────────────┘ 改进:流隔离在 QUIC 层,一个流丢包不影响其他流 连接建立和 TLS 握手合并,0-RTT / 1-RTT 建连关键变化:可靠性和多路复用从内核的 TCP 搬到了用户态的 QUIC,只借用 UDP 做把包送过去这件事。这让协议可以快速演进,不必等操作系统更新。十、抓包验证理论要落地,抓一次包就全明白了。# 抓指定主机的 TCP 包,不解析域名,显示详细头部sudotcpdump-iany-nn-vvvhost203.0.113.8 and tcp port443# 抓包存文件,用 Wireshark 分析sudotcpdump-ieth0-wcapture.pcap-s0# 只看 TCP 握手(SYN 包)sudotcpdump-nntcp[tcpflags] tcp-syn ! 0# 查看路径 MTU(Linux)tracepath203.0.113.8# 测试 MTU:加 DF 位发大包,找到不分片能通过的最大值ping-Mdo-s1472203.0.113.8# 1472 8(ICMP) 20(IP) 1500Wireshark 里展开一个包,你会看到和前面图完全对应的层级结构:▼ Frame 123: 1514 bytes on wire ▼ Ethernet II, Src: aa:bb:.., Dst: cc:dd:.. Type: IPv4 (0x0800) ▼ Internet Protocol Version 4, Src: 10.0.0.2, Dst: 203.0.113.8 Total Length: 1500 Protocol: TCP (6) Time to Live: 64 ▼ Transmission Control Protocol, Src Port: 54321, Dst Port: 443 Sequence Number: 1 Flags: 0x018 (PSH, ACK) ▼ Transport Layer Security ...十一、要点归纳┌─ 机制 ────────────────────────────────────────────────┐ │ 每层把上层 PDU 整体视为数据,加自己的头部 │ │ 接收端逐层剥离,层与层之间只通过头部约定交互 │ └───────────────────────────────────────────────────────┘ ┌─ 寻址的三个层次 ──────────────────────────────────────┐ │ MAC → 相邻节点,逐跳重写 │ │ IP → 目标主机,端到端不变(NAT 除外) │ │ 端口 → 主机内进程,属于传输层 │ └───────────────────────────────────────────────────────┘ ┌─ 开销与 MTU ──────────────────────────────────────────┐ │ TCP/IP/以太网固定开销约 58 字节 │ │ MSS MTU - IP头 - TCP头 1460(标准以太网) │ │ 小包的头部占比极高 → 实时同步必须合并包 │ │ 避免 IP 分片;UDP 协议保守取 1200 字节 │ │ ICMP 被过滤导致 PMTUD 失效 → 小包通、大包死 │ └───────────────────────────────────────────────────────┘ ┌─ 应用层的责任 ────────────────────────────────────────┐ │ TCP 是字节流 → 必须自己定消息边界(长度前缀) │ │ 长度字段必须校验上限,否则是 OOM 漏洞 │ │ 统一用大端字节序 │ │ UDP 不粘包但不可靠 → 用 KCP/ENet,不要自己写 │ └───────────────────────────────────────────────────────┘
返回列表