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

5
我目前在一个基于PHP的项目中使用IPv4和IPv6地址,并且需要比较两个IP以确定哪一个更大。例如,192.168.1.9大于192.168.1.1。为了做到这一点,我使用inet_ptonunpack将IP转换为二进制字符串(我熟悉ip2long,但它仅适用于IPv4)。
这种方法起初似乎很有效,但我很快发现,当我将任何以.32结尾的IP与较低的IP地址进行比较时,结果是不正确的。例如,如果我将192.168.1.0与192.168.1.32进行比较,我的脚本告诉我192.168.1.0大于192.168.1.32。这仅发生在其中一个IP以.32结尾时。IP的前三个八位组可以更改,结果相同。

The following PHP code generates a page that illustrates this issue:

// 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>';}
}

?>

Here is a sample of the result:

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
...

Converting the IPs back to human readable format returns the correct IP, so I believe the issue is with the comparison. I have tried switching to strcmp for the binary comparison, however the result was the same.

Any help in determining the cause of this would be greatly appreciated. I am not set on using the IP conversion and comparison methods shown in the example script, however I do need to stick with methods that support both IPv4 and IPv6. Thanks.

I am running PHP verison 5.3.3, with Zend Engine v2.3.0 and ionCube PHP Loader v4.6.1

Edit: I resolved the issue by changing the unpack format from "A4" (space-padded strings) to "a4" (NUL-padded strings). See my answer below for details.


3
欢迎使用PHP。享受您极其松散的类型转换。 - Ignacio Vazquez-Abrams
但是请先尝试将它们转换为数字。 - Ignacio Vazquez-Abrams
对于IPv6,它们将是非常大的二进制数(太大而无法转换为整数)。我尝试将它们强制转换为二进制,但结果没有改变。 - Seth McCauley
另外,strcmp旨在比较二进制字符串。如上所述,当我尝试使用此方法进行比较时,结果是相同的。 - Seth McCauley
未经测试 - 可能有用:IPvx提供了有用的IP(v4/v6)地址函数。似乎将其转换为二进制格式可能会使比较更容易? - Ryan Vincent
非常感谢。我更愿意坚持使用inet_pton...这段代码很快就会在生产中使用,所以我不想使用我不熟悉的库。 - Seth McCauley
2个回答

4
经过一番研究,我发现了这个问题的原因。当使用unpack函数时,我使用了格式代码"A4",这种格式用于以空格填充的字符串。这会导致任何以数字32结尾的内容都被视为空格,然后被修剪掉。例如,在拆开二进制值192.168.1.32后,我将其转换为十六进制。结果是C0A801,但应该是C0A80120。如果IP地址是192.32.32.32,情况更糟,因为它会一直修剪到C0(相当于192.0.0.0)。

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


2
如果您想将其转换为数字而不是二进制,下面有几个有用的函数:
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逻辑。保留了HTML标签,不做解释。

实际上,该函数并不检查ipv6格式是否正确,但是该函数的结果可以被视为正确的,0:0:0:0:0:0:0:1 = 1。 在PHP中,该评估也将比较像ipv6ToNum(':::2') === '2'这样的字符串,这也是有效的。 - nik.longstone
谢谢您的回复,但我更愿意调试inet_pton函数的结果,因为我在许多地方都使用它,并且需要数据是二进制的。inet_pton通常用于在PHP中处理IPv6,所以我希望有人之前遇到过这个问题。 - Seth McCauley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接