18

我正在构建一个类来表示 IPv4 子网。我将网络地址和子网掩码存储为 4 字节二进制字符串,它们是在构造函数期间根据参数构建的。我希望构造函数接受的一种表示是CIDR notation

我的按位运算有点生疏,我遇到的问题是将子网掩码的十进制整数 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都将存储为双精度数(因此不能很好地使用按位运算)。

4

5 回答 5

8

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

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

但我假设您并不真正关心速度,而是关心简洁,在这种情况下,您可以执行以下操作:

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

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

于 2012-08-23T23:14:28.703 回答
6

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

$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

第一个问题也可以用文本方法解决,尽管需要使用str_split()(在 PHP 4.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));

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

于 2012-08-24T03:30:40.067 回答
3

为什么不这样做:

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

你说你不喜欢左移然后右移,两个左移怎么样;)

在这之后,我把它扔进ip2longor long2ip。要从掩码到 CIDR,我会这样做:

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

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

于 2012-08-28T18:43:08.277 回答
3

为什么要计算它?只需创建一个包含 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];and $mask2cidr[$mask]

于 2012-09-01T11:17:17.983 回答
1

(无耻的自我宣传)

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

这是我构建 IPv4 子网掩码的方法:

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

由于命名空间,它不适用于旧版本的 PHP,但在添加之前有分支,我很乐意接受拉取请求以修复兼容性问题。代码(几乎)100% 被单元测试覆盖 :)

唯一的依赖是pear Math_BigInteger 包

于 2012-09-01T21:38:13.520 回答