一个 4000 B IPv4 数据报遇到 1500 B 出口 MTU,可能被拆成三片,也可能被丢弃并触发错误反馈,关键在 DF、路径和端点处理。IPv4 提供尽力而为的数据报承载,地址、长度、TTL 与分片字段共同决定当前这一包怎样转发;可靠、有序和拥塞恢复则需要相应上层机制。
| 查阅问题 | 本篇入口 | 最容易算错的部分 |
| 头长和包长怎样区分 | 逐字段与校验 | IHL × 4、Total Length 含头 |
| 多条路由都匹配选哪条 | CIDR 与下一跳 | 最长前缀、策略与邻居 |
| 分片偏移为何是 185 | 完整分片算例 | IP 数据偏移以 8 B 为单位 |
| 收到最后一片能否交付 | 重组条件 | 总长度、缺口、重叠与超时 |
| 小包通、大包不通 | PMTUD | 大小相关现象与因果证据 |
| DSCP/ECN 怎样共存 | 服务与拥塞标记 | 六位 DSCP 和两位 ECN 独立 |
地址与路由:32 bit 地址不是直接的物理位置
CIDR 前缀将地址分为路由聚合所用的高位与剩余部分。192.0.2.130/26 所属网络为 192.0.2.128/26,掩码 255.255.255.192;在常规广播子网语义下地址范围到 192.0.2.191,其中网络/广播地址用途与普通主机不同。不能把“每子网一定减二”推广到 /31 点到点或 /32 主机路由。
在一张普通目的路由表中,匹配 198.51.100.0/24 的包优先使用它,而不是同样匹配的 0.0.0.0/0。策略路由、VRF 和多路径可以增加选择上下文,但不会让 ARP 代替路由决策:路由选出出口/下一跳后,才处理该链路的邻居映射。
地址不提供发送者认证;NAT 可改写地址及相关字段,也不会把 IPv4 变为可靠传输协议。32 bit 编号空间不等于全部可公开分配,历史分配池耗尽也不是所有区域、机构和用途在同一天失去任何地址。
基本头逐字段理解
| 字段 | 语义 | 阅读边界 |
| Version / IHL | IPv4;头长为 IHL×4 B | 最小 20 B,最大 60 B;不能小于 5 |
| Total Length | 当前这一 IP 包的头加数据 | 最大 65535 B,不含以太网填充/FCS |
| Identification | 为需要重组的数据报提供标识 | 不是所有 IPv4 包全球唯一序号 |
| Flags / Fragment Offset | 禁分片、更多分片和数据位置 | 偏移不从以太网头或 IP 头开始 |
| TTL | 限制转发生命周期 | 路由转发至少递减,不能靠初始值精确确定系统 |
| Protocol | 指示上层,如 TCP 6、UDP 17 | 不是端口号 |
| Header Checksum | 当前 IPv4 头的校验 | 不覆盖 IP 载荷,也不认证来源 |
解析时先确认实际可用字节足够,再校验版本、IHL 和 Total Length 之间的关系;从以太网收到额外填充不能把它算进 IP 数据。IP Options 有各自格式与复制属性,不能假定每片都简单复制一份完全相同的头。RFC 791:IPv4 格式与分片
TTL 与头校验为什么每跳相关
路由器转发时更新 TTL,头部发生变化,校验和也要更新;源/目的在普通非 NAT 转发中保持不变,不意味着整个 IP 头不变。TTL 耗尽时丢弃,并在协议允许和资源/策略条件下生成 ICMP Time Exceeded;错误反馈有例外与限速,不能要求每次丢弃必定返回可见 ICMP。
IPv4 头校验和对 16 bit 字进行一补和,再取反,进位要折回低 16 bit。验证完整头时,一补和应为全 1,等价的“取反后结果”为零。它只保护 IPv4 头;TCP/UDP 校验另有伪首部与载荷覆盖规则,IPv4 UDP 还存在零校验和的特定语义,不能说所有上层都必定提供同样保障。
末尾代码会构造完整头,验证校验和,再把 TTL 从 64 改为 63,确认旧校验不再成立并重算。这比只记“路由器每跳减一”更能说明字段之间的依赖。
TTL 只减一,为什么旧校验就失效?
把 TTL 与 Protocol 组成的16位字标成金色,把校验字段标成蓝色。两者的变化在同一份头部中发生;下方保留旧值,便于看见重算修复了什么。载荷开关刻意不改变头,展示校验覆盖范围的边界。
正在呈现知识画面
TTL 只减一,为什么旧校验就失效?IPv4 头校验覆盖头字段。TTL 改变会破坏旧校验,载荷改变却不会;通过头校验不能证明数据或发送者可信。
{"id":"ipv4-checksum-dependency","title":"TTL 只减一,为什么旧校验就失效?","summary":"IPv4 头校验覆盖头字段。TTL 改变会破坏旧校验,载荷改变却不会;通过头校验不能证明数据或发送者可信。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"ttl\" data-i18n=\"ttl\"></label><output for=\"ttl\" id=\"ttl-value\"></output><input id=\"ttl\" type=\"range\" min=\"1\" max=\"64\" value=\"63\" step=\"1\"></div><label class=\"check\"><input type=\"checkbox\" id=\"payload\"><span data-i18n=\"payload\"></span></label><section class=\"panel\"><h3 data-i18n=\"header\"></h3><div id=\"header\"></div></section><div class=\"metrics\"><div><span data-i18n=\"original\"></span><strong id=\"original\"></strong></div><div><span data-i18n=\"test\"></span><strong id=\"test\"></strong></div><div><span data-i18n=\"new\"></span><strong id=\"new\"></strong></div><div><span data-i18n=\"valid\"></span><strong id=\"valid\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#header{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px}.metrics strong{font-size:19px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}const initial=[0x4500,28,0x1234,0,0x4011,0,0xc000,0x020a,0xc633,0x6414];function checksum(words){let sum=words.reduce((a,b)=>a+b,0);while(sum>>>16)sum=(sum&65535)+(sum>>>16);return(~sum)&65535}const old=checksum(initial),hex=n=>'0x'+n.toString(16).padStart(4,'0');function draw(){const h=[...initial],ttl=+$('#ttl').value;h[4]=(ttl<<8)|17;h[5]=old;const test=checksum(h);h[5]=0;const value=checksum(h);h[5]=value;cells('header',h.map((v,i)=>({text:hex(v),cls:i===4?'gold':i===5?'on':''})));set('ttl-value',ttl);set('original',hex(old));set('test',hex(test)+(test===0?' ✓':' ✗'));set('new',hex(value));set('valid',hex(checksum(h))+' ✓');set('equation','payload bit = '+Number($('#payload').checked)+'\\nheader only: checksum = 0 means valid');$('#test').dataset.value=test;$('#new').dataset.value=value;}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"header":"头部十个16位字","heading":"TTL 只减一,为什么旧校验就失效?","intro":"修改 TTL,比较旧校验、重算校验与载荷改变。这里实际执行16位一补校验。","new":"重算后的校验值","original":"原头校验值","payload":"翻转载荷中的一位","reset":"重置","scope":"20字节无选项IPv4头,原TTL64;只对内存中的报文字节计算。载荷不是完整UDP消息,不模拟上层校验或真实转发。","scopeLabel":"模型范围与约定","test":"保留旧校验时的验证值","ttl":"当前 TTL","valid":"重算后的验证值"}}
分片:按数据长度对齐,而非按整片长度对齐
设原数据报 Total Length=4000、IHL=5,无选项,则 IP 数据为 3980 B。出口 MTU=1500,每片至少保留 20 B 头;非末片的数据长度需为 8 B 整数倍,最大取 floor((1500−20)/8)×8=1480 B。
| 分片 | 原数据范围 | 数据长度 | Offset 字段 | MF | 本片 Total Length |
| 1 | 0~1479 | 1480 | 0 | 1 | 1500 |
| 2 | 1480~2959 | 1480 | 185 | 1 | 1500 |
| 3 | 2960~3979 | 1020 | 370 | 0 | 1040 |
1480/8=185、2960/8=370。非末片是数据长度按 8 对齐,1500 B 的整片总长本身并非 8 的整数倍。每片都有自己的 Total Length、偏移、MF 和校验和,Identification 则用于把属于同一数据报的片联系起来;TCP/UDP 起始头只位于原始数据开头,不会在每个分片复制一次。
DF 是 16 bit Flags/Offset 字的 0x4000,MF 是 0x2000,偏移掩码为 0x1fff;这避免把图上从左数的标志位编号误当作最低位编号。设置 DF 不会让数据自动变小,出口放不下时通常需要丢弃并给出相应反馈。
Identification 的约束需结合源、目的、协议和重组时间窗。RFC 6864 对不分片的 atomic datagram 放宽了 ID 使用要求,因此“每个 IP 包必须有唯一 ID”不成立;高包速率还会让有限 ID 空间与重组更值得关注。RFC 6864:IPv4 ID 语义
重组要凑齐区间,而非只等最后一片
收到末片 MF=0 后,可由 8×Offset + 本片数据长度 得到原始 IP 数据总长度:本例为 370×8+1020=3980 B,再加原始首片头长得到 4000 B。不能把 Offset 原值直接加到末片 Total Length,更不能把末片的 IP 头再次计入数据范围。
知道总长度还不够,前面的区间必须没有缺口,并通过相应重叠、边界和资源检查;片可乱序到达。普通 IP 转发路径不需要每跳重组,但防火墙、NAT 等中间处理可能为了其功能重组。超时或资源限制导致重组失败后,IP 层本身不会请求补发某一片,上层是否以及怎样重传取决于协议。
重组超时由实现及配置决定,不是全网统一常数。Linux 7.2 的 IP_FRAG_TIME 为 30×HZ,并用于初始化可配置的 ipfrag_time;这仍只是该实现的基准,不代表实际网络所有设备。碎片化也使中间设备处理更复杂,现代应用通常更希望通过合理分包避免它。
末片到了,为什么仍不能交付?
先递送最后一片,再补中间分片,观察“知道多长”与“已经凑齐”的差别。每个按钮上的 Offset 是报文字段,区间图上的位置已经乘过8;重复同一分片只复用已有覆盖,不能让已收字节数虚增。
C=8⌊8MTU−20⌋,Ldata=8Offsetlast+Llast
正在呈现知识画面
末片到了,为什么仍不能交付?MF=0 只揭示终点;重组还要求从零到终点没有缺口。Offset 的单位是8字节,重复递送不会补上别处的洞。
{"id":"ipv4-fragment-coverage","title":"末片到了,为什么仍不能交付?","summary":"MF=0 只揭示终点;重组还要求从零到终点没有缺口。Offset 的单位是8字节,重复递送不会补上别处的洞。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mtu\"><button data-mode=\"576\" data-i18n=\"m576\"></button><button data-mode=\"1280\" data-i18n=\"m1280\"></button><button data-mode=\"1500\" data-i18n=\"m1500\"></button></div><div class=\"fragment-area\"><div id=\"fragments\" class=\"choices\"></div><p id=\"fragment-info\"></p></div><div class=\"actions\"><button id=\"duplicate\" data-i18n=\"duplicate\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div id=\"ranges\" class=\"coverage\"></div><div class=\"legend\"><span data-i18n=\"receivedLegend\"></span><span class=\"second\" data-i18n=\"missingLegend\"></span></div><div class=\"metrics\"><div><span data-i18n=\"known\"></span><strong id=\"known\"></strong></div><div><span data-i18n=\"received\"></span><strong id=\"received\"></strong></div><div><span data-i18n=\"ready\"></span><strong id=\"ready\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.fragment-area{position:relative}#fragment-info{position:absolute;inset:auto 0 0;margin:0;padding:16px 8px;height:96px;display:flex;align-items:center;font-size:13px;color:var(--muted)}#fragments{grid-template-columns:repeat(4,minmax(0,1fr))}#fragments button{min-height:96px;white-space:pre-line;font:12px/1.5 var(--viz-font-ui)}.coverage{display:flex;gap:2px;height:42px;margin:14px 0}.coverage i{min-width:0;background:var(--second-soft);border-bottom:3px solid var(--second);font:12px/39px var(--viz-font-ui);text-align:center;font-style:normal}.coverage i.on{background:var(--accent-soft);border-color:var(--accent)}.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let mtu=1500,pieces=[],received=new Set,last=null;function reset(){const chunk=Math.floor((mtu-20)/8)*8;pieces=[];for(let start=0;start<3980;start+=chunk)pieces.push({start,end:Math.min(start+chunk,3980)});received=new Set;last=null;draw()}function draw(){const bytes=[...received].reduce((s,i)=>s+pieces[i].end-pieces[i].start,0),known=received.has(pieces.length-1),ready=bytes===3980;const box=$('#fragments');if(!box.children.length)for(let i=0;i<8;i++){const b=document.createElement('button');b.dataset.fragment=i;b.onclick=()=>{received.add(i);last=i;draw()};box.append(b)}[...box.children].forEach((b,i)=>{const p=pieces[i];b.disabled=!p||received.has(i);b.style.visibility=p?'visible':'hidden';b.textContent=p?'#'+(i+1)+' '+(received.has(i)?'✓':'→')+'\\nOffset '+p.start/8+'\\nMF '+Number(i<pieces.length-1):'·'});$('#ranges').replaceChildren(...pieces.map((p,i)=>{const e=document.createElement('i');e.style.flex=String(p.end-p.start);e.className=received.has(i)?'on':'';e.textContent=p.end-p.start>=398?'#'+(i+1):'';e.title='['+p.start+','+p.end+')';return e;}));set('fragment-info',t('splitSummary').replace('{count}',pieces.length).replace('{chunk}',Math.floor((mtu-20)/8)*8));$('#fragment-info').style.visibility=pieces.length<=4?'visible':'hidden';set('known',known?3980:t('unknown'));set('received',bytes);set('ready',t(ready?'yes':'no'));set('equation','chunk = floor(('+mtu+' − 20)/8)×8 = '+Math.floor((mtu-20)/8)*8+'\\nend = Offset×8 + fragment data\\ncoverage = '+bytes+' / 3980');$('#duplicate').disabled=last===null;$('#ready').dataset.ready=String(ready);$('#known').dataset.known=String(known);$('#ranges').dataset.pieces=JSON.stringify(pieces);pressed(String(mtu));}$('#duplicate').onclick=()=>{received.add(last);draw()};$('#reset').onclick=reset;document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mtu=+b.dataset.mode;reset()});reset();","audio":false,"strings":{"deliver":"递送分片","duplicate":"重复最后一次递送","heading":"末片到了,为什么仍不能交付?","intro":"修改 MTU,任选分片递送顺序。总长度与已收到的区间分别保留。","known":"数据总长 / B","m1280":"1280","m1500":"1500","m576":"576","missingLegend":"仍有缺口","mtu":"出口 MTU(B)","no":"尚不可以","ready":"能否重组交付","received":"已收数据 / B","receivedLegend":"已经收到","reset":"重置","scope":"固定4000 B无选项IPv4数据报,20 B头,3980 B数据;只提供合法且不重叠的分片,忽略超时和资源回收。改MTU开始一次新重组。","scopeLabel":"模型范围与约定","splitSummary":"{count} 个分片,每片数据上限 {chunk} B。","unknown":"未知","yes":"可以"}}
PMTUD:错误反馈帮助端点调整包大小
经典 IPv4 PMTUD 让端点发送 DF 包,当中间出口 MTU 不够时,路由器丢弃并按条件返回 ICMP Destination Unreachable Type 3 Code 4,携带 MTU 等信息。端点据此更新路径估计,传输/应用层调整后续分包;不是把原包的 DF 自动清零再继续发送。RFC 1191
如果必要 ICMP 被过滤,端点可能持续重发过大的包,形成黑洞。但“小 ping 通、大 ping 不通”只提示大小相关问题,还可能是过滤、限速或不同路径。验证需要控制探测大小、协议和方向,并观察出入口或 ICMP 证据,不能单凭一次探测下结论。
TCP MSS 通告约束对端发送的 TCP 数据规模,并不等于整包 MTU,也不覆盖 UDP。MSS clamping 可缓解某些 TCP 路径问题,但所依据 PMTU 可能不完整,路径还可能变化,不能作为所有 ICMP 被屏蔽场景的必然修复。PLPMTUD/DPLPMTUD 利用分包层的探测与确认机制帮助发现可用大小,其状态机仍要正确实现。RFC 8899:数据报传输的 DPLPMTUD
DSCP:标记需由网络策略兑现
IPv4 第二个字节高六位为 DSCP、低两位为 ECN,ECN 不是“DSCP 的低两位”。DSCP 选择逐跳转发行为,设备是否信任、重标记、整形或映射到哪个队列,由网络策略决定。RFC 2474:DS 字段
CS 类、AF 类与 EF 表达不同服务设计。AF 的不同丢弃优先级是在相应类中的处理维度,不能把全部代码点按数字大小排成统一高低;EF 常用代码点为十进制 46,但并不等于任意设备上的“无限最高优先级”。低延迟行为需要配置服务速率、准入和调度约束。RFC 3246:EF PHB
例如 DSCP=46、ECN=ECT(0)=2,完整字节为 (46<<2)|2=0xBA。修改 DSCP 时应保留所需 ECN 位,不能随手覆盖整个字节破坏拥塞信号。
ECN:先有协商,再解释拥塞反馈
| ECN 两位 | 含义 |
| 00 | Not-ECT |
| 10 | ECT(0) |
| 01 | ECT(1) |
| 11 | CE,经历拥塞 |
以 RFC 3168 的经典 TCP ECN 为例,SYN 用 TCP ECE/CWR 标志表达协商,SYN-ACK 用相应响应,而不是通过给初始 SYN 的 IP 头写 ECT 来完成协商。之后合适数据包可标 ECT,支持 ECN 的网络节点可在相应拥塞条件下标 CE,接收方经 ACK 的 ECE 回报,发送方执行拥塞响应并用 CWR 表示已响应。RFC 3168:经典 ECN
CE 不保证从此绝不丢包,队列、严重拥塞和不支持路径仍可丢弃。发送方的窗口响应取决于所用拥塞算法,不能统一称为每次减半;DCTCP 等有不同反馈处理。后续规范也放宽了部分实验限制,因此应把上述过程明确视为经典基线,不拿它覆盖所有现代 ECN 变体。RFC 8311:ECN 实验边界更新
本地构造、分片与校验
import struct
from ipaddress import IPv4Address, IPv4Network
def checksum(data):
assert len(data)%2 == 0
total = sum(struct.unpack('!%dH' % (len(data)//2), data))
while total >> 16:
total = (total&0xffff)+(total>>16)
return (~total)&0xffff
header = bytearray(struct.pack('!BBHHHBBH4s4s',
0x45, (46<<2)|2, 4000, 0x1234, 0, 64, 17, 0,
IPv4Address('192.0.2.10').packed, IPv4Address('198.51.100.20').packed))
header[10:12] = struct.pack('!H', checksum(header))
assert checksum(header) == 0
assert header[1] == 0xBA
header[8] = 63
assert checksum(header) != 0
header[10:12] = bytes(2)
header[10:12] = struct.pack('!H', checksum(header))
assert checksum(header) == 0
payload = bytes(i%251 for i in range(3980))
chunk = ((1500-20)//8)*8
fragments = []
for start in range(0,len(payload),chunk):
data = payload[start:start+chunk]
fragments.append((start//8, int(start+len(data)<len(payload)), len(data)+20, data))
assert [f[:3] for f in fragments] == [(0,1,1500),(185,1,1500),(370,0,1040)]
assert b''.join(f[3] for f in fragments) == payload
assert fragments[-1][0]*8 + len(fragments[-1][3]) + 20 == 4000
assert str(IPv4Network('192.0.2.130/26', strict=False)) == '192.0.2.128/26'
print('IPv4 头校验、TTL 更新、DS 字节、分片区间与 CIDR 检查通过')
这是无选项教学报文的本地验证,未发送探测流量、修改 MTU/MSS 或调整拥塞控制配置。
继续查阅