Php将ipv6转换为数字


Php convert ipv6 to number

在Ipv4中,我们可以使用ip2long将其转换为数字,

如何在PHP中将ipv6压缩为数字?

我试过inet_pton,但它不起作用。

$ip_1='2001:0db8:85a3:0000:0000:8a2e:0370:7334'; 
$ip_2='2001:11ff:ffff:f';//Compressed
echo inet_pton($ip_1); 
//OUTPUT  ИЃ.ps4
echo inet_pton($ip_2);
//OUTPUT Warning: inet_pton(): Unrecognized address 2001:11ff:ffff:f

使用:

$ip  = 'fe80:0:0:0:202:b3ff:fe1e:8329';
$dec = ip2long_v6($ip);
$ip2 = long2ip_v6($dec);
// $ip  = fe80:0:0:0:202:b3ff:fe1e:8329
// $dec = 338288524927261089654163772891438416681
// $ip2 = fe80::202:b3ff:fe1e:8329

功能:

启用GMPBCMATH扩展。

function ip2long_v6($ip) {
    $ip_n = inet_pton($ip);
    $bin = '';
    for ($bit = strlen($ip_n) - 1; $bit >= 0; $bit--) {
        $bin = sprintf('%08b', ord($ip_n[$bit])) . $bin;
    }
    if (function_exists('gmp_init')) {
        return gmp_strval(gmp_init($bin, 2), 10);
    } elseif (function_exists('bcadd')) {
        $dec = '0';
        for ($i = 0; $i < strlen($bin); $i++) {
            $dec = bcmul($dec, '2', 0);
            $dec = bcadd($dec, $bin[$i], 0);
        }
        return $dec;
    } else {
        trigger_error('GMP or BCMATH extension not installed!', E_USER_ERROR);
    }
}
function long2ip_v6($dec) {
    if (function_exists('gmp_init')) {
        $bin = gmp_strval(gmp_init($dec, 10), 2);
    } elseif (function_exists('bcadd')) {
        $bin = '';
        do {
            $bin = bcmod($dec, '2') . $bin;
            $dec = bcdiv($dec, '2', 0);
        } while (bccomp($dec, '0'));
    } else {
        trigger_error('GMP or BCMATH extension not installed!', E_USER_ERROR);
    }
    $bin = str_pad($bin, 128, '0', STR_PAD_LEFT);
    $ip = array();
    for ($bit = 0; $bit <= 7; $bit++) {
        $bin_part = substr($bin, $bit * 16, 16);
        $ip[] = dechex(bindec($bin_part));
    }
    $ip = implode(':', $ip);
    return inet_ntop(inet_pton($ip));
}

演示

请注意,对于大IP地址,所有答案都会导致错误的结果,或者要经过非常复杂的过程才能接收实际数字。从IPv6地址检索实际整数值需要两件事:

  1. IPv6支持
  2. GMP扩展(--with-gmp

有了这两个先决条件,转换就简单到:

$ip = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff';
$int = gmp_import(inet_pton($ip));
echo $int; // 340282366920938463463374607431768211455

inet_pton返回的二进制数字压缩in_addr表示已经是一个整数,可以直接导入GMP,如上所示。没有必要进行特殊转换或其他任何操作。

请注意,另一种方法同样简单:

$int = '340282366920938463463374607431768211455';
$ip = inet_ntop(str_pad(gmp_export($int), 16, "'0", STR_PAD_LEFT));
echo $ip; // ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff

因此,构建两个所需的功能非常简单:

function ipv6_to_integer($ip) {
    return (string) gmp_import(inet_pton($ip));
}
function ipv6_from_integer($integer) {
    return inet_ntop(str_pad(gmp_export($integer), 16, "'0", STR_PAD_LEFT));
}

$ip_2不是有效的IPv6地址。您需要":"在那里的某个地方,以指示零遗漏点。

如果你把它作为之一

$ip_2='2001::11ff:ffff:f';
$ip_2='2001:11ff::ffff:f';
$ip_2='2001:11ff:ffff::f';

那么inet_pton()应该可以正常工作。

如前所述,PHP没有128整数类型,因此您可以从二进制字符串inet_pton()中获得一个数字字符串。。。是的,就是这样,这就是为什么它看起来很奇怪。如果你看看这些字符串的部分,你会发现它们正是你所期望的。

以下是如何将二进制字符串扩展为数字字符串(str_pad()中最初缺少参数"0"):

/**
 * @param string $ip A human readable IPv4 or IPv6 address.
 * @return string Decimal number, written out as a string due to limits on the size of int and float.
 */
function ipv6_numeric($ip) {
    $binNum = '';
    foreach (unpack('C*', inet_pton($ip)) as $byte) {
        $binNum .= str_pad(decbin($byte), 8, "0", STR_PAD_LEFT);
    }
    // $binNum is now a human readable string, but in binary.
    // If you have the gmp PHP extension, you can convert it to decimal
    return gmp_strval(gmp_init(ltrim($binNum, '0'), 2), 10);
}

试试这个

function inet6_to_int64($addr)
{
    /* Expand the address if necessary */
    if (strlen($addr) != 39) {
        $addr = inet6_expand($addr);
        if ($addr == false) return false;
    } // if
    $addr = str_replace(':', '', $addr);
    $p1 = '0x' . substr($addr, 0, 16);
    $p2 = '0x' . substr($addr, 16);
    $p1 = gmp_init($p1);
    $p2 = gmp_init($p2);
    $result = array(gmp_strval($p1), gmp_strval($p2));
    return $result;
} // inet6_to_int64()

有关更多功能或详细信息,请访问http://www.soucy.org/project/inet6/

这里有两个将十六进制转换为十进制和将十进制转换为十六进制的函数。这只适用于IPv6十六进制和IPv6整数表示(因此,IPv4编号使用ip2long()和long2ip())。理论上,可以将IPv4点符号数转换为十六进制值并使用这些值,但这可能有些过头了。

这将:

  • 将所有完整的IPv6数字转换为字符串化的长int(最多39个字符,如果标志设置为true,则左填充以进行排序)
  • 将字符串化的"long"转换回十六进制IPv6表示,根据需要对完整的32位十六进制字符串进行左填充。如果适当的标志设置为true,则可以选择从右到左放置冒号

这些可以修改为处理几乎任何长度的十六进制值或实际上任何长度的整数,并且可以相应地调整冒号和填充的位置。

十六进制到十进制

    function bchexdec($hex,$padl)
    // Input: A hexadecimal number as a String.
    // Output: The equivalent decimal number as a String.
    // - if padl==true then pad left to fill 39 characters for string sort
    {
        if (strlen($hex) != 39) 
        {
            $hex = inet6_expand($hex);
            if ($hex == false) return false;
        }
        $hex=str_replace(":","",$hex);
        $dec = 0;
        $len = strlen($hex);
        for ($i = 1; $i <= $len; $i++) 
        {
            $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
        }
        if ($padl==true)
        {
            $dec=str_pad($dec,39,"0",STR_PAD_LEFT);
        }
        return $dec;
    }

十进制到十六进制

    function bcdechex($dec,$colon) 
    // Input: A decimal as a String.
    // Output: The equivalent hex value.
    // - if $colon==true then add colons.   
    {
        $hex = '';
        // RFC 5952, A Recommendation for IPv6 Address Text Representation, Section 4.3 Lowercase specifies letters should be lowercase. Though that generally doesn't matter, use lowercase
        //   to conform with the RFC for those rare systems that do so. 
        do 
        {    
            $last = bcmod($dec, 16);
            $hex = dechex($last).$hex;
            $dec = bcdiv(bcsub($dec, $last), 16);
        } while($dec>0);
        $hex=str_pad($hex,32,"0",STR_PAD_LEFT);
        // Add colons if $colon==true
        if ($colon==true)
        {
            $hex=strrev($hex);
            $chunks=str_split($hex,4);
            $hex=implode(":", $chunks);
            $hex=strrev($hex);
        }
        return $hex;
    }

这是基于在各种地方找到的想法和例子,以及我自己对易于排序和存储的IPv6地址的需求。

好的,与Ben Wong聊天的一些启示。。。真正的问题是优化数据库中关于地理IP服务的查找。

这个geoIP数据库提供的数据库布局太慢了,不能只在开始和结束时应用普通的ol'BETWEEN,即使存储是最经济的。

我在聊天中首先提出的建议是将IP地址分割成4个int,然后依次进行比较,但在第二个int中,这可能还不够,因为你仍在搜索整个DB,它超过了100万行。我也有一个用子网掩码进行匹配的想法,但考虑到一些范围不在大掩码内,这样做可能还是不够的。

我会看看我能做些什么,然后编辑这个答案。但在此期间,我将为其他愿意提供帮助的人发布此消息。

boen_arobot的答案版本,避免了溢出问题,如果可用,使用BC数学。

function ipv62numeric($ip)
{
    $str = '';
    foreach (unpack('C*', inet_pton($ip)) as $byte) {
        $str .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT);
    }
    $str = ltrim($str, '0');
    if (function_exists('bcadd')) {
        $numeric = 0;
        for ($i = 0; $i < strlen($str); $i++) {
            $right  = base_convert($str[$i], 2, 10);
            $numeric = bcadd(bcmul($numeric, 2), $right);
        }
        $str = $numeric;
    } else {
        $str = base_convert($str, 2, 10);
    }
    return $str;
}

示例:

echo ipv62numeric('2001:11ff:ffff::f');

将以字符串形式返回"425408532245347499564798372846648688655",这是正确的答案。