Skip to content

实现 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-inputel-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 效果截图

IpSubnet 效果截图

工具访问地址:https://www.i91tools.com/tools/ip-subnet