背景
在不借助第三方工具的前提下,实现IP高效定位地理位置(这里以ipv6为例子)
设计思路
- 需要将IP转换成一个整数类型
- 根据IP文件(ipv6.txt)生成一个索引文件
- head:存储IP索引的起始偏移量和结束偏移量:起始偏移量和结束偏移量各占4个字节,共8个字节
- data:存储IP的详细信息,长度根据ipv6的具体数据大小决定,结束时用\x00提示
- index:索引信息,起始IP的值(38个字节)和详细信息的位置偏移量(4个字节),共42个字节
- 使用二分查找法对比查找IP和IP文件中的IP范围
- 首先获取head中内容,得到起始和结束索引的位置偏移量
- 然后根据这个偏移量获取到IP的索引位置,加上二分法通过与起始IP对比
- 找到后通过位置偏移量获取到最终地理位置
代码实现
<?php
class Ug_Util_Ipsss {
private $forceReForm = false; // 是否强制重新生成索引文件
private $filename = "/colombo_ipv6lib.txt"; //保存IP地址的文件
private $head = array(); // 0起始IP的文件开始位置,1起始IP的文件结束位置
private $index = array();
private $data = array(); //数据信息的位置
private $start_data_offset = 8; // 起始偏移量
private $index_len = 0; // 索引长度
CONST READ_64bit_OFFSET = 9;
CONST READ_32bit_OFFSET = 5;
CONST EVERY_INDEX_OFFSET = 42;
/**
* @description 初始化文件
* @param $filename
* @param $forceReForm
*/
public function __construct($filename = "", $forceReForm = false)
{
//变量赋值
$this->filename = empty($filename) ? $this->filename : $filename;
$this->forceReForm = $forceReForm;
$this->formatFile = "/ipv6";
//若强制重新生成索引标志为真或者不存在索引文件,则重新生成
if ($this->forceReForm || !file_exists($this->formatFile)) {
$this->ipv6FormatFile();
}
}
/**
* @description 用txt文件生成索引文件
*
*/
private function ipv6FormatFile()
{
//读源文件,写入到新的索引文件
$readfd = fopen($this->filename, 'rb');
$writefd = fopen($this->formatFile.'_tmp', 'wb+');
if ($readfd === false || $writefd === false) {
return false;
}
while (!feof($readfd)) {
$line = fgets($readfd);
if (empty($line)) {
continue;
}
$line_items = explode("|", $line);
//将起始IP转换为数字
if(filter_var($line_items[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$start_ip = $this->ipv6ToInt($line_items[0]);
} else {
$start_ip = intval($line_items[0]);
}
//将结束IP转换为数字
if(filter_var($line_items[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$line_items[1] = pack("A38", $this->ipv6ToInt($line_items[1]));
} else {
$line_items[1] = pack("A38", intval($line_items[1]));
}
//删除起始IP
unset($line_items[0]);
//1. 构造索引内容ip+该ip对应数据所存储的偏移量
//2. 头索引:索引内容 的偏移量,所以每次起始的数据偏移量要增加数据的长度
$tmp_index_offset = pack("A38L",$start_ip,$this->start_data_offset);
array_push($this->index, $tmp_index_offset);
$tmp_data = implode("|", $line_items) . '\x00';
array_push($this->data, $tmp_data);
$this->index_len = $this->index_len + strlen($tmp_index_offset);
$this->start_data_offset = $this->start_data_offset + strlen($tmp_data);
}
array_push($this->head, pack("L", $this->start_data_offset));
array_push($this->head, pack("L", $this->index_len + $this->start_data_offset - 8));
//将数据写到临时文件中
$this->write_array($writefd, $this->head);
$this->write_array($writefd, $this->data);
$this->write_array($writefd, $this->index);
fclose($readfd);
fclose($writefd);
rename($this->formatFile.'_tmp', $this->formatFile);
return true;
}
public function searchIpv6($ip = "")
{
$output = array("valid"=>false, "info"=>array(), "error_msg"=>"");
$fd = fopen($this->formatFile, "rb");
if(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$search_int_ip = $this->ipv6ToInt($ip);
} else {
return false;
}
//1. 读取head里面的偏移量信息
$head = unpack("Lleft/Lright", fgets($fd, 9));
$left = $head['left'];
$right = $head['right'];
while ($left <= $right) {
//计算索引个数
$index_count = ($right-$left+self::EVERY_INDEX_OFFSET)/self::EVERY_INDEX_OFFSET;
$index_middle = intval($index_count/2) < 1 ? 1 : intval($index_count/2);
$offset_middle = $left + ($index_middle - 1) * self::EVERY_INDEX_OFFSET ;
fseek($fd, $offset_middle, SEEK_SET);
//获取起始IP和详细信息的偏移量
//在这里读取的时候要用fread,fget读取会出错
$info = unpack("A38tmp_ip/Ltmp_offset", fread($fd, 43));
$start_ip = $info['tmp_ip'];
//读取结束IP的值
fseek($fd, $info['tmp_offset'], SEEK_SET);
$end_ip = unpack("A38ip", fread($fd, 39))['ip'];
if (bcsub($search_int_ip,$start_ip,0) < 0) {
$right = $offset_middle - self::EVERY_INDEX_OFFSET_IPV6;
} elseif (bcsub($search_int_ip,$end_ip,0) > 0){
$left = $offset_middle + self::EVERY_INDEX_OFFSET_IPV6;
} else {
$info_detail= fgets($fd);
$output['valid'] = true;
$output['info'] = explode("|", $info_detail);
$output['info'][count($output['info']) - 1] =
trim($output['info'][count($output['info']) - 1], PHP_EOL);
unset($output['info'][0]);
goto final_out;
}
}
fclose($fd);
$output['valid'] = false;
$output['info'] = "NO IP FOUND";
final_out:
return $output;
}
/**
* @description 将IPv4地址转换为整型
* @param string $ip
* @return int
*/
private function ip2int($ip)
{
return sprintf("%u", ip2long($ip));
}
/**
* @description 将IPv6地址转换为string (int会溢出)
* @param string $ip
* @return int
*/
public function ipv6ToInt($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;
}
}