我目前有一个包含IP地址的MySQL数据库。在搜索表单上,客户端希望在部分IP地址上进行搜索,并(可能)弹出许多结果。我目前在mysql中存储的IP地址为unsigned int。我使用的是PHP 5.2,因此无法访问PHP 5.7及其INET6_NTOA函数。
目前的数据库有50000多条记录,而且还在继续增长,所以我不想把所有的IP都转换成点符号,然后进行匹配——这似乎有点笨拙。
有没有更好的方法让我搜索部分IP地址?
实际上,无符号整数列已经是在部分ip地址上搜索匹配项的最有效方法了!请不要把精力和CPU时间浪费在转换回点符号或在某种字符串列上进行LIKE搜索上。
有几种方法可以写下部分IP地址,但最终,它们都归结为带有网络掩码的基本IP。此外,假设partial是指具有公共前缀的所有IP,那么这也相当于指定了一系列IP。
无论哪种方式,部分IP地址规范最终都被描述为两个32位的无符号整数,编码格式与数据库列相同。要么你有一个起始ip和结束ip,要么你有基本ip和掩码。这些整数可以直接在SQL查询中使用,以有效地获得匹配。更好的是,如果您使用ip范围方法,那么引擎将能够利用ip列上的有序索引。你再也指望不到比这更好的了。
那么如何构建IP范围呢?这取决于你的部分地址最初是如何指定的,但假设你知道网络掩码,那么起始地址等于(base-ip&net-mask),结束地址是((base-ip&net-mack)|(~netmask)),其中&,|和~分别表示按位和、按位或和不按位。
更新
下面是一个示例代码,用于应用我所描述的策略。
现在,距离我上次编写PHP代码已经很长时间了,下面的代码从未执行过,所以请原谅我可能引入的任何错误。我还特意选择"扩展"每个表示法场景,以使它们更容易理解,而不是将所有它们压缩在一个非常复杂的正则表达式中。
if (preg_match(' /^ ('d{1,3}) [.] ('d{1,3}) [.] ('d{1,3}) [.] ('d{1,3}) [/] ('d{1,2}) $/x', $input, $r)) {
// Four-dotted IP with number of significant bits: 123.45.67.89/24
$a = intval($r[1]);
$b = intval($r[2]);
$c = intval($r[3]);
$d = intval($r[4]);
$mask = intval($r[5]);
} elseif (preg_match(' /^ ('d{1,3}) (?: [.] [*0] [.] [*0] [.] [*0] )? $/x', $input, $r)) {
// Four-dotted IP with three-last numbers missing, or equals to 0 or '*':
// 123.45, 123.45.0.0, 123.45.*.* (assume netmask of 8 bits)
$a = intval($r[1]);
$b = 0;
$c = 0;
$d = 0;
$mask = 8;
} elseif (preg_match(' /^ ('d{1,3}) [.] ('d{1,3}) (?: [.] [*0] [.] [*0] )? $/x', $input, $r)) {
// Four-dotted IP with two-last numbers missing, or equals to 0 or '*':
// 123.45, 123.45.0.0, 123.45.*.* (assume netmask of 16 bits)
$a = intval($r[1]);
$b = intval($r[2]);
$c = 0;
$d = 0;
$mask = 16;
} elseif (preg_match(' /^ ('d{1,3}) [.] ('d{1,3}) [.] ('d{1,3}) (?: [.] [*0] )? $/x', $input, $r)) {
// Four-dotted IP with last number missing, or equals to 0 or *:
// 123.45.67, 123.45.67.0, 123.45.67.* (assume netmask of 24 bits)
$a = intval($r[1]);
$b = intval($r[2]);
$c = intval($r[3]);
$d = 0;
$mask = 24;
} elseif (preg_match(' /^ ('d{1,3}) [.] ('d{1,3}) [.] ('d{1,3}) [.] ('d{1,3}) $/x', $input, $r)) {
// Four-dotted IP: 123.45.67.89 (assume netmask of 32 bits)
$a = intval($r[1]);
$b = intval($r[2]);
$c = intval($r[3]);
$d = intval($r[4]);
$mask = 32;
} else {
throw new Exception('...');
}
if ($a < 0 || $a > 255) { throw new Exception('...') };
if ($b < 0 || $b > 255) { throw new Exception('...') };
if ($c < 0 || $c > 255) { throw new Exception('...') };
if ($d < 0 || $d > 255) { throw new Exception('...') };
if ($mask < 1 || $mask > 32) { throw new Exception('...') };
$baseip = ($a << 24) + ($b << 16) + ($c << 8) + ($d);
$netmask = (1 << (32 - $mask)) - 1;
$startip = $baseip & netmask;
$endip = ($baseip & netmask) | (~netmask);
// ...
doSql( "SELECT ... FROM ... WHERE ipaddress >= ? && ipaddress <= ?", $startip, $endip);
// or
doSql( "SELECT ... FROM ... WHERE ((ipaddress & ?) = ?)", $netmask, $startip);
假设您处理的是IPv4地址,每个地址只有32位。
有一个MySQL INET_NTOA
函数,负责通过您的IP返回字符串。
所以,你可能想要使用smth,比如:
SELECT ... FROM ... WHERE INET_NTOA(...) LIKE (...)
希望能有所帮助。
UPD:为了提高生产力,我建议您更新表,添加新的CHAR(16)
字段用于IP的字符串表示,并添加触发器ON UPDATE
,即用INET_NTOA(...)
值填充该字段。在这个领域中进行选择会很有魅力。
给你。
$ip = '127.5.3';
if (preg_match('/^([0-9]*)?'.?([0-9]*)?'.?([0-9]*)?'.?([0-9]*)$/',$ip, $m)) {
$from = (int)$m[1]*256*256*256 +(int)$m[2]*256*256 + (int)$m[3]*256 + (int)$m[4];
// or $from = ip2long($m[1].'.'.$m[2].'.'.$m[3].'.'.$m[4]);
$to = ($m[1]>0?$m[1]:255)*256*256*256 + ($m[2]>0?$m[2]:255)*256*256 + ($m[3]>0?$m[3]:255)*256+($m[4]>0?$m[4]:255);
// select * from sometable where ip between $from and $to
} else
echo "Incorrect IP";
由于您想要部分搜索并返回具有匹配ips的列表,我建议使用LIKE,然后在末尾使用%
SELECT ip FROM ip_table
WHERE ip LIKE '$ip%'