子网划分与VLSM实战-从CIDR到无类别路由

子网划分与VLSM实战-从CIDR到无类别路由

🚙 快速导读
小猫咪

变长子网掩码(VLSM)与子网划分实战:从 CIDR 到无类别路由(子网掩码 可用主机数 路由聚合)

我们每天面对的问题是:手上一整块地址,怎么按部门、机房、链路各自的真实规模切出去,既不浪费也不冲突? 这就是子网划分(Subnetting)和变长子网掩码(Variable Length Subnet Mask, VLSM)要解决的事。

引言

CIDR 把"固定的 A/B/C 类"变成了"任意长度的网络前缀",这是一次观念上的松绑。但松绑之后,紧接着就出现了一个更实际的问题:前缀长度既然是自由的,那我在一个地址池里,能不能同时用好几个不同的前缀长度?

答案是能,而且这正是 VLSM 的价值所在。

本文从"掩码的二进制本质"开始,先讲清楚定长子网划分(FLSM)怎么把一个 /24 切整齐,再用一个真实的部门需求案例说明均分为什么必然浪费,最后给出 VLSM 的两条铁律、一套可复用的 Python 计算脚本,以及几个新手最容易踩的坑。

一、先把地基打牢:子网掩码的二进制本质

IPv4 地址是 32 位二进制数,我们平时看到的 192.168.10.100 只是它的点分十进制写法。而子网掩码,本质上就是一串"前面全是 1、后面全是 0"的 32 位二进制数。

/n 这个斜线记法,含义就是:前 n 位是网络前缀(网络号),剩下的 32 - n 位是主机号。

以 192.168.10.100/26 为例:

190.168.10.100  ->  11000000.10101000.00001010.01100100
/26 掩码        ->  11111111.11111111.11111111.11000000  (255.255.255.192)
                    |<------  网络前缀 26 位  ------>|<- 主机 6 位 ->|

主机位有 6 位,所以这个子网能容纳 2^6 = 64 个地址。但其中有两个地址不能分给设备:

  • 网络地址:主机位全 0,192.168.10.0,用来代表整个网络本身;
  • 广播地址:主机位全 1,192.168.10.63,发往它的包会送达子网内所有主机。

所以真正能用的地址是 192.168.10.1 到 192.168.10.62,共 62 个。这就是那条最经典的公式:

可用主机数 = 2^(32 - n) - 2

常用前缀速查表

前缀子网掩码地址块大小可用主机数典型用途
/24255.255.255.0256254一个标准部门网
/25255.255.255.12812812660 ~ 126 台设备
/26255.255.255.192646230 ~ 62 台设备
/27255.255.255.224323015 ~ 30 台设备
/28255.255.255.24016146 ~ 14 台设备
/29255.255.255.24886小型设备组、服务器带外管理
/30255.255.255.25242路由器/防火墙互联链路
/31255.255.255.25420(RFC 3021 点对点)PPP 拨号链路
/22255.255.252.010241022多部门聚合
/23255.255.254.0512510中型办公区

一个容易被忽略的校验:子网掩码必须是连续的 1。255.255.255.192 合法,因为 11000000;但 255.255.255.193(11000001)是非法掩码,设备会直接拒绝配置。掩码中间不能出现 "1-0-1" 这种断点。

二、定长子网划分(FLSM):先把一个 /24 切整齐

所谓定长(Fixed Length Subnet Mask, FLSM),就是所有子网用同一个前缀长度,大小完全一致。

需求:公司拿到 192.168.10.0/24,要分给 4 个部门,每部门约 50 台设备。

第一步,定前缀。 50 台设备要求 2^h - 2 >= 50,取 h = 6,则 n = 32 - 6 = 26。也就是说要从主机位借 26 - 24 = 2 位给网络,能切出 2^2 = 4 个子网 —— 刚好 4 个部门。

第二步,算块大小。 块大小 = 主机位能表示的量 = 2^6 = 64。也可以记成一条超好用的速算口诀:

块大小 = 256 - 掩码最后一个非 255 的字节
例:/26 -> 掩码 ...192 -> 256 - 192 = 64

第三步,数格子。 从网络地址开始,每 64 个地址切一刀:

子网网络地址可用地址范围广播地址可用数
子网 0192.168.10.0192.168.10.1 – .62192.168.10.6362
子网 1192.168.10.64192.168.10.65 – .126192.168.10.12762
子网 2192.168.10.128192.168.10.129 – .190192.168.10.19162
子网 3192.168.10.192192.168.10.193 – .254192.168.10.25562

4 个子网一共发出去 248 个可用地址,只"浪费"了 8 个(每个子网固定的网络地址 + 广播地址)。看起来很完美,对吧?

但现实中的需求从来不是均等的

部门实际设备数需要的网段规模
销售部100至少 /25(126 可用)
技术部50/26 足够(62 可用)
财务部20/27 足够(30 可用)
管理部10/28 足够(14 可用)
路由器互联2/30 足够(2 可用)

现在如果继续用 FLSM 均分,会发生什么?

  • 用 /26 均分:只有 4 个 /26,5 个需求直接装不下;
  • 用 /25 均分:只有 2 个 /25,更装不下;
  • 用 /24 切成 8 个 /27(每个 30 台):销售部 100 台必须拿 4 个 /27 拼起来,财务部、管理部、互联链路各占 1 个 —— 合计 4+2+1+1+1 = 9 个,依然不够,而且被切得七零八落。

问题的症结很清楚:均分意味着"规模小的部门被迫浪费,规模大的部门又不够用"。 销售部要 100 个地址,却只能按 128(/25)级别申请;管理部只要 10 个,却也要吃掉一个 32 地址的 /27,白白丢掉 20 个。

这正是 VLSM 登场的理由。

三、VLSM:按需分配,大的先来

VLSM 的核心思想只有一句话:在同一个地址池里,允许不同的子网使用不同的前缀长度。

同样一块 192.168.10.0/24,VLSM 会这样分配:

顺序部门需求选用前缀网络地址可用范围广播地址占用大小
1销售部100/25192.168.10.0.1 – .126.127128
2技术部50/26192.168.10.128.129 – .190.19164
3财务部20/27192.168.10.192.193 – .222.22332
4管理部10/28192.168.10.224.225 – .238.23916
5路由互联2/30192.168.10.240.241 – .242.2434
—预留扩展——192.168.10.244.244 – .254.25512

占用合计 128 + 64 + 32 + 16 + 4 = 244,剩下 12 个地址,还能再塞下三条 /30 互联链路。一块 /24,装下 5 个异构网段 + 3 条预留链路,几乎零浪费。

VLSM 的两条铁律

铁律一:从大到小分配。

必须先把需求最大的网段安顿好,再用剩下的边角料满足小需求。原因很直观 —— 大网段对起始位置的要求更苛刻。

反例:如果先给财务部一个 /27,从 .0 开始占用 192.168.10.0 – .31。这时销售部想要 /25,而 /25 的起始地址必须能被 128 整除,192.168.10.32 不满足条件,只能退到 192.168.10.128。结果是 .32 – .127 这 96 个地址被白白空掉,无法再组成任何 /25,只能被切碎成小块。顺序错了,代价就是 96 个地址。

铁律二:边界对齐。

一个子网的起始地址必须是它自身块大小的整数倍。

  • /26 的块大小是 64,所以起始地址必须是 0 / 64 / 128 / 192;
  • /27 的块大小是 32,起始地址必须是 0 / 32 / 64 / 96 / 128 / 160 / 192 / 224;
  • /30 的块大小是 4,起始地址必须是 4 的倍数。

回头看上面的分配表:.128 落在 /26 边界(128 ÷ 64 = 2 ✓),.192 落在 /27 边界(192 ÷ 32 = 6 ✓),.224 落在 /28 边界(224 ÷ 16 = 14 ✓),.240 落在 /30 边界(240 ÷ 4 = 60 ✓)。只要坚持"从大到小",后面的小块天然就会落在对齐位置上,这也是为什么这条规则能让 VLSM 变得好算。

手动算 VLSM 的三步法

  1. 算需求:对每个网段求 h,满足 2^h - 2 >= 需求台数,得到前缀 n = 32 - h;
  2. 排顺序:把所有需求按"前缀长度从小到大"(即网段从大到小)排序;
  3. 按序切:维护一个游标 cursor 指向下一个未分配地址,每分配一个块就往后推进块大小那么多,同时保证起始地址按块大小向上对齐。

四、用 Python 把这件事算清楚

手工算容易出错,标准库 ipaddress 就是为此而生的。以下代码均可直接复制运行。

4.1 看清单个网段的基本信息

import ipaddress

net = ipaddress.ip_network("192.168.10.0/26", strict=False)

print("网络地址 :", net.network_address)      # 192.168.10.0
print("广播地址 :", net.broadcast_address)    # 192.168.10.63
print("子网掩码 :", net.netmask)              # 255.255.255.192
print("地址总数 :", net.num_addresses)        # 64
print("可用主机 :", net.num_addresses - 2)    # 62

hosts = list(net.hosts())
print("首个可用 :", hosts[0])                 # 192.168.10.1
print("末个可用 :", hosts[-1])                # 192.168.10.62

注意 strict=False:如果写成 ip_network("192.168.10.100/26") 且不加这个参数,Python 会直接报错,因为它认为"你给了一个带主机位的地址,却声称它是网络地址"—— 这个报错本身就是在帮你守"边界对齐"这条规则。

4.2 定长划分:把一个 /24 切成 4 个 /26

import ipaddress

parent = ipaddress.ip_network("192.168.10.0/24")

for idx, sub in enumerate(parent.subnets(new_prefix=26)):
    hosts = list(sub.hosts())
    print(f"子网{idx} | {str(sub):<20} | 网关 {hosts[0]:<15} | "
          f"可用数 {len(hosts)} | 广播 {sub.broadcast_address}")

# 子网0 | 192.168.10.0/26      | 网关 192.168.10.1  | 可用数 62 | 广播 192.168.10.63
# 子网1 | 192.168.10.64/26     | 网关 192.168.10.65 | 可用数 62 | 广播 192.168.10.127
# 子网2 | 192.168.10.128/26    | 网关 192.168.10.129| 可用数 62 | 广播 192.168.10.191
# 子网3 | 192.168.10.192/26    | 网关 192.168.10.193| 可用数 62 | 广播 192.168.10.255

4.3 VLSM 自动分配脚本

把"从大到小 + 边界对齐"翻译成代码,只有十几行:

import ipaddress

def fit_prefix(need: int, max_prefix: int) -> int:
    """返回能容纳 need 台主机的'最长'前缀(即最省地址的子网)"""
    for prefix in range(32, max_prefix - 1, -1):
        if (1 << (32 - prefix)) - 2 >= need:
            return prefix
    raise ValueError(f"地址池太小,装不下 {need} 台主机")

def vlsm_allocate(pool: str, demands: list):
    net = ipaddress.ip_network(pool)
    # 铁律一:从大到小排序
    demands = sorted(demands, key=lambda x: -x[1])
    cursor = int(net.network_address)

    rows = []
    for name, need in demands:
        prefix = fit_prefix(need, net.prefixlen)
        block = 1 << (32 - prefix)              # 该子网的地址总数
        # 铁律二:起始地址按块大小向上对齐
        start = ((cursor + block - 1) // block) * block
        sub = ipaddress.ip_network((start, prefix))
        cursor = start + block                  # 游标推进

        hosts = list(sub.hosts())
        rows.append((name, need, str(sub), str(hosts[0]),
                     f"{hosts[0]} ~ {hosts[-1]}", len(hosts), block))

    for r in rows:
        print(f"{r[0]:<8} 需求{r[1]:>4}台  网段 {r[2]:<20} 网关 {r[3]:<15} "
              f"范围 {r[4]:<32} 可用 {r[5]:>4} 占用 {r[6]:>4}")

    rest = int(net.broadcast_address) + 1 - cursor
    print(f"\n剩余未分配地址:{rest} 个")
    if rest > 0:
        print(f"可再切 {rest // 4} 条 /30 互联链路")

# 需求不要求按顺序写,脚本内部会排序
vlsm_allocate("192.168.10.0/24", [
    ("管理部", 10),
    ("销售部", 100),
    ("路由互联", 2),
    ("技术部", 50),
    ("财务部", 20),
])

运行结果:

销售部   需求 100台  网段 192.168.10.0/25    网关 192.168.10.1   范围 192.168.10.1 ~ 192.168.10.126   可用  126 占用  128
技术部   需求  50台  网段 192.168.10.128/26  网关 192.168.10.129 范围 192.168.10.129 ~ 192.168.10.190 可用   62 占用   64
财务部   需求  20台  网段 192.168.10.192/27  网关 192.168.10.193 范围 192.168.10.193 ~ 192.168.10.222 可用   30 占用   32
管理部   需求  10台  网段 192.168.10.224/28  网关 192.168.10.225 范围 192.168.10.225 ~ 192.168.10.238 可用   14 占用   16
路由互联 需求   2台  网段 192.168.10.240/30  网关 192.168.10.241 范围 192.168.10.241 ~ 192.168.10.242 可用    2 占用    4

剩余未分配地址:12 个
可再切 3 条 /30 互联链路

和第三节手工算的表格完全一致 —— 网关取的都是各子网的第一个可用地址,这也是工程上最常见的约定。

4.4 反向操作:CIDR 路由聚合(超网)

子网划分是把大块切小(前缀变长);路由聚合是把连续的小块合并成一块大网段(前缀变短),目的是压缩路由表,这也是 CIDR 最原始的使命。

import ipaddress

nets = [ipaddress.ip_network(n) for n in [
    "192.168.0.0/24",
    "192.168.1.0/24",
    "192.168.2.0/24",
    "192.168.3.0/24",
]]

for agg in ipaddress.collapse_addresses(nets):
    print(agg)

# 192.168.0.0/22

四条 /24 通告在路由表里,可以被一条 /22 替代,路由表项直接减少到原来的四分之一。判断能不能聚合的方法也很简单:看网络位之后、前缀之外的那几位,能否被"归零"。 这里第三段 0/1/2/3 的后两位不同,往前借 2 位就能覆盖全部,所以是 /22。

# 判断两个网段是否为"兄弟网段"(可合并成上一级)
def is_sibling(a, b):
    a, b = ipaddress.ip_network(a), ipaddress.ip_network(b)
    return (a.prefixlen == b.prefixlen
            and a.supernet()[0] == b.supernet()[0])

print(is_sibling("192.168.0.0/24", "192.168.1.0/24"))   # True  -> 可聚合为 /23
print(is_sibling("192.168.0.0/24", "192.168.2.0/24"))   # False -> 不能直接合并

五、几个新手最容易踩的坑

#坑说明与正确做法
1忘了减 2可用地址 = 2^h - 2。/28 是 16 个地址、14 个可用,不是 16
2把网络地址配到设备上192.168.10.0/26 这个地址属于"网络本身",配到网卡上在多数系统会被拒绝
3以为 /31 不能用RFC 3021 允许点对点链路用 /31,两个地址都能用,省一半地址。但老设备/老系统可能不支持,跨厂商互联建议仍用 /30
4非连续掩码255.255.255.193 是非法掩码,掩码必须"前 1 后 0"不能有断点
5子网数量不是 2 的幂FLSM 下借 k 位只能切出 2^k 个子网。要 5 个网段就得借 3 位(切出 8 个),多出来的可以留作预留
6顺序错导致碎片VLSM 必须从大到小排,否则大网段无处对齐,产生无法利用的"空洞"
7混淆"划分"与"聚合"划分:前缀变长、子网数变多、路由表变长;聚合:前缀变短、覆盖网段变多、路由表变短。方向完全相反
8网关地址随手写团队内统一约定(如统一用各子网第一个可用地址),否则排障时极易配错下一跳
9只看掩码不看实际需求20 台设备用 /27(30 可用)比用 /26(62 可用)省一半地址,别图"整齐"而浪费

六、落地到设备:华为 VRP / eNSP 配置片段

理论算完,最终要敲到设备上。华为设备支持掩码位数写法,比点分十进制更省事:

# 三层交换机:每个 VLAN 一个网关地址(均为各子网首个可用地址)
interface Vlanif10
 ip address 192.168.10.1 25          # 销售部 /25,可用 .1 ~ .126
#
interface Vlanif20
 ip address 192.168.10.129 26        # 技术部 /26,可用 .129 ~ .190
#
interface Vlanif30
 ip address 192.168.10.193 27        # 财务部 /27,可用 .193 ~ .222
#
interface Vlanif40
 ip address 192.168.10.225 28        # 管理部 /28,可用 .225 ~ .238
#
# 路由器互联链路:/30 只放两个地址
interface GigabitEthernet0/0/1
 ip address 192.168.10.241 30

一个实用技巧:在 eNSP 里配完之后用 display ip interface brief 核对每个接口的掩码位数,再把子网边界贴在拓扑图旁边。排障时 90% 的"ping 不通",根因都是某一边的掩码写成了 /24 而另一边是 /25,导致双方对广播域的理解不一致。

七、结论

CIDR 解决的是"前缀长度可以自由选择",而 VLSM 把它推向了更实用的层次:同一个地址池里,不同子网可以有不同的前缀长度。

回顾全文,最需要记住的就两条铁律:

  1. 从大到小分配 —— 大块对边界更挑剔,必须先安顿;
  2. 边界对齐 —— 起始地址必须是自身块大小的整数倍。

再加上三个随手要用的算式:可用主机数 = 2^h - 2、块大小 = 256 - 掩码最后一个非 255 字节、子网数 = 2^借位数。

这套方法在 IPv4 时代支撑了近乎苛刻的地址利用率,在 IPv6 时代依然有用 —— 因为 IPv6 的 /64 前缀划分、/48 站点分配,本质上还是同一套"按需切分 + 边界对齐"的思维,只是位数从 32 位换成了 128 位。

参考文献

  1. RFC 4632 - Classless Inter-domain Routing (CIDR): The Internet Address Assignment and Aggregation Plan
  2. RFC 3021 - Using 31-Bit Prefixes on IPv4 Point-to-Point Links
  3. Wikipedia: Classless Inter-Domain Routing
  4. Wikipedia: Variable Length Subnet Mask
  5. Python 官方文档:ipaddress — IPv4/IPv6 操作库
  6. 参考文章:无类别域间路由(CIDR):理解IP网络和子网划分
  7. 参考文章:一文完全理解 IP 地址与子网掩码
「一张 9.92MB 的 PNG,让我给图库补了套缩略图」 2026-08-30

评论区