Appearance
实现 IpSubnet 工具
分类:developer | 标签:IP、子网、CIDR 计算网络地址、广播地址、可用主机数与范围
2.1 功能说明
计算网络地址、广播地址、可用主机数与范围
2.1.1 使用指南
功能说明
输入 IPv4 地址与 CIDR(如 192.168.1.10/24)或子网掩码,计算网络地址、广播地址、可用主机范围与数量。
IPv6 计算
- 压缩与展开:按 RFC 5952 规范输出压缩格式(小写、去前导零、最长的连续全零段用
::替代;仅一个全零分组时不压缩),同时给出 8 组 4 位十六进制的完整形式。 - 前缀计算:给定前缀长度(0-128,如
/64),用 128 位大整数运算得出网络地址、 地址段的首末地址与地址总数。 - IPv4 映射:IPv4 与
::ffff:a.b.c.d(RFC 4291)互相转换, 也支持解析64:ff9b::10.0.0.1这类内嵌 IPv4 的写法。
输入支持带方括号([2001:db8::1])、带 zone id(fe80::1%eth0)以及 地址/前缀 连写。
使用场景
网络规划、容器网段划分、VLAN/ACL 配置核对、IPv6 地址规范化与双栈环境排障。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
ipToInt()intToIp()calc()clear()parseIPv4Parts()parseIPv6()expandV6()compressV6()bigToV6()isMappedV4()embeddedV4()v6Kind()calc6()clear6()toMapped()fromMapped()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { ref } from 'vue'
import ToolPageShell from '@/components/ToolPageShell.vue'
const activeTab = ref('v4')
const ip = ref('')
const cidr = ref('')
const error = ref('')
const result = ref(null)
function ipToInt(a) {
const p = a.split('.')
if (p.length !== 4) return null
let n = 0
for (let i = 0; i < 4; i++) {
const o = Number(p[i])
if (!Number.isInteger(o) || o < 0 || o > 255) return null
n = (n << 8) | o
}
return n >>> 0
}
function intToIp(n) {
return [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255].join('.')
}
function calc() {
error.value = result.value = null
const addr = ip.value.trim()
const ai = ipToInt(addr)
if (ai === null) { error.value = 'IPv4 地址格式不正确'; return }
let bits
const c = cidr.value.trim()
if (/^\d{1,2}$/.test(c)) {
bits = Number(c)
if (bits < 0 || bits > 32) { error.value = 'CIDR 前缀必须在 0-32 之间'; return }
} else {
const mi = ipToInt(c)
if (mi === null) { error.value = '子网掩码格式不正确'; return }
bits = 0
let m = mi
while (m & 0x80000000) { bits++; m <<= 1 }
if ((mi & ((1 << (32 - bits)) - 1)) !== 0) { error.value = '子网掩码不是合法的连续 1'; return }
}
const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0
const wildcard = (~mask) >>> 0
const network = (ai & mask) >>> 0
const broadcast = (network | wildcard) >>> 0
const hosts = bits >= 31 ? 0 : Math.pow(2, 32 - bits) - 2
result.value = {
network: intToIp(network),
broadcast: intToIp(broadcast),
mask: intToIp(mask),
wildcard: intToIp(wildcard),
hosts: String(hosts),
first: bits >= 31 ? '-' : intToIp(network + 1),
last: bits >= 31 ? '-' : intToIp(broadcast - 1)
}
}
function clear() { ip.value = cidr.value = error.value = result.value = null }
/* ================= IPv6 计算 ================= */
const ip6 = ref('')
const prefix6 = ref('64')
const error6 = ref('')
const result6 = ref(null)
const V6_SAMPLES = [
'2001:db8:abcd:1234::1/64',
'fe80::204:61ff:fe9d:f156/10',
'::ffff:192.168.1.10',
'2001:0db8:0000:0000:0000:ff00:0042:8329/32'
]
function parseIPv4Parts(s) {
const p = s.split('.')
if (p.length !== 4) return null
const out = []
for (const seg of p) {
if (!/^\d{1,3}$/.test(seg)) return null
const n = Number(seg)
if (n > 255) return null
out.push(n)
}
return out
}
// 解析 IPv6 为 8 个 16 位分组;失败时返回 { error }
function parseIPv6(input) {
let s = String(input ?? '').trim()
if (!s) return { error: 'IPv6 地址不能为空' }
s = s.replace(/^\[/, '').replace(/\]$/, '')
const pct = s.indexOf('%')
if (pct !== -1) s = s.slice(0, pct) // 去掉 zone id,如 %eth0
if (!s.includes(':')) return { error: 'IPv6 地址至少需要包含一个冒号' }
if ((s.match(/::/g) || []).length > 1) return { error: '"::" 在一个地址中只能出现一次' }
if (s.includes(':::')) return { error: '存在非法的连续冒号 ":::"' }
// 处理内嵌 IPv4,如 ::ffff:192.168.1.1
let tailV4 = null
const lastColon = s.lastIndexOf(':')
const tail = s.slice(lastColon + 1)
if (tail.includes('.')) {
const v4 = parseIPv4Parts(tail)
if (!v4) return { error: '内嵌的 IPv4 部分格式不正确:' + tail }
tailV4 = [(v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3]]
s = s.slice(0, lastColon + 1) + '0:0'
}
const dbl = s.indexOf('::')
let head, tailArr
if (dbl === -1) {
head = s.split(':')
tailArr = []
} else {
const h = s.slice(0, dbl)
const t = s.slice(dbl + 2)
head = h === '' ? [] : h.split(':')
tailArr = t === '' ? [] : t.split(':')
}
for (const g of [...head, ...tailArr]) {
if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return { error: `分组 "${g}" 不是合法的 1-4 位十六进制` }
}
let groups
if (dbl === -1) {
if (head.length !== 8) return { error: `未使用 "::" 时必须为 8 个分组,当前为 ${head.length} 个` }
groups = head.map(g => parseInt(g, 16))
} else {
const fill = 8 - head.length - tailArr.length
if (fill < 1) return { error: '"::" 至少要代表一个全零分组,当前分组数已达 8 个' }
groups = [
...head.map(g => parseInt(g, 16)),
...new Array(fill).fill(0),
...tailArr.map(g => parseInt(g, 16))
]
}
if (tailV4) { groups[6] = tailV4[0]; groups[7] = tailV4[1] }
return { groups }
}
// 8 组 4 位十六进制的完整形式
function expandV6(g) {
return g.map(n => n.toString(16).padStart(4, '0')).join(':')
}
// RFC 5952 压缩:小写、去前导零、最长连续零段(≥2 组)用 :: 替代,并列时取最左
function compressV6(g) {
let bestStart = -1, bestLen = 0, curStart = -1, curLen = 0
for (let i = 0; i < 8; i++) {
if (g[i] === 0) {
if (curStart === -1) { curStart = i; curLen = 1 } else curLen++
if (curLen > bestLen) { bestLen = curLen; bestStart = curStart }
} else {
curStart = -1
curLen = 0
}
}
const parts = g.map(n => n.toString(16))
if (bestLen < 2) return parts.join(':')
return parts.slice(0, bestStart).join(':') + '::' + parts.slice(bestStart + bestLen).join(':')
}
const v6ToBig = g => g.reduce((acc, n) => (acc << 16n) | BigInt(n), 0n)
function bigToV6(v) {
const out = new Array(8)
for (let i = 7; i >= 0; i--) {
out[i] = Number(v & 0xffffn)
v >>= 16n
}
return out
}
function isMappedV4(g) {
return g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0xffff
}
function embeddedV4(g) {
return `${g[6] >> 8}.${g[6] & 255}.${g[7] >> 8}.${g[7] & 255}`
}
function v6Kind(g) {
const all0 = g.every(n => n === 0)
if (all0) return '未指定地址 ::/128'
if (g.slice(0, 7).every(n => n === 0) && g[7] === 1) return '环回地址 ::1/128'
if (isMappedV4(g)) return `IPv4 映射地址 ::ffff:0:0/96(对应 ${embeddedV4(g)})`
if (g[0] === 0x64 && g[1] === 0xff9b) return `NAT64 前缀 64:ff9b::/96(对应 ${embeddedV4(g)})`
if ((g[0] & 0xff00) === 0xff00) return '组播地址 ff00::/8'
if ((g[0] & 0xffc0) === 0xfe80) return '链路本地地址 fe80::/10'
if ((g[0] & 0xfe00) === 0xfc00) return '唯一本地地址 ULA fc00::/7'
if (g[0] === 0x2001 && g[1] === 0x0db8) return '文档示例地址 2001:db8::/32'
if ((g[0] & 0xe000) === 0x2000) return '全球单播地址 2000::/3'
return '其他 / 保留地址'
}
function calc6() {
error6.value = ''
result6.value = null
let addr = String(ip6.value ?? '').trim()
let pre = String(prefix6.value ?? '').trim()
// 支持 "地址/前缀" 连写;注意 [xx]/64 形式
const slash = addr.lastIndexOf('/')
if (slash !== -1) {
const inline = addr.slice(slash + 1).trim()
addr = addr.slice(0, slash).trim()
if (inline) pre = inline
}
if (pre === '') pre = '128'
const parsed = parseIPv6(addr)
if (parsed.error) { error6.value = parsed.error; return }
if (!/^\d{1,3}$/.test(pre)) { error6.value = '前缀长度必须是 0-128 之间的整数'; return }
const prefix = Number(pre)
if (prefix < 0 || prefix > 128) { error6.value = '前缀长度必须在 0-128 之间'; return }
const g = parsed.groups
const value = v6ToBig(g)
const mask = prefix === 0 ? 0n : ((1n << BigInt(prefix)) - 1n) << BigInt(128 - prefix)
const net = value & mask
const last = net | ((1n << BigInt(128 - prefix)) - 1n)
result6.value = {
compressed: compressV6(g),
expanded: expandV6(g),
kind: v6Kind(g),
prefix,
network: compressV6(bigToV6(net)),
first: compressV6(bigToV6(net)),
last: compressV6(bigToV6(last)),
total: (1n << BigInt(128 - prefix)).toLocaleString('en-US'),
mappedV4: isMappedV4(g) || (g[0] === 0x64 && g[1] === 0xff9b) ? embeddedV4(g) : ''
}
}
function clear6() {
ip6.value = ''
prefix6.value = '64'
error6.value = ''
result6.value = null
}
function useSample(s) {
ip6.value = s
prefix6.value = ''
calc6()
}
/* ---------------- IPv4 ↔ IPv6 映射地址 ---------------- */
const mapV4 = ref('')
const mapV6 = ref('')
const mapError = ref('')
const mapResult = ref(null)
function toMapped() {
mapError.value = ''
mapResult.value = null
const v4 = parseIPv4Parts(String(mapV4.value ?? '').trim())
if (!v4) { mapError.value = 'IPv4 地址格式不正确'; return }
const g = [0, 0, 0, 0, 0, 0xffff, (v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3]]
mapResult.value = {
'IPv4 映射地址': `::ffff:${v4.join('.')}`,
'纯十六进制写法': compressV6(g),
完整展开: expandV6(g),
'IPv4 兼容地址(已废弃)': `::${v4.join('.')}`,
'NAT64 (64:ff9b::/96)': compressV6([0x64, 0xff9b, 0, 0, 0, 0, g[6], g[7]])
}
}
function fromMapped() {
mapError.value = ''
mapResult.value = null
const parsed = parseIPv6(String(mapV6.value ?? '').trim())
if (parsed.error) { mapError.value = parsed.error; return }
const g = parsed.groups
const isNat64 = g[0] === 0x64 && g[1] === 0xff9b
if (!isMappedV4(g) && !isNat64) {
mapError.value = '该地址不是 IPv4 映射地址(::ffff:0:0/96)或 NAT64 地址(64:ff9b::/96)'
return
}
mapResult.value = {
'IPv4 地址': embeddedV4(g),
压缩格式: compressV6(g),
完整展开: expandV6(g),
地址类型: v6Kind(g)
}
}2.3 效果截图
