前言

前几天帮朋友设置路由器,在局域网配置里看到一行字:“IP 地址 192.168.1.100,子网掩码 255.255.255.0”。朋友问我:“这个 255 到底是干嘛的?为什么同一个办公室的电脑 IP 都是 192.168.1.xxx?”我解释了半天网络位和主机位,发现越说越绕。回家之后我打开 DevEco Studio 6.1.1 Beta1,想着干脆写一个小工具,把 IP 和掩码输进去,网络地址、广播地址、可用 IP 范围一次性全算出来,还能把掩码翻译成“/24”这种 CIDR 格式。这样下次再有人问,我就直接掏手机算给他看。

这篇文章就是那个晚上折腾出来的记录。我会从 IP 地址的二进制本质讲起,把子网掩码的“与运算”魔法掰开揉碎,然后用 HarmonyOS 的 TextInput 和几行位运算逻辑,搭出一个能跑在 Pura X Max 模拟器上的计算器。代码也给全,你拷进去就能当随身网络小助手。

一、IP 地址不是电话号码——它更像一个“小区门牌”

我们平时看到的 IP 地址,比如 192.168.1.100,其实不是一整个数字,而是被点分成了四段,每段 8 个二进制位,合起来一共 32 位。这种写法只是为了人眼好读,计算机内部只认一串 32 位的 0 和 1。

这 32 位天生就分成两部分:前面一段是“网络号”,后面一段是“主机号”。就像你家的地址“XX小区 3 栋 502”,网络号是小区名,主机号是具体到哪一户。但问题来了:这 32 位里,前多少位是网络号,后多少位是主机号?这得由“子网掩码”来定。掩码也是一串 32 位二进制数,它的规则简单粗暴:左边一串连续的 1,右边一串连续的 0。1 盖住的地方就是网络号,0 盖住的地方就是主机号。

比如掩码 255.255.255.0,写成二进制是 11111111 11111111 11111111 00000000,有 24 个 1。那么 IP 地址的前 24 位就是网络号,后 8 位是主机号。因为 1 的个数是 24,我们通常记成 /24,这就是 CIDR 表示法。

有了这些预备知识,网络地址、广播地址、可用 IP 范围就都能用“位运算”算出来了。比如:

  • 网络地址 = IP 地址 按位与 子网掩码。它就是把主机号部分全部清零,剩下的就是这个小区的“入口地址”。
  • 广播地址 = 网络地址 按位或 (掩码取反后的主机位全 1)。相当于往这个小区所有住户群发消息的目的地址。
  • 可用 IP 范围 = 网络地址 + 1 到 广播地址 - 1。这两个首尾地址不能分配给具体设备,网络地址是小区本身的名字,广播地址是群发大喇叭。

在代码里,我们需要先把点分十进制的 IP 转成一个 32 位无符号整数,掩码也同样转,然后用位运算求出上述三个地址,再转回点分十进制给人看。转整数的办法是把四段数字分别左移 24、16、8、0 位再相加;转回去就是反向取出每 8 位。

二、位运算到底在干什么——举一个接地气的例子

假设有个小型办公网,IP 地址是 192.168.1.100,掩码是 255.255.255.0(/24)。

第一步,转成整数:

  • IP:192<<24 | 168<<16 | 1<<8 | 100 = 十进制 3232235876?实际上是 0xC0A80164
  • 掩码:255<<24 | 255<<16 | 255<<8 | 0 = 0xFFFFFF00

第二步,网络地址 = IP & 掩码 = 0xC0A80164 & 0xFFFFFF00 = 0xC0A80100。转回十进制就是 192.168.1.0。这个地址不能用在任何设备上,它代表整个子网。

第三步,广播地址。先算出掩码的反码(把 0 和 1 翻转),但只取低 32 位。在 JavaScript 里,~mask 会得到一个负数,因为位运算默认按 32 位有符号整数处理。我们需要用 (~mask) >>> 0 强制转成无符号整数。然后广播地址 = 网络地址 | 主机位全1。具体是 network | (~mask >>> 0),结果 0xC0A80100 | 0x000000FF = 0xC0A801FF,即 192.168.1.255

第四步,可用 IP 范围:把网络地址 +1 作为起始,广播地址 -1 作为结束,中间的所有地址都可以分配给电脑、手机。比如这个子网里,可用 IP 从 192.168.1.1192.168.1.254,共 254 个。

如果掩码是 /26,也就是 255.255.255.192,那网络位就有 26 位,主机位只剩 6 位。一个子网里只有 2^6 - 2 = 62 个可用 IP。这些都是用 Math.pow(2, 32 - prefixLength) - 2 算出来的。

三、输入校验——别让用户乱填坑

用户可能输入非法的 IP 或掩码,比如 256.1.1.1/33,或者掩码不连续。我们的工具要能识别并提示。

IP 地址校验:用 split('.') 分成四段,每段必须是 0~255 的数字,且不能有空缺。

掩码校验:支持两种输入方式——点分十进制(如 255.255.255.0)和 CIDR 数字(如 24)。如果用户输入的是纯数字(1~32),就生成对应的掩码整数:0xFFFFFFFF << (32 - prefix) >>> 0。如果输入的是点分格式,先转成整数,然后检查它是不是合法的连续 1 加连续 0 的格式。检查方法:把掩码取反加 1,再和原掩码与运算,应该等于 ~mask + 1?实际上合法的掩码满足 (mask & (mask - 1)) 的概念不适用,因为连续 1 的掩码转成整数后,加 1 会变成只有一位 1 的值,再与原掩码与运算应该得 0?但更简单的是:mask & (-mask) 的结果是掩码最低位的 1 对应的值,我们不这样检查。简单点,直接判断 ((~mask + 1) & ~mask) === 0?其实我们可以把掩码转成二进制字符串,然后确保是前面全是 1 后面全是 0。但为了简洁,可以让用户自己保证输入正确,或者只接受 CIDR 数字,这样最安全。我打算提供两个输入框:一个输入 IP,一个输入掩码(支持点分十进制或 CIDR 数字)。内部判断:如果输入包含点,就按点分解析;否则作为数字。

解析掩码时,如果用户输入了 24,就生成 0xFFFFFF00。然后计算前缀长度:统计整数中 1 的个数,用于显示 CIDR。

界面:IP 输入框、掩码输入框、计算按钮。下方结果区域展示:网络地址、广播地址、子网掩码、CIDR 格式、可用 IP 范围、可用数量。

为了方便,预设 IP 192.168.1.100,掩码 24,让用户直接点计算就能看到效果。

四、完整代码——用位运算把子网拆得明明白白

以下代码适配 DevEco Studio 6.1.1 Beta1、SDK22 语法,Pura X Max 模拟器。新建 Empty Ability 项目,替换 entry/src/main/ets/pages/Index.ets。无需任何权限,纯本地运算。

/*
 * IP 地址与子网掩码计算器
 * 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22
 */

@Entry
@Component
struct Index {
  @State ipInput: string = '192.168.1.100';
  @State maskInput: string = '24';
  @State networkAddr: string = '';
  @State broadcastAddr: string = '';
  @State usableRange: string = '';
  @State usableCount: number = 0;
  @State cidrNotation: string = '';
  @State maskDisplay: string = '';
  @State errorMsg: string = '';

  // 点分十进制转32位整数
  private ipToInt(ip: string): number {
    let parts = ip.split('.');
    if (parts.length !== 4) return -1;
    let result = 0;
    for (let i = 0; i < 4; i++) {
      let num = parseInt(parts[i]);
      if (isNaN(num) || num < 0 || num > 255) return -1;
      result = (result << 8) | num;
    }
    return result >>> 0; // 无符号
  }

  // 32位整数转点分十进制
  private intToIp(num: number): string {
    return [
      (num >>> 24) & 0xFF,
      (num >>> 16) & 0xFF,
      (num >>> 8) & 0xFF,
      num & 0xFF
    ].join('.');
  }

  // 从掩码整数计算前缀长度(1的个数)
  private prefixLength(maskInt: number): number {
    let count = 0;
    let m = maskInt >>> 0;
    while (m) {
      count += m & 1;
      m >>>= 1;
    }
    return count;
  }

  // 解析掩码:支持点分十进制或CIDR数字
  private parseMask(input: string): number {
    if (input.indexOf('.') !== -1) {
      return this.ipToInt(input);
    } else {
      let prefix = parseInt(input);
      if (isNaN(prefix) || prefix < 1 || prefix > 32) return -1;
      // 生成连续的1
      return (0xFFFFFFFF << (32 - prefix)) >>> 0;
    }
  }

  // 计算按钮响应
  private calculate(): void {
    this.errorMsg = '';
    let ipInt = this.ipToInt(this.ipInput.trim());
    if (ipInt === -1) {
      this.errorMsg = 'IP 地址格式不正确';
      return;
    }
    let maskInt = this.parseMask(this.maskInput.trim());
    if (maskInt === -1) {
      this.errorMsg = '子网掩码格式不正确(如 24 或 255.255.255.0)';
      return;
    }
    // 检查掩码是否合法(连续的1+0)
    let invMask = (~maskInt) >>> 0;
    if ((invMask + 1) & invMask) {
      // 不合法:invMask + 1 必须为 2 的幂
      this.errorMsg = '子网掩码不合法,必须为连续的 1 和 0';
      return;
    }

    let network = (ipInt & maskInt) >>> 0;
    let broadcast = (network | ((~maskInt) >>> 0)) >>> 0;
    let firstHost = network + 1;
    let lastHost = broadcast - 1;
    let prefix = this.prefixLength(maskInt);
    let hosts = lastHost - firstHost + 1;
    if (hosts < 0) hosts = 0;

    this.networkAddr = this.intToIp(network);
    this.broadcastAddr = this.intToIp(broadcast);
    this.usableRange = this.intToIp(firstHost) + ' ~ ' + this.intToIp(lastHost);
    this.usableCount = hosts;
    this.cidrNotation = this.intToIp(ipInt & maskInt) + '/' + prefix;
    this.maskDisplay = this.intToIp(maskInt);
  }

  build() {
    Column() {
      Text('IP 子网计算器')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 8 })

      Text('输入 IP 地址和子网掩码,计算网络信息')
        .fontSize(15)
        .fontColor('#888')
        .margin({ bottom: 20 })

      // 输入区
      Column() {
        Row() {
          Text('IP 地址').fontSize(16).width(90)
          TextInput({ placeholder: '如 192.168.1.100', text: this.ipInput })
            .onChange((v: string) => { this.ipInput = v; })
            .layoutWeight(1)
            .fontSize(16)
        }
        .width('100%').margin({ bottom: 10 })

        Row() {
          Text('子网掩码').fontSize(16).width(90)
          TextInput({ placeholder: '如 24 或 255.255.255.0', text: this.maskInput })
            .onChange((v: string) => { this.maskInput = v; })
            .layoutWeight(1)
            .fontSize(16)
        }
        .width('100%')
      }
      .width('88%')
      .padding(14)
      .backgroundColor('#F5F5F5')
      .borderRadius(12)
      .margin({ bottom: 15 })

      Button('计算')
        .type(ButtonType.Capsule)
        .backgroundColor('#1976D2')
        .fontColor(Color.White)
        .fontSize(18)
        .onClick(() => { this.calculate(); })
        .margin({ bottom: 15 })

      if (this.errorMsg !== '') {
        Text(this.errorMsg)
          .fontSize(16)
          .fontColor('#F44336')
          .margin({ bottom: 10 })
      }

      // 结果卡片
      if (this.networkAddr !== '') {
        Column() {
          Text('计算结果').fontSize(18).fontWeight(FontWeight.Medium).margin({ bottom: 12 }).alignSelf(ItemAlign.Start)

          Row() { Text('网络地址').fontSize(14).fontColor('#888').width(100); Text(this.networkAddr).fontSize(16).fontWeight(FontWeight.Medium) }.margin({ bottom: 6 })
          Row() { Text('广播地址').fontSize(14).fontColor('#888').width(100); Text(this.broadcastAddr).fontSize(16).fontWeight(FontWeight.Medium) }.margin({ bottom: 6 })
          Row() { Text('可用范围').fontSize(14).fontColor('#888').width(100); Text(this.usableRange).fontSize(15).fontWeight(FontWeight.Medium) }.margin({ bottom: 6 })
          Row() { Text('可用IP数').fontSize(14).fontColor('#888').width(100); Text(this.usableCount.toString()).fontSize(16).fontWeight(FontWeight.Medium) }.margin({ bottom: 6 })
          Row() { Text('CIDR表示').fontSize(14).fontColor('#888').width(100); Text(this.cidrNotation).fontSize(16).fontWeight(FontWeight.Medium) }.margin({ bottom: 6 })
          Row() { Text('子网掩码').fontSize(14).fontColor('#888').width(100); Text(this.maskDisplay).fontSize(16).fontWeight(FontWeight.Medium) }
        }
        .width('88%')
        .padding(16)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
      }

      Text('💡 网络地址 = IP & 掩码;广播地址 = 网络地址 | ~掩码')
        .fontSize(12)
        .fontColor('#AAA')
        .width('90%')
        .textAlign(TextAlign.Center)
        .margin({ top: 15 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FAFAFA')
  }
}

这个代码包含完整的 IP 解析、掩码解析、位运算和结果展示。掩码输入框支持 24255.255.255.0 两种格式。计算按钮点击后先校验输入合法性,然后一次性算出所有需要的值。结果卡片展示了网络地址、广播地址、可用 IP 范围、可用数量、CIDR 表示和掩码本身。

运行效果

把代码粘进 DevEco Studio,Run 到 Pura X Max 模拟器。页面顶部是两个输入框,IP 默认填了 192.168.1.100,掩码默认 24。点“计算”,下方立刻出现结果卡片:网络地址 192.168.1.0,广播地址 192.168.1.255,可用范围从 192.168.1.1192.168.1.254,可用 IP 数 254,CIDR 显示 192.168.1.0/24。如果故意把掩码改成 255.255.255.248,再算一次,可用范围就缩成了 192.168.1.97 ~ 192.168.1.102,只有 6 个 IP。整个计算瞬间完成,位运算在模拟器上跑得飞快,没有任何延迟。

总结

这个 IP 子网计算器本身不大,却把一个网络管理员的基础技能完整搬到了手机上。通过实现它,你可以掌握:

  • 点分十进制与 32 位整数的互转:用移位和位操作拼接、拆分 IP 地址,是处理二进制协议的基本功。
  • 子网掩码的位运算本质:网络地址(IP & mask)、广播地址(network | ~mask)、可用范围的推导,全依赖于“与或非”运算。
  • CIDR 前缀长度计算:通过统计掩码中 1 的个数,将传统掩码表达转换为更简洁的 /24 格式。
  • ArkUI 的输入校验与结果展示:用简单的 TextInput 和 Button 完成交互,卡片式布局让信息一目了然。

下次再有人在路由设置里看到 255.255.255.0,你不仅能解释它是什么,还能掏出手机当场算出子网里到底能接多少台设备。这种“懂原理、会动手”的感觉,可能就是程序员和普通用户之间那道小小的分界线吧。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐