CIDR 按位操作 - 我可以更明智一点吗?

2022-08-30 22:18:22

我正在构建一个类来表示 IPv4 子网。我将网络地址和子网掩码存储为4字节二进制字符串,这些字符串是在基于参数的构造函数期间构建的。我希望构造函数接受的表示形式之一是 CIDR 表示法

我的按位操作有点生锈,我遇到困难的地方是将子网掩码的十进制整数CIDR表示形式转换为4字节二进制字符串,反之亦然。我还发现我无法在琴弦上执行左/右移位 - 我确定我以前已经成功做到了吗?


我已经设法将转换为二进制字符串以使用以下代码:

// An example input value.
$mask = 24; // 255.255.255.0

if ($mask < 0 || $mask > 32) {
  // Invalid prefix size
  throw new RangeException('Invalid CIDR prefix size');
} else if ($mask === 0) {
  // Handle 0
  $mask = "\x00\x00\x00\x00";
} else {
  // Left-pad a 4-byte string with $mask set bits
  $mask = pack('N', (0x01 << 31) >> ($mask - 1));
}

我不喜欢这种逻辑,原因有二:

  • 我不喜欢被视为特例0
  • 我不喜欢右移后左移

我确信有一种方法可以更有效地做到这一点,这种方式可以正确处理,而不会将其视为特例。0


将二进制字符串转换回CIDR前缀大小的十进制表示形式时,我当前正在使用下面的代码。在验证以其他格式提供的子网掩码时,我还有另一个非常相似的代码块,以确保设置的位是连续的。

// An example input value.
$mask = "\xff\xff\xff\x00"; // /24

// Convert the binary string to an int so bit shifts will work
$mask = current(unpack('N', $mask));

// A counter to represent the CIDR
$cidr = 0;

// Loop and check each bit
for ($i = 31; $i > 0; $i--) {
  if (($mask >> $i) & 0x01) {
    $cidr++;
  } else {
    break;
  }
}

// Return the result
return $cidr;

我不喜欢这个,因为循环 - 我觉得有一个更智能的按位方法来做到这一点。


有没有更智能的方法来完成这些任务中的任何一个?

想法/建议/一般滥用请...


编辑:

任何解决方案都需要在 PHP 4.3.10 及更高版本上运行,并且必须同时在 32 位和 64 位平台上工作。请记住,PHP中的所有整数都是有符号的,在32位平台上,任何东西都将存储为双精度值(因此在按位操作中不会很好)。>= 0x80000000


答案 1

第二个问题也可以看作是在倒数中查找第一个设置位(而不是在非反转数字中找到第一个未设置位),这等效于查找数字的整数 log2。

这是按位世界中相当普遍的问题,并且有许多针对它的速度优化算法。您正在使用(慢速)明显的算法:http://www-graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious

但是我假设你并不真正关心速度,而是关心简洁性,在这种情况下,你可以做这样的事情:

$cidr = (int) (32 - log(~current(unpack('N', $mask)) & 0xffffffff, 2));

必须与 64 位整数兼容。& 0xffffffff


答案 2

第二个问题可以通过文本方法解决:

$mask = "\xff\xff\xff\x00";

$cidr = strspn(sprintf('%b', current(unpack('N', $mask))), 1);

它用于将整数转换为二进制文本表示形式,并计算初始文本表示的数量。sprintf()strspn()

更新

在 64 位计算机上,二进制文本表示形式在左填充了 32 个零,因此需要像这样修补代码:ltrim()

$cidr = strspn(ltrim(sprintf('%b', current(unpack('N', $mask))), 0), 1);

更新 2

第一个问题也可以用文本方法解决,尽管需要使用(这在PHP 4.x中不起作用):str_split()

$mask = vsprintf('%c%c%c%c', array_map('bindec', str_split(str_pad(str_repeat(1, $mask), 32, 0), 8)));

更新 3

对我有用的是以下内容(在32位和64位上都进行了测试):

$mask = pack('N', 0xffffffff << (32 - $mask));

在此过程中,数字成为浮点数,但保持足够的精度来处理位移位。


推荐