如何将 PHP 中的 IP 地址作为二进制字符串进行比较?

How do I compare IP addresses in PHP as binary strings?

我目前正在一个基于 PHP 的项目中处理 IPv4 和 IPv6 地址,我需要能够比较两个 IP 以确定哪个更大。例如,192.168.1.9 大于 192.168.1.1。为此,我使用 inet_pton and unpack (I am familiar with ip2long 将 IP 转换为二进制字符串,但仅限于 IPv4)。

这个方法起初似乎工作正常,但是我很快发现当我将任何以 .32 结尾的 IP 与更低的 IP 地址进行比较时,我得到了不正确的结果。例如,如果我将 192.168.1.0 与 192.168.1.32 进行比较,我的脚本会告诉我 192.168.1.0 大于 192.168.1.32。只有当其中一个 IP 以 .32 结尾时才会发生这种情况。 IP的前三个八位字节可以改变,结果是一样的。

以下 PHP 代码生成一个页面来说明此问题:

// Loop through every possible last octet, starting with zero
for ($i = 0; $i <= 255; $i++) {

    // Define two IPs, with second IP increasing on each loop by 1
    $IP1 = "192.168.1.0";
    $IP2 = "192.168.1.".$i;

    // Convert each IP to a binary string
    $IP1_bin = current(unpack("A4",inet_pton($IP1)));
    $IP2_bin = current(unpack("A4",inet_pton($IP2)));

    // Convert each IP back to human readable format, just to show they were converted properly
    $IP1_string = inet_ntop(pack("A4",$IP1_bin));
    $IP2_string = inet_ntop(pack("A4",$IP2_bin));

    // Compare each IP and echo the result
    if ($IP1_bin < $IP2_bin) {echo '<p>'.$IP1_string.' is LESS than '.$IP2_string.'</p>';}
    if ($IP1_bin === $IP2_bin) {echo '<p>'.$IP1_string.' is EQUAL to '.$IP2_string.'</p>';}
    if ($IP1_bin > $IP2_bin) {echo '<p>'.$IP1_string.' is GREATER than '.$IP2_string.'</p>';}

    // I have also tried using strcmp for the binary comparison, with the same result
    // if (strcmp($IP1_bin,$IP2_bin) < 0) {echo '<p>'.$IP1_string.' is LESS than '.$IP2_string.'</p>';}
    // if (strcmp($IP1_bin,$IP2_bin) === 0) {echo '<p>'.$IP1_string.' iS EQUAL to '.$IP2_string.'</p>';}
    // if (strcmp($IP1_bin,$IP2_bin) > 0) {echo '<p>'.$IP1_string.' is GREATER than '.$IP2_string.'</p>';}
}

?>

这是结果示例:

192.168.1.0 is EQUAL to 192.168.1.0
192.168.1.0 is LESS than 192.168.1.1
192.168.1.0 is LESS than 192.168.1.2
192.168.1.0 is LESS than 192.168.1.3
192.168.1.0 is LESS than 192.168.1.4
...
192.168.1.0 is LESS than 192.168.1.31
192.168.1.0 is GREATER than 192.168.1.32
192.168.1.0 is LESS than 192.168.1.33
...

将 IP 转换回人类可读格式 returns 正确的 IP,因此我认为问题在于比较。我尝试切换到 strcmp 进行二进制比较,但结果是一样的。

如果您能帮助确定此问题的原因,我们将不胜感激。我不打算使用示例脚本中显示的 IP 转换和比较方法,但我确实需要坚持使用同时支持 IPv4 和 IPv6 的方法。谢谢。

我是 运行 PHP 版本 5.3.3,使用 Zend Engine v2.3.0 和 ionCube PHP Loader v4.6.1

编辑:我通过将解包格式从 "A4"(space 填充字符串)更改为 "a4"(NUL -填充字符串)。详情请看下面我的回答。

如果您想转换为数字而不是二进制,请使用以下几个有用的函数:

function ipv6ToNum($ip)
{
    $binaryNum = '';
    foreach (unpack('C*', inet_pton($ip)) as $byte) {
        $binaryNum .= str_pad(decbin($byte), 8, "0", STR_PAD_LEFT);
    }
    $binToInt = base_convert(ltrim($binaryNum, '0'), 2, 10);

    return $binToInt;
}

function ipv4ToNum($ip)
{
    $result = 0;
    $ipNumbers = explode('.', $ip);
    for ($i=0; $i < count($ipNumbers); $i++) {
        $power = count($ipNumbers) - $i;
        $result += pow(256, $i) * $ipNumbers[$i];
    }

    return $result;
}

(我在ipv4ToNum中重复ip2long的逻辑)

经过多次故障排除,我发现了这个问题的原因。使用 unpack 函数时,我使用了 "A4" 格式代码,它用于 space 填充字符串。这导致任何以数字 32 结尾的东西都被视为白色 space,然后会被 trimmed。例如,在解包 192.168.1.32 的二进制值后,我将其转换为十六进制。结果是 C0A801 但应该是 C0A80120。如果 IP 是 192.32.32.32,结果会更糟,因为它将 trim 一直下降到 C0(本质上是 192.0.0.0)。

切换到解压缩格式 "a4"(NUL 填充字符串)解决了这个问题。现在,唯一被截断的 IP 是以 .0 结尾的 IP,这是预期的行为。我发现这解决了这个问题很奇怪,因为我遇到的几乎每个解包 IPv4 和 IPv6 地址的例子都使用 "A4" 格式。也许他们在新版本中将 inet_pton 更改为 space-padded。