我可能算是比较关注 ipv6wry.db 这个IPv6数据库的人之一了吧,之前就有写了自动更新脚本 Rhilip/ipv6wry.db ,再早之前在PT-help中也使用了该库。
昨天晚上不知道在想些什么,搜索了一圈没见到 PHP 版本解析库,突然就有写一个的想法。
Github Source: https://github.com/Rhilip/ipv6wry-php
Packgist: https://packagist.org/packages/rhilip/ipv6wry
前人们的工作
官方给出的解析工具中只有 C、Python 版本的实现
真红酱在他的CSDN中使用的方法是使用Python导出CSV文件,然后入库,然后直接根据IPv6前四个字段的值检索数据库。这样的问题是数据库里面需要存储近11w条数据,且不好得到更新(或者说更新过于麻烦)。

JohnWong/python-tool 公开了另外一种Python的实现,只不过其实现基于Python2。不过好在2017年,本人就在工具 PT-help 中将其实现改成了Python3。
IPDB格式说明
以下说明来自官方文档
1 | 文件头 |
所以直接使用该 ipv6wry.db 的核心思想在于从索引区找到记录区的偏移地址,然后根据读取字符串(UTF-8编码)以及是否有重定向偏移继续读取。
版本实现
Rhilip/ipv6wry-php 使用单例模式,当前(v0.1.0)向外开放两个静态方法
1 | /** |
编写过程主要参照的是之前在PT-help中的实现(不过没有实现其IPv4查询的部分),并参照其他PHP的GeoIP库的实现。下面就随便讲一下中间遇到的主要问题以及解决方法。
IPv6前4字段转换为长整数
parseIpv6()因为我们在索引区查找记录区的偏移地址需要知道开始IP地址,其值是IPv6前4字段的长整数表示。其方法在Python有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18def parseIpv6(ip):
if v6ptn.match(ip) is None:
return -1
count = ip.count(':')
if count >= 8 or count < 2:
return -1
ip = ip.replace('::', '::::::::'[0:8 - count + 1], 1)
if ip.count(':') < 6:
return -1
v6 = 0
for sub in ip.split(':')[0:4]:
if len(sub) > 4:
return -1
if len(sub) == 0:
v6 = v6 * 0x10000
else:
v6 = v6 * 0x10000 + int(sub, 16)
return v6或者更为简单的(需要Python版本大于3.3)
1
2
3import ipaddr
ip6 = int(ipaddr.IPAddress(ip))
ip = (ip6 >> 64) & 0xFFFFFFFFFFFFFFFFPHP并没有直接的实现,
inet_pton返回的是binary格式,如果需要转成int形式,还需要使用unpack形式(见 https://stackoverflow.com/questions/18276757/php-convert-ipv6-to-number ),过于繁琐。考虑参照Python的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function parseIpv6(string $ip): int
{
// 检查是不是ipv6
$ipv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
if ($ipv6 === false) return -1;
// 补全 ::
$count = substr_count($ipv6, ':');
$ipv6 = preg_replace('/::/', str_repeat(':', 8 - $count + 1), $ipv6, 1);
// 我们只要前4个,并将其将其转换为整数
$v6_prefix_long = 0;
$subs = array_slice(explode(':', $ipv6), 0, 4);
foreach ($subs as $sub) {
if (strlen($sub) > 4) return -1;
$v6_prefix_long = bcadd(bcmul($v6_prefix_long, 0x10000), intval($sub, 16));
}
return (int)$v6_prefix_long;
}此处是唯一使用 bcmath的地方,原因在于如果给定的ipv6地址过大(例如
fe80::1),直接使用$v6 = $v6 * 0x10000 + intval($sub, 16)会导致最后的值为 float类型,而使用bcadd之后再转换为int不会有该问题。偏移读取并转换Binary形式
read(),readInt(),readRawText()考虑到PHP直接操作字符串存在过多问题,且PHP字符串相关截取方法并不如Python直接可以使用
[start:end:step]的形式,所以使用fseek+fread的形式进行偏移读取。1
2
3
4
5function read(int $offset = 0, int $size = 1): string
{
fseek($this->handle, $offset, SEEK_SET);
return fread($this->handle, $size);
}读取出来的类型有byte,int64,UTF-8编码的字符串,需要分别解析出来。在Python里面的实现分别为
1
2
3
4
5
6
7
8
9
10
11
12
13
14byte_ = db[6] # byte
int64_ = int.from_bytes(self.db[0x10: 0x18], byteorder='little') # int64
def readRawText(self, start):
bs = []
if self.type == 4 and start == self.except_raw:
return bs
while self.db[start] != 0:
bs += [self.db[start]]
start += 1
return bytes(bs)
# UTF-8编码的字符串,00结尾
utf8_ = readRawText(start).decode('utf-8')转换为对应的PHP实现,直接使用unpack方法(little endian),
1
2
3
4
5
6
7
8
9
10
11private function readInt(int $offset = 0, int $size = 8): int
{
$s = $this->read($offset, $size);
if ($size == 3) {
$s .= "\x00";
$size = 4;
}
$format = [8 => 'P', 4 => 'V', 1 => 'C'][$size];
return unpack($format, $s)[1];
}其中当
$size = 1时,可以使用hexdec(bin2hex($s))的方法,但是考虑会增加方法,不如均使用unpack($format,$s)[1]来处理Binary形式的数字。
注意,当$size = 3时,应该将其补全到4 bytes然后按照4 bytes的形式处理。(v0.1.2 fix)
更多的PHP binary to int方法可见 pmmp/BinaryUtils 库中的方法,本处仅择取了部分所需的字段方法。而读取UTF-8编码的字符串则使用下述方法,考虑到编码,循环次数应该为3的倍数,但实际仍每次读入一个字节并使用
chr()返回指定的字符。1
2
3
4
5
6
7
8
9
10
11
12private function readRawText(int $start): string
{
$bs = '';
# 使用循环读取,0为终止
while (0 != $p = $this->readInt($start, 1)) {
$bs .= chr($p);
$start += 1;
}
return $bs;
}解决了上面两个问题,其他的则较为简单,直接从Python的实现照抄就行,无别的需要折腾的地方。
