首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP IP 地址與長整數互轉:ip2long、long2ip 與資料庫儲存最佳實踐

     編輯:PHP 相關知識

前言

將 IPv4 位址轉為無號長整數 (Unsigned 32-bit Integer) 儲存於資料庫,可大幅節省空間(4 bytes vs 15 chars)、加快索引比較、支援範圍查詢(BETWEEN)。本文詳細說明 PHP 內建函式、MySQL 對應函式、手動實作原理、有號/無號陷阱與 IPv6 擴充建議。

PHP 內建函式(推薦)

<?php
$ip = '192.168.1.100';

// IP -> 長整數 (有號整數,可能為負數)
$long = ip2long($ip);
var_dump($long); // int(-1062731676)  <-- 32 位元有號整數溢位

// 長整數 -> IP
$back = long2ip($long);
var_dump($back); // string(13) "192.168.1.100"

// 關鍵:資料庫儲存時需轉為無號整數
$unsigned = sprintf('%u', $long); // "3232236036"
var_dump($unsigned);

// 完整工具函式
function ipToUnsignedLong($ip) {
    $long = ip2long($ip);
    if ($long === false) return false; // 非法 IP
    return sprintf('%u', $long); // 轉無號字串
}

function unsignedLongToIp($unsignedLong) {
    // PHP 內部自動處理字串轉整數
    return long2ip((int)$unsignedLong);
}

// 測試
$ips = ['0.0.0.0', '127.0.0.1', '192.168.1.1', '255.255.255.255'];
foreach ($ips as $ip) {
    $ulong = ipToUnsignedLong($ip);
    $back  = unsignedLongToIp($ulong);
    echo "$ip => $ulong => $back\n";
}
?>

手動實作原理(教學用)

<?php
/**
 * 手動 IP -> 無號長整數
 * 公式:w*256^3 + x*256^2 + y*256 + z
 */
function ipToLongManual($ip) {
    $parts = explode('.', $ip);
    if (count($parts) !== 4) return false;
    
    $long = 0;
    foreach ($parts as $part) {
        $octet = (int)$part;
        if ($octet < 0 || $octet > 255) return false;
        $long = ($long << 8) + $octet; // 位移累加
    }
    return $long; // PHP 整數為有號,> 2^31-1 會變負數
}

/**
 * 手動 無號長整數 -> IP
 */
function longToIpManual($long) {
    // 確保視為無號 32 位元
    $long = $long & 0xFFFFFFFF;
    
    $parts = [];
    for ($i = 3; $i >= 0; $i--) {
        $parts[] = ($long >> ($i * 8)) & 0xFF;
    }
    return implode('.', $parts);
}

// 測試
$ip = '192.168.1.100';
$long = ipToLongManual($ip);
echo "Manual: $ip => $long => " . longToIpManual($long) . "\n";
?>

MySQL 對應函式

-- IP -> 無號長整數 (儲存用)
SELECT INET_ATON('192.168.1.100');  -- 回傳 3232236036 (UNSIGNED INT)

-- 無號長整數 -> IP (查詢顯示用)
SELECT INET_NTOA(3232236036);       -- 回傳 '192.168.1.100'

-- 建表範例
CREATE TABLE `ip_logs` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `ip_int` INT UNSIGNED NOT NULL COMMENT 'INET_ATON 儲存',
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_ip` (`ip_int`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入
INSERT INTO `ip_logs` (`ip_int`) VALUES (INET_ATON('192.168.1.100'));

-- 範圍查詢:找出 192.168.1.0/24 區段
SELECT * FROM `ip_logs` 
WHERE `ip_int` BETWEEN INET_ATON('192.168.1.0') AND INET_ATON('192.168.1.255');

-- 顯示時轉回 IP
SELECT INET_NTOA(`ip_int`) AS `ip` FROM `ip_logs`;

有號/無號整數陷阱完整解析

環境型別範圍IP 255.255.255.255 表現
PHP (32-bit 系統)有號整數-2,147,483,648 ~ 2,147,483,647ip2long() 回傳 -1 (溢位)
PHP (64-bit 系統)有號整數-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807ip2long() 正確回傳 4,294,967,295
MySQL INT SIGNED有號-2,147,483,648 ~ 2,147,483,647無法儲存 > 127.255.255.255
MySQL INT UNSIGNED無號0 ~ 4,294,967,295完整支援所有 IPv4

跨平台相容寫法

<?php
function ipToStorage($ip) {
    $long = ip2long($ip);
    if ($long === false) return null;
    
    // 統一輸出無號十進位字串,資料庫欄位用 BIGINT UNSIGNED 或 CHAR(10)
    return sprintf('%u', $long & 0xFFFFFFFF);
}

function ipFromStorage($stored) {
    // 字串轉整數自動處理大數值
    return long2ip((int)$stored);
}

// 測試 32/64 位元相容
$testIps = ['10.0.0.1', '192.168.1.100', '255.255.255.255'];
foreach ($testIps as $ip) {
    $stored = ipToStorage($ip);
    $restored = ipFromStorage($stored);
    echo "$ip -> $stored -> $restored\n";
}
?>

IPv6 支援(現代專案必備)

IPv6 位址 128 位元,無法用 32/64 位元整數儲存。建議方案:

方案儲存型別優點缺點
VARBINARY(16)16 bytes 二進位最省空間、MySQL INET6_ATON()/INET6_NTOA() 原生支援需轉換函式、人類不可讀
CHAR(39)標準文字格式可直接讀取、支援壓縮格式 (::)佔空間大、索引較慢
兩欄位:ipv4_int + ipv6_binINT UNSIGNED + VARBINARY(16)IPv4 繼續高效、IPv6 相容查詢需 UNION/COALESCE
<?php
// PHP 5.3+ 原生支援 IPv6
$ipv6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';

// 轉二進位 (適合 VARBINARY(16))
$bin = inet_pton($ipv6); // 16 bytes binary string
var_dump(bin2hex($bin)); // 20010db885a3000000008a2e03707334

// 還原
$back = inet_ntop($bin);
var_dump($back); // 2001:db8:85a3::8a2e:370:7334 (自動壓縮)

// MySQL 5.6+ 同步函式
// INSERT SET ip_bin = INET6_ATON('2001:db8::1');
// SELECT INET6_NTOA(ip_bin);
?>

效能測試:字串 vs 整數索引

<?php
// 建立 100 萬筆測試資料
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$pdo->exec("CREATE TABLE ip_test_str (ip VARCHAR(15), KEY idx_ip(ip)) ENGINE=InnoDB");
$pdo->exec("CREATE TABLE ip_test_int (ip_int INT UNSIGNED, KEY idx_ip(ip_int)) ENGINE=InnoDB");

$ips = [];
for ($i = 0; $i < 1000000; $i++) {
    $ips[] = sprintf('%d.%d.%d.%d', mt_rand(1,254), mt_rand(0,255), mt_rand(0,255), mt_rand(1,254));
}

$stmtStr = $pdo->prepare('INSERT INTO ip_test_str (ip) VALUES (?)');
$stmtInt = $pdo->prepare('INSERT INTO ip_test_int (ip_int) VALUES (INET_ATON(?))');

$pdo->beginTransaction();
foreach ($ips as $ip) {
    $stmtStr->execute([$ip]);
    $stmtInt->execute([$ip]);
}
$pdo->commit();

// 查詢效能測試
$target = '192.168.1.100';
$start = microtime(true);
$pdo->query("SELECT * FROM ip_test_str WHERE ip = '$target'")->fetchAll();
echo "字串查詢: " . (microtime(true) - $start) * 1000 . " ms\n";

$start = microtime(true);
$pdo->query("SELECT * FROM ip_test_int WHERE ip_int = INET_ATON('$target')")->fetchAll();
echo "整數查詢: " . (microtime(true) - $start) * 1000 . " ms\n";

/*
典型結果 (InnoDB, 100萬筆, 索引命中):
字串查詢: 1.2 ms
整數查詢: 0.8 ms
儲存空間: VARCHAR(15) ~16MB vs INT UNSIGNED ~4MB (含索引)
*/
?>
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved