变长子网掩码(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
常用前缀速查表
| 前缀 | 子网掩码 | 地址块大小 | 可用主机数 | 典型用途 |
|---|---|---|---|---|
| /24 | 255.255.255.0 | 256 | 254 | 一个标准部门网 |
| /25 | 255.255.255.128 | 128 | 126 | 60 ~ 126 台设备 |
| /26 | 255.255.255.192 | 64 | 62 | 30 ~ 62 台设备 |
| /27 | 255.255.255.224 | 32 | 30 | 15 ~ 30 台设备 |
| /28 | 255.255.255.240 | 16 | 14 | 6 ~ 14 台设备 |
| /29 | 255.255.255.248 | 8 | 6 | 小型设备组、服务器带外管理 |
| /30 | 255.255.255.252 | 4 | 2 | 路由器/防火墙互联链路 |
| /31 | 255.255.255.254 | 2 | 0(RFC 3021 点对点) | PPP 拨号链路 |
| /22 | 255.255.252.0 | 1024 | 1022 | 多部门聚合 |
| /23 | 255.255.254.0 | 512 | 510 | 中型办公区 |
一个容易被忽略的校验:子网掩码必须是连续的 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 个地址切一刀:
| 子网 | 网络地址 | 可用地址范围 | 广播地址 | 可用数 |
|---|---|---|---|---|
| 子网 0 | 192.168.10.0 | 192.168.10.1 – .62 | 192.168.10.63 | 62 |
| 子网 1 | 192.168.10.64 | 192.168.10.65 – .126 | 192.168.10.127 | 62 |
| 子网 2 | 192.168.10.128 | 192.168.10.129 – .190 | 192.168.10.191 | 62 |
| 子网 3 | 192.168.10.192 | 192.168.10.193 – .254 | 192.168.10.255 | 62 |
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 | /25 | 192.168.10.0 | .1 – .126 | .127 | 128 |
| 2 | 技术部 | 50 | /26 | 192.168.10.128 | .129 – .190 | .191 | 64 |
| 3 | 财务部 | 20 | /27 | 192.168.10.192 | .193 – .222 | .223 | 32 |
| 4 | 管理部 | 10 | /28 | 192.168.10.224 | .225 – .238 | .239 | 16 |
| 5 | 路由互联 | 2 | /30 | 192.168.10.240 | .241 – .242 | .243 | 4 |
| — | 预留扩展 | — | — | 192.168.10.244 | .244 – .254 | .255 | 12 |
占用合计 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 的三步法
- 算需求:对每个网段求
h,满足2^h - 2 >= 需求台数,得到前缀n = 32 - h; - 排顺序:把所有需求按"前缀长度从小到大"(即网段从大到小)排序;
- 按序切:维护一个游标
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 把它推向了更实用的层次:同一个地址池里,不同子网可以有不同的前缀长度。
回顾全文,最需要记住的就两条铁律:
- 从大到小分配 —— 大块对边界更挑剔,必须先安顿;
- 边界对齐 —— 起始地址必须是自身块大小的整数倍。
再加上三个随手要用的算式:可用主机数 = 2^h - 2、块大小 = 256 - 掩码最后一个非 255 字节、子网数 = 2^借位数。
这套方法在 IPv4 时代支撑了近乎苛刻的地址利用率,在 IPv6 时代依然有用 —— 因为 IPv6 的 /64 前缀划分、/48 站点分配,本质上还是同一套"按需切分 + 边界对齐"的思维,只是位数从 32 位换成了 128 位。
参考文献
- RFC 4632 - Classless Inter-domain Routing (CIDR): The Internet Address Assignment and Aggregation Plan
- RFC 3021 - Using 31-Bit Prefixes on IPv4 Point-to-Point Links
- Wikipedia: Classless Inter-Domain Routing
- Wikipedia: Variable Length Subnet Mask
- Python 官方文档:ipaddress — IPv4/IPv6 操作库
- 参考文章:无类别域间路由(CIDR):理解IP网络和子网划分
- 参考文章:一文完全理解 IP 地址与子网掩码