深蓝黑背景封面。左侧是二进制对齐推导图:两行地址并排展示第三字节的二进制展开,第一行「10.1.8.0/24」的 8 展开为 0000100-0,第二行「10.1.9.0/24」的 9 展开为 0000100-1,前七位「0000100」以青色块高亮标注「公共前缀 23 位 = 16 + 7」,最后一位 0 与 1 以紫色块高亮标注「唯一不同的一位」;向下箭头指向结果卡「汇总网段:10.1.8.0/23」,注「覆盖 10.1.8.0 到 10.1.9.255,恰好两个 /24」。底部一行灰色注「能直接合并的前提:低位网段第三字节是偶数,正好落在 /23 边界上」。右侧大标题「10.1.8.0/24 与 10.1.9.0/24 能汇总成哪个网段」,副标题「二进制对齐,找最长公共前缀」。左上标注「笔试真题 · 网络」,右上圆角胶囊标注「Day 02 · 11-19」。 图 1:本文封面,第三字节的二进制对齐推导。

先说明属性:这是一篇历史笔试真题复盘,题目出自本地历年笔试题集,与任何公司的当前招聘无关。CIDR 路由聚合是网络笔试的稳定考点,四个选项里往往既有「看着对」的 10.1.0.0/16,也有「方向反了」的 10.1.10.0/24。这类题三分钟能做完,前提是你手里有一把「二进制对齐」的尺子。顺带一提,多可用区部署时的内网网段规划、安全组规则配置,靠的也是同一套聚合计算。

先给结论,再给推导。

正确答案是 10.1.8.0/23。推导只需要盯住第三字节,因为前两个字节 10.1 完全相同。8 的二进制是 00001000,9 是 00001001——前七位 0000100 一模一样,只有最后一位 0 与 1 不同。也就是说两个地址的公共前缀是 16 + 7 = 23 位,把第 24 位让出来作主机位,得到汇总网段 10.1.8.0/23。验算覆盖范围:/23 的网络位 23 位、主机位 9 位,每个网段含 512 个地址,10.1.8.0/23 正好覆盖 10.1.8.0 到 10.1.9.255,即原来的两个 /24,一个不多一个不少。

为什么不能随手写成 10.1.9.0/23?

这是本题的暗坑。两个 /24 能合并成 /23 的前提是低位网段的第三字节落在 /23 边界上,也就是必须是偶数。/23 的网段在第三字节上以 2 为粒度对齐:8.0/23 覆盖 8 和 9,10.0/23 覆盖 10 和 11。9 不是 /23 边界,「10.1.9.0/23」这个写法本身就是非法网段(主机位没清零),而且若硬按 9 起始算,覆盖的是 9 和 10,把别人的网段包进来了。所以聚合的第一条法则:先对齐再合并——能直接合成 /23 的两个 /24,第三字节必须是 2k 与 2k+1。

换一对数字,感受方向的变化。

若题目改成 10.1.9.0/24 和 10.1.10.0/24 呢?9 是 00001001,10 是 00001010,公共前缀只剩 6 位,总前缀 22 位,只能汇总成 10.1.8.0/22——而 /22 覆盖 8 到 11 四个 /24,多包进了两个本来不在题目里的网段。路由聚合里这叫「宁大勿错」:对不齐就只能放长到公共前缀对应的掩码,同时必须确认多覆盖的网段可以接受。笔试里如果选项只有 /22 而没有 /23,答案就得选它;但本题 /23 恰好对齐,选 /22 就是白扔精度。

用代码把位运算固化下来。

下面这段程序实现通用聚合:异或找不同位、算最长公共前缀、按掩码清零主机位,并对两组网段分别验算(Node.js 22.15.0,node cidr.js):

function toInt(ip) {
  return ip.split('.').reduce((acc, o) => (acc << 8) + Number(o), 0) >>> 0;
}
function aggregate(ip1, ip2) {
  const a = toInt(ip1), b = toInt(ip2);
  const x = (a ^ b) >>> 0;                       // 不同的位
  const diffBits = x === 0 ? 0 : 32 - Math.clz32(x);
  const prefix = 32 - diffBits;                  // 最长公共前缀
  const mask = prefix === 0 ? 0 : (0xFFFFFFFF << (32 - prefix)) >>> 0;
  return [a & mask, prefix];
}
console.log('8 的二进制 :', (8).toString(2).padStart(8, '0'));
console.log('9 的二进制 :', (9).toString(2).padStart(8, '0'));
console.log('10 的二进制:', (10).toString(2).padStart(8, '0'));
console.log(aggregate('10.1.8.0', '10.1.9.0'));  // 期望 /23
console.log(aggregate('10.1.9.0', '10.1.10.0')); // 只能 /22

实测输出:

8 的二进制 : 00001000
9 的二进制 : 00001001
10 的二进制: 00001010
[ 167802880, 23 ]    // 即 10.1.8.0/23,第三字节范围 8~9
[ 167802880, 22 ]    // 即 10.1.8.0/22,覆盖 8~11,多包进两个网段

167802880 换回点分十进制正是 10.1.8.0,与手工推导一致;9 与 10 的组合确实只能给到 /22。

深蓝黑背景概念图。上半部是第三字节八位的对齐示意图:横排八个格子标注位序号 7 到 0,三行分别展示 8(00001000)、9(00001001)、10(00001010)的二进制;8 与 9 的前七位公共区用青色括号标出「前缀 23 位」,8 与 10 的前六位公共区用紫色括号标出「前缀只有 22 位」。中部左右对比两张结果卡:左卡青色描边「8.0/24 + 9.0/24 → 10.1.8.0/23」,注「低位网段按 /23 边界对齐,聚合不引入任何多余地址」;右卡灰色描边「9.0/24 + 10.0/24 → 只能 10.1.8.0/22」,注「/22 覆盖 8~11 四个 /24,多包进两个网段,不能直接合并」。下方两张法则卡:法则一「先对齐再合并」——能合成 /23 的两个 /24,第三字节必须是 2k 与 2k+1;法则二「宁大勿错」——对不齐就放长到公共前缀对应的掩码,并确认多覆盖的网段可接受。底部总结条:聚合的方向与子网划分相反,一个自底向上合并、一个自顶向下切分,考的都是同一套位对齐功夫。 图 2:位对齐法则与「9+10」的边界反例对照。

顺手排掉另外两个干扰项。

10.1.0.0/16 确实包得住两个网段,但它覆盖 256 个 /24,把没出现在题目里的 254 个网段也圈了进去,聚合精度全丢;10.0.0.0/8 同理,包得更狠。至于 10.1.10.0/24,它与任一原网段都不存在包含关系,属于凑数项。选择题里「包得住」从来不是标准,「恰好覆盖的最长前缀」才是。

聚合与划分是同一套功夫的两个方向。

子网划分是自顶向下:一个大网段切成若干小网段,主机位变多;路由聚合是自底向上:相邻小网段合成超网,前缀变短。考场上的统一动作都是「把变化的那个字节写成二进制,数公共前缀,检查边界对齐」。常见失分有三:忘了低位网段必须落在目标掩码边界上;把「能覆盖」当成「恰好覆盖」;以及把 10.1.0.0/16 这种「肯定包得住但精度全丢」的选项当答案。你在网段规划或安全组配置里算过聚合吗?有没有被边界对齐坑过?欢迎分享。