CIDR逐位操作-我能更聪明一点吗


CIDR bitwise operations - could I be a bit wiser?

我正在构建一个类来表示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;

我不喜欢这样,因为循环-我确信有一种更智能的比特方式可以做到这一点。


有没有更智能的方法来完成这两项任务?

想法/建议/普遍滥用请。。。


编辑:

任何解决方案都需要在PHP4.3.10及以上版本上运行,并且必须在32位和64位平台上运行。请记住,PHP中的所有整数都是有符号的,在32位平台上,任何>= 0x80000000都将存储为双精度(因此,使用逐位操作时效果不佳)。

您的第二个问题可以被视为在倒数字中找到第一个设置位(而不是在非倒数字中寻找第一个未设置位),这相当于找到该数字的整数log2。

这是比特世界中一个相当常见的问题,有许多速度优化的算法。您使用的是(慢速)明显的算法:http://www-graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious

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

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

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

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

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

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

更新

在64位机器上,二进制文本表示保留了32个零,因此代码需要使用ltrim()进行修补,如下所示:

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

更新2

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

$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));

在这个过程中,数字变成了一个浮点,但保留了足够的精度来处理比特移位。

为什么不这样做:

$netmask = ( (1<<32) -1 ) << ( 32 - $cidr);

你说你不喜欢左移右移,两个左移怎么样?)

之后,我将其放入ip2longlong2ip中。要从掩码转到CIDR,我将执行以下操作:

$mask = ip2long($mask);
$base = ( ( 1 << 32 ) - 1 );
$cidr = 32 - log( ( $mask ^ $base ) + 1 , 2);

当然,您可以根据需要使用packdechexunpack来适应您的存储类型。

为什么要计算它?只需创建一个由32个子网掩码组成的数组。

$cidr2mask = array( "'x00'x00'x00'x00", "'x80'x00'x00'x00", "'xc0'x00'x00'x00", "'xe0'x00'x00'x00",
                    "'xf0'x00'x00'x00", "'xf8'x00'x00'x00", "'xfc'x00'x00'x00", "'xfe'x00'x00'x00", 
                    "'xff'x00'x00'x00", "'xff'x80'x00'x00", "'xff'xc0'x00'x00", "'xff'xe0'x00'x00", 
                    "'xff'xf0'x00'x00", "'xff'xf8'x00'x00", "'xff'xfc'x00'x00", "'xff'xfe'x00'x00", 
                    "'xff'xff'x00'x00", "'xff'xff'x80'x00", "'xff'xff'xc0'x00", "'xff'xff'xe0'x00", 
                    "'xff'xff'xf0'x00", "'xff'xff'xf8'x00", "'xff'xff'xfc'x00", "'xff'xff'xfe'x00", 
                    "'xff'xff'xff'x00", "'xff'xff'xff'x80", "'xff'xff'xff'xc0", "'xff'xff'xff'xe0", 
                    "'xff'xff'xff'xf0", "'xff'xff'xff'xf8", "'xff'xff'xff'xfc", "'xff'xff'xff'xfe");
$mask2cidr = array_flip($cidr2mask);

然后只使用$cidr2mask[$cidr];$mask2cidr[$mask]

(无耻的自我推销)

我已经建立了一个PHP库,它为IP地址做了非常类似的事情。

以下是我如何构建IPv4子网掩码:

<?php
$mask = (~0) << (32 - $cidr);
$binary_mask = pack('N', $mask);
echo implode('.', unpack('C4', $binary_mask));

由于名称空间的原因,它不能在旧版本的PHP上工作,但在添加之前有分支,我很乐意接受pull请求来解决兼容性问题。该代码(几乎)100%被单元测试覆盖:)

唯一的依赖项是pear Math_BigInteger包。