Как стать автором
Обновить

IP Tool — База данных IP адресов

Время на прочтение8 мин
Количество просмотров15K

Вступление


Долгое время я пользовался библиотекой SxGeo от zapimir. И до недавнего времени меня всё устраивало. Устраивало до тех пор, пока не было необходимости добавлять в БД свои данные.


Не найдя в интернете упаковщика данных от SxGeo и не найдя в себе силы требовать нужный мне функционал от разработчика, было принято решение писать свой костыль. Хотя на это решение повлиял и ещё 2 недостатка используемой библиотеки:


  • ограничение по количеству справочников;
  • невозможность узнать интервал адресов, в который входит искомый адрес;
  • отсутствие пакета в packagist.

Собственно, делюсь с вами своей разработкой.


Отличия между прототипом и моим решением:


  • IPTool — это всего лишь инструмент для создания базы данных и поиска в ней, в то время, как проект SxGeo — проект, предоставляющий не только инструментарий, но и сами базы данных;
  • База данных IPTool занимает больше места (т.к. первый адрес диапазона хранится полностью и занимает 4 байта, в то время, как в SxGeo только 3 байта);
  • IPTool имеет только один режим — чтение данных с диска (Режим подгрузки базы в память — в планах);
  • Помимо данных, IPTool возвращает диапазон IP адресов, в который входит искомый адрес;
  • IPTool предусматривает методы получения данных из справочников (всех или по порядковому номеру);
  • В базе данных IPTool предусмотрена возможность лицензирования самой базы данных;
  • IPTool легко устанавливается с помощью Composer;

Использование


Инициализация IP Tool


/* Путь к базе данных - /path/to/iptool.database */
$iptool = new \Ddrv\Iptool\Iptool('/path/to/iptool.database');

Получение информации о базе данных


print_r($iptool->about());

Array
(
    [created] => 1507199627
    [author] => Anonymous Author
    [license] => MIT
    [networks] => Array
        (
            [count] => 276148
            [data] => Array
                (
                    [country] => Array
                        (
                            [0] => code
                            [1] => name
                        )
                )
        )
)

Поиск информации об IP адресе


print_r($iptool->find('81.32.17.89'));

Array
(
    [network] => Array
        (
            [0] => 81.32.0.0
            [1] => 81.48.0.0
        )
    [data] => Array
        (
            [country] => Array
                (
                    [code] => es
                    [name] => Spain
                )
        )
)

Получить все элементы справочника


print_r($iptool->getRegister('country'));

Array
(
    [1] => Array
        (
            [code] => cn
            [name] => China
        )
    [2] => Array
        (
            [code] => es
            [name] => Spain
        )
...
    [N] => Array
        (
            [code] => jp
            [name] => Japan
        )
)

Получение элемента справочника по его порядковому номеру


print_r($iptool->getRegister('country',2));

Array
    (
        [code] => cn
        [name] => China
    )
)

Процесс создания БД более трудоёмкий, но он описан с документации, которая доступна в репозитории и в wiki GitHub'а на русском и ломаном английском.


UPD1. Сравнение скорости работы IPTool и SxGeo


Для большей достоверности результатов, я создал БД для IPTool на основе данных SxGeo


Подготовка к сравнительному тесту


$ cd /path/to/test/dir
$ mkdir csv
$ mkdir csv/sxgeo
$ mkdir t

необходимо скопировать файлы SxGeo.php и SxGeoCity.dat в текущую директорию (/path/to/test/dir)


Установка IPTool


$ composer require ddrv/iptool:~1.0

Импорт БД SxGeo в csv файлы


import.php
<?php
/* Импорт БД SxGeo в csv файлы */
include_once __DIR__.DIRECTORY_SEPARATOR.'SxGeo.php';
class ExtSxGeo extends SxGeo
{
    public function parseBase() 
    {
        $s=0;
        $firstIp = '0.0.0.0';
        $seek = 0;
        $data = $this->parseCity($seek,1);
        $sxNet = fopen(__DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxNet.csv','w');
        $sxCnt = fopen(__DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxCnt.csv','w');
        $sxRgn = fopen(__DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxRgn.csv','w');
        $sxCts = fopen(__DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxCts.csv','w');
        $ids = [
            'cnt' => [],
            'rgn' => [],
            'cts' => [],
        ];
        for ($octet=1;$octet<=223;$octet++) {
            $bip = pack('C',$octet);
            $min = $this->b_idx_arr[$octet-1];
            $max = $this->b_idx_arr[$octet];
            for ($b=$min; $b<=$max;$b++) {
                fseek($this->fh, $this->db_begin + $b * $this->block_len);
                $block = fread($this->fh, $this->block_len);
                $i = unpack('C4',$bip.substr($block,0,3));
                $ip = implode('.',$i);
                $lastIp = long2ip(ip2long($ip)-1);
                $csvNet = [
                    $firstIp,
                    $lastIp,
                    $data['city']['id'],
                    $data['region']['id'],
                    $data['country']['id'],
                ];
                fputcsv($sxNet,$csvNet);
                if (!isset($ids['cts'][$data['city']['id']])) {
                    $ids['cts'][$data['city']['id']] = true;
                    $csvCts = [
                        $data['city']['id'],
                        $data['city']['lat'],
                        $data['city']['lon'],
                        $data['city']['name_ru'],
                        $data['city']['name_en'],
                    ];
                    fputcsv($sxCts,$csvCts);
                }
                if (!isset($ids['rgn'][$data['region']['id']])) {
                    $ids['rgn'][$data['region']['id']] = true;
                    $csvRgn = [
                        $data['region']['id'],
                        $data['region']['iso'],
                        $data['region']['name_ru'],
                        $data['region']['name_en'],
                    ];
                    fputcsv($sxRgn,$csvRgn);
                }
                if (!isset($ids['cnt'][$data['country']['id']])) {
                    $ids['cnt'][$data['country']['id']] = true;
                    $csvCnt = [
                        $data['country']['id'],
                        $data['country']['iso'],
                        $data['country']['lat'],
                        $data['country']['lon'],
                        $data['country']['name_ru'],
                        $data['country']['name_en'],
                    ];
                    fputcsv($sxCnt,$csvCnt);
                }
                $firstIp = $ip;
                $seek = hexdec(bin2hex(substr($block, $this->block_len - $this->id_len, $this->id_len)));
                $data = $this->parseCity($seek,1);
            }
        }
        $lastIp = '255.255.255.255';
        $csvNet = [
            $firstIp,
            $lastIp,
            $data['city']['id'],
            $data['region']['id'],
            $data['country']['id'],
        ];
        fputcsv($sxNet,$csvNet);
        if (!isset($ids['cts'][$data['city']['id']])) {
            $ids['cts'][$data['city']['id']] = true;
            $csvCts = [
                $data['city']['id'],
                $data['city']['lat'],
                $data['city']['lon'],
                $data['city']['name_ru'],
                $data['city']['name_en'],
            ];
            fputcsv($sxCts,$csvCts);
        }
        if (!isset($ids['rgn'][$data['region']['id']])) {
            $ids['rgn'][$data['region']['id']] = true;
            $csvRgn = [
                $data['region']['id'],
                $data['region']['iso'],
                $data['region']['name_ru'],
                $data['region']['name_en'],
            ];
            fputcsv($sxRgn,$csvRgn);
        }
        if (!isset($ids['cnt'][$data['country']['id']])) {
            $ids['cnt'][$data['country']['id']] = true;
            $csvCnt = [
                $data['country']['id'],
                $data['country']['iso'],
                $data['country']['lat'],
                $data['country']['lon'],
                $data['country']['name_ru'],
                $data['country']['name_en'],
            ];
            fputcsv($sxCnt,$csvCnt);
        }
        fclose($sxNet);
        fclose($sxCnt);
        fclose($sxRgn);
        fclose($sxCts);
    }
}
$sxgeo = new ExtSxGeo( __DIR__.DIRECTORY_SEPARATOR.'SxGeoCity.dat',2);
$sxgeo->parseBase();

Запускаем скрипт и ждём.


$ php import.php

Создание БД IPTool из полученных csv файлов


convert.php
<?php
/* Создание БД IPTool из полученных csv файлов */
require_once(__DIR__.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php');

/* Используем директорию для хранения временных файлов. У скрипта должны быть права на запись в эту директорию. */
$tmpDir = __DIR__.'/t';

/* Инициализируем класс Converter. */
$converter = new \Ddrv\Iptool\Converter($tmpDir);

/* Указываем путь для сохранения БД. Скрипт должен иметь права на запись этого файла. */
$dbFile = __DIR__.DIRECTORY_SEPARATOR.'iptool.sxgeo.city.dat';

/* Запоминаем в переменные пути к нужным CSV файлам. */
$sxNet = __DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxNet.csv';
$sxCnt = __DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxCnt.csv';
$sxRgn = __DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxRgn.csv';
$sxCts = __DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxCts.csv';

/* Устанавливаем инфорацию об авторе. */
$converter->setAuthor('Ivan Dudarev');

/* Указываем лицензию. */
$converter->setLicense('MIT');

/* Добавляем исходники в формате CSV. */
$converter->addCSV('sxNet',$sxNet);
$converter->addCSV('sxCnt',$sxCnt);
$converter->addCSV('sxRgn',$sxRgn);
$converter->addCSV('sxCts',$sxCts);

/* Описываем справочник Country. */
$country = array(
    'id' => array(
        'type' => 'int',
        'column' => 0,
    ),
    'iso' => array(
        'type' => 'string',
        'column' => 1,
        'transform' => 'low',
    ),
    'lat' => array(
        'type' => 'double',
        'column' => 2,
    ),
    'lon' => array(
        'type' => 'double',
        'column' => 3,
    ),
    'nameRu' => array(
        'type' => 'string',
        'column' => 4,
    ),
    'nameEn' => array(
        'type' => 'string',
        'column' => 5,
    ),
);
$converter->addRegister('country','sxCnt',0, $country);

/* Описываем справочник Region. */
$region = array(
    'id' => array(
        'type' => 'int',
        'column' => 0,
    ),
    'iso' => array(
        'type' => 'string',
        'column' => 1,
        'transform' => 'low',
    ),
    'nameRu' => array(
        'type' => 'string',
        'column' => 2,
    ),
    'nameEn' => array(
        'type' => 'string',
        'column' => 3,
    ),
);
$converter->addRegister('region','sxRgn',0, $region);

/* Описываем справочник City. */
$city = array(
    'id' => array(
        'type' => 'int',
        'column' => 0,
    ),
    'lat' => array(
        'type' => 'double',
        'column' => 1,
    ),
    'lon' => array(
        'type' => 'double',
        'column' => 2,
    ),
    'nameRu' => array(
        'type' => 'string',
        'column' => 3,
    ),
    'nameEn' => array(
        'type' => 'string',
        'column' => 4,
    ),
);
$converter->addRegister('city','sxCts',0, $city);

/* Описываем диапазоны. */
$data = array(
    'city' => 2,
    'region' => 3,
    'country' => 4,
);
$converter->addNetworks('sxNet', 'ip', 0, 1, $data);

$errors = $converter->getErrors();
if (!$errors) {
    $converter->create($dbFile);
} else {
    print_r($errors);
}

Запускаем скрипт и ждём.


$ php convert.php

Сравнение величины БД


$ ls -l *.dat
...
-rw-r--r-- 1 www www 13435116 Jun 30 15:46 SxGeoCity.dat
-rw-r--r-- 1 www www 33190825 Oct 12 06:40 iptool.sxgeo.city.dat
...

Объём базы IPTool больше в 3 раза (что не есть плюс)


compare.php
<?php
require_once(__DIR__.DIRECTORY_SEPARATOR.'Iptool.php');
require_once(__DIR__.DIRECTORY_SEPARATOR.'SxGeo.php');
$dbFile = __DIR__.DIRECTORY_SEPARATOR.'iptool.sxgeo.city.dat';
$iptool = new \Ddrv\Iptool\Iptool($dbFile);
$sxgeo = new SxGeo( __DIR__.DIRECTORY_SEPARATOR.'SxGeoCity.dat',2);

/* Готовим данные для теста */
$ips = [];
for ($i=0;$i<100;$i++) {
    $ipa = [];
    for($octet = 0;$octet<4;$octet++) {
        $ipa[] = rand(0,255);
    }
    $ip = implode('.',$ipa);
    $ips[] = $ip;
}
/* IPTool */
$res = [];
$t1 = microtime(true);
foreach ($ips as $ip) {
    $res[] = $iptool->find($ip);
}
$t2 = microtime(true);
echo 'IP Tool : '.($t2-$t1).PHP_EOL;
/* SxGeo */
$res = [];
$t1 = microtime(true);
foreach ($ips as $ip) {
    $res[] = $sxgeo->getCityFull($ip);
}
$t2 = microtime(true);
echo 'SxGeo   : '.($t2-$t1).PHP_EOL;

Сравненительный тест скорости iptool-1.0.6 и SxGeo-2.2.3


$ php compare.php

Результат трёх тестов по 100 адресов


IP Tool : 0.026905059814453
SxGeo   : 0.031632900238037

IP Tool : 0.025413036346436
SxGeo   : 0.023004055023193

IP Tool : 0.016932010650635
SxGeo   : 0.022341012954712

Результат трёх тестов по 1 адресу


IP Tool : 0.0013048648834229
SxGeo   : 0.00016021728515625

IP Tool : 0.00047779083251953
SxGeo   : 0.00011301040649414

IP Tool : 0.00046205520629883
SxGeo   : 0.00035595893859863

UPD2. В версии 1.0.7 алгоритм поиска переведён на бинарный поиск


Сравненительный тест скорости iptool-1.0.7 и SxGeo-2.2.3


$ php compare.php

Результат трёх тестов по 100 адресов


IP Tool : 0.012892961502075
SxGeo   : 0.033740043640137

IP Tool : 0.0073931217193604
SxGeo   : 0.032436847686768

IP Tool : 0.0043089389801025
SxGeo   : 0.028012990951538

Результат трёх тестов по 1 адресу


IP Tool : 0.0011000633239746
SxGeo   : 0.0009000301361084

IP Tool : 0.00040006637573242
SxGeo   : 0.00079989433288574

IP Tool : 0.00030016899108887
SxGeo   : 0.00020003318786621

Вывод


Нужно работать над размером БД;


  • реализовать связь между справочниками, это заметно сократит размер базы диапазонов;
  • склеивать интервалы с одинаковыми данными (хотя в данной БД таковых нет, они взяты из SxGeo как есть);
  • хранить начальные адреса диапазонов в виде 3х байт, как в SxGeo.

UPD 3


На некоторых проектах конвертер не нужен (база генерится в одном проекте и дублируется в другие), что добавляло лишнюю зависимость в проект (pdo_sqlite). В связи с этим было принято решение разделить библиотек на 2 проекта. Ну и под шумок сменилось пространство имён.


Теперь проект живёт здесь:


Создание базы GitHub Packagist
Поиск по базе GitHub Packagist

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
0
Комментарии9

Публикации

Истории

Работа

PHP программист
157 вакансий

Ближайшие события