將 IPv4 位址轉為無號長整數 (Unsigned 32-bit Integer) 儲存於資料庫,可大幅節省空間(4 bytes vs 15 chars)、加快索引比較、支援範圍查詢(BETWEEN)。本文詳細說明 PHP 內建函式、MySQL 對應函式、手動實作原理、有號/無號陷阱與 IPv6 擴充建議。
<?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";
?>
-- 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,647 | ip2long() 回傳 -1 (溢位) |
| PHP (64-bit 系統) | 有號整數 | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 | ip2long() 正確回傳 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 位址 128 位元,無法用 32/64 位元整數儲存。建議方案:
| 方案 | 儲存型別 | 優點 | 缺點 |
|---|---|---|---|
| VARBINARY(16) | 16 bytes 二進位 | 最省空間、MySQL INET6_ATON()/INET6_NTOA() 原生支援 | 需轉換函式、人類不可讀 |
| CHAR(39) | 標準文字格式 | 可直接讀取、支援壓縮格式 (::) | 佔空間大、索引較慢 |
| 兩欄位:ipv4_int + ipv6_bin | INT 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);
?>
<?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 (含索引)
*/
?>