mirror of
https://github.com/chinayin/ip2region-core-php.git
synced 2025-12-02 19:42:48 +08:00
commit
8e9a957bad
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# EditorConfig is awesome: http://EditorConfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[**.{php,md}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/.gitattributes export-ignore
|
||||||
|
/.gitignore export-ignore
|
||||||
|
/.github export-ignore
|
||||||
|
/.php-cs-fixer.php export-ignore
|
||||||
|
/.editorconfig export-ignore
|
||||||
|
/phpunit.xml export-ignore
|
||||||
|
/phpunit.xml.dist export-ignore
|
||||||
|
/phpstan.neon export-ignore
|
||||||
|
/tests export-ignore
|
||||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
target-branch: "2.x"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
|
- package-ecosystem: "composer"
|
||||||
|
directory: "/"
|
||||||
|
target-branch: "2.x"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
42
.github/workflows/php.yml
vendored
Normal file
42
.github/workflows/php.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: PHP Composer
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: build (PHP ${{ matrix.php-versions }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php-versions: [ '7.2', '7.4', '8.0', '8.1' ]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup PHP, with composer and extensions
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php-versions }}
|
||||||
|
tools: pecl
|
||||||
|
extensions: mbstring, dom
|
||||||
|
|
||||||
|
- name: Get composer cache directory
|
||||||
|
id: composer-cache
|
||||||
|
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||||
|
|
||||||
|
- name: Cache composer dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
|
||||||
|
restore-keys: ${{ runner.os }}-composer-
|
||||||
|
|
||||||
|
- name: Validate composer.json and composer.lock
|
||||||
|
run: composer validate
|
||||||
|
|
||||||
|
- name: Install Composer dependencies
|
||||||
|
run: composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader
|
||||||
|
|
||||||
|
- name: Run test suite
|
||||||
|
run: composer travis
|
||||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/vendor/
|
||||||
|
/composer.lock
|
||||||
|
.idea
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
.DS_Store
|
||||||
|
.php-cs-fixer.cache
|
||||||
|
phpunit.xml
|
||||||
36
.php-cs-fixer.php
Normal file
36
.php-cs-fixer.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (!file_exists(__DIR__ . '/src')) {
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileHeaderComment = <<<'EOF'
|
||||||
|
This file is part of the Ip2Region package.
|
||||||
|
|
||||||
|
Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
Use of this source code is governed by a Apache2.0-style
|
||||||
|
license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
@link https://github.com/lionsoul2014/ip2region
|
||||||
|
|
||||||
|
For the full copyright and license information, please view the LICENSE
|
||||||
|
file that was distributed with this source code.
|
||||||
|
EOF;
|
||||||
|
|
||||||
|
return (new PhpCsFixer\Config())
|
||||||
|
->setUsingCache(true)
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setRules([
|
||||||
|
'@PHP71Migration' => true,
|
||||||
|
'@PHPUnit75Migration:risky' => true,
|
||||||
|
'@PSR12' => true,
|
||||||
|
'header_comment' => ['header' => $fileHeaderComment],
|
||||||
|
])
|
||||||
|
->setFinder(
|
||||||
|
PhpCsFixer\Finder::create()
|
||||||
|
->ignoreVCSIgnored(true)
|
||||||
|
->files()
|
||||||
|
->name('*.php')
|
||||||
|
->exclude('vendor')
|
||||||
|
->in(__DIR__)
|
||||||
|
);
|
||||||
99
README.md
Normal file
99
README.md
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# ip2region SDK for PHP
|
||||||
|
|
||||||
|
[](https://github.com/chinayin)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://packagist.org/packages/chinayin/ip2region-core)
|
||||||
|
[](https://packagist.org/packages/chinayin/ip2region-core)
|
||||||
|

|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
运行环境要求 PHP 7.1 及以上版本,以及[cURL](http://php.net/manual/zh/book.curl.php)。
|
||||||
|
|
||||||
|
#### 官方原生查询包
|
||||||
|
|
||||||
|
特点:包更小,数据路径自定义
|
||||||
|
|
||||||
|
> composer require chinayin/ip2region-core
|
||||||
|
|
||||||
|
#### 包含数据查询包
|
||||||
|
|
||||||
|
特点:`xdb数据`封装在composer包内,数据会不定期更新
|
||||||
|
|
||||||
|
使用方法:[github.com/chinayin/ip2region](https://github.com/chinayin/ip2region-sdk-php)
|
||||||
|
|
||||||
|
> composer require chinayin/ip2region
|
||||||
|
|
||||||
|
### Quick Examples
|
||||||
|
|
||||||
|
#### 完全基于文件的查询
|
||||||
|
|
||||||
|
```php
|
||||||
|
use ip2region\XdbSearcher;
|
||||||
|
|
||||||
|
$ip = '1.2.3.4';
|
||||||
|
$xdb = './ip2region.xdb';
|
||||||
|
try {
|
||||||
|
$region = XdbSearcher::newWithFileOnly($xdb)->search($ip);
|
||||||
|
var_dump($region);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
var_dump($e->getMessage());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 备注:并发使用,每个线程或者协程需要创建一个独立的 searcher 对象。
|
||||||
|
|
||||||
|
#### 缓存 VectorIndex 索引
|
||||||
|
|
||||||
|
如果你的 php 母环境支持,可以预先加载 vectorIndex 缓存,然后做成全局变量,每次创建 Searcher 的时候使用全局的
|
||||||
|
vectorIndex,可以减少一次固定的 IO 操作从而加速查询,减少 io 压力。
|
||||||
|
|
||||||
|
```php
|
||||||
|
use ip2region\XdbSearcher;
|
||||||
|
|
||||||
|
$ip = '1.2.3.4';
|
||||||
|
$xdb = './ip2region.xdb';
|
||||||
|
try {
|
||||||
|
// 1、加载 VectorIndex 缓存,把下述的 vIndex 变量缓存到内存里面。
|
||||||
|
$vIndex = XdbSearcher::loadVectorFromFile($xdb);
|
||||||
|
if (null === $vIndex) {
|
||||||
|
throw new \RuntimeException("failed to load vector index from '$xdb'.");
|
||||||
|
}
|
||||||
|
// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
|
||||||
|
$searcher = XdbSearcher::newWithVectorIndex($xdb, $vIndex);
|
||||||
|
// 3、查询
|
||||||
|
$region = $searcher->search($ip);
|
||||||
|
var_dump($region);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
var_dump($e->getMessage());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 备注:并发使用,每个线程或者协程需要创建一个独立的 searcher 对象,但是都共享统一的只读 vectorIndex。
|
||||||
|
|
||||||
|
#### 缓存整个 xdb 数据
|
||||||
|
|
||||||
|
如果你的 PHP 母环境支持,可以预先加载整个 xdb 的数据到内存,这样可以实现完全基于内存的查询,类似之前的 memory search 查询。
|
||||||
|
|
||||||
|
```php
|
||||||
|
use ip2region\XdbSearcher;
|
||||||
|
|
||||||
|
$ip = '1.2.3.4';
|
||||||
|
$xdb = './ip2region.xdb';
|
||||||
|
try {
|
||||||
|
// 1、加载整个 xdb 到内存。
|
||||||
|
$cBuff = XdbSearcher::loadContentFromFile($xdb);
|
||||||
|
if (null === $cBuff) {
|
||||||
|
throw new \RuntimeException("failed to load content buffer from '$xdb'");
|
||||||
|
}
|
||||||
|
// 2、使用全局的 cBuff 创建带完全基于内存的查询对象。
|
||||||
|
$searcher = XdbSearcher::newWithBuffer($cBuff);
|
||||||
|
// 3、查询
|
||||||
|
$region = $searcher->search($ip);
|
||||||
|
var_dump($region);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
var_dump($e->getMessage());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 备注:并发使用,用整个 xdb 缓存创建的 searcher 对象可以安全用于并发。
|
||||||
43
composer.json
Normal file
43
composer.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "chinayin/ip2region-core",
|
||||||
|
"description": "Ip2region (2.0 - xdb) is a offline IP address manager framework and locator with ten microsecond searching performance. xdb engine implementation for many programming languages\n\n",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "lionsoul2014",
|
||||||
|
"email": "1187582057@qq.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chinayin",
|
||||||
|
"email": "whereismoney@qq.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"require": {
|
||||||
|
"PHP": ">=7.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^6.0|^9.5",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.0",
|
||||||
|
"phpstan/phpstan": "^1.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ip2region\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"ip2region\\Tests\\": "tests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vendor/bin/phpunit",
|
||||||
|
"test-ci": "vendor/bin/phpunit --coverage-text",
|
||||||
|
"lint": "vendor/bin/php-cs-fixer fix -v",
|
||||||
|
"analyse": "vendor/bin/phpstan analyse",
|
||||||
|
"travis": [
|
||||||
|
"composer lint",
|
||||||
|
"composer analyse"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
phpstan.neon
Normal file
13
phpstan.neon
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
parameters:
|
||||||
|
level: 5
|
||||||
|
checkMissingIterableValueType: false
|
||||||
|
checkFunctionNameCase: true
|
||||||
|
reportUnmatchedIgnoredErrors: false
|
||||||
|
checkGenericClassInNonGenericObjectType: false
|
||||||
|
inferPrivatePropertyTypeFromConstructor: true
|
||||||
|
treatPhpDocTypesAsCertain: false
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests
|
||||||
|
ignoreErrors:
|
||||||
|
- '#PHPDoc tag .* has invalid value.*#'
|
||||||
25
phpunit.xml.dist
Normal file
25
phpunit.xml.dist
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit backupGlobals="false"
|
||||||
|
backupStaticAttributes="false"
|
||||||
|
bootstrap="./tests/bootstrap.php"
|
||||||
|
colors="true"
|
||||||
|
convertErrorsToExceptions="true"
|
||||||
|
convertNoticesToExceptions="true"
|
||||||
|
convertWarningsToExceptions="true"
|
||||||
|
processIsolation="false"
|
||||||
|
stopOnFailure="false"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="All">
|
||||||
|
<directory>./tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory suffix=".php">src/</directory>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
<php>
|
||||||
|
<env name="XDB_PATH" value="../assets/ip2region.xdb"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
368
src/XdbSearcher.php
Normal file
368
src/XdbSearcher.php
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Ip2Region package.
|
||||||
|
*
|
||||||
|
* Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
* Use of this source code is governed by a Apache2.0-style
|
||||||
|
* license that can be found in the LICENSE file.
|
||||||
|
*
|
||||||
|
* @link https://github.com/lionsoul2014/ip2region
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ip2region;
|
||||||
|
|
||||||
|
class XdbSearcher
|
||||||
|
{
|
||||||
|
public const HeaderInfoLength = 256;
|
||||||
|
public const VectorIndexRows = 256;
|
||||||
|
public const VectorIndexCols = 256;
|
||||||
|
public const VectorIndexSize = 8;
|
||||||
|
public const SegmentIndexSize = 14;
|
||||||
|
|
||||||
|
// xdb file handle
|
||||||
|
private $handle = null;
|
||||||
|
|
||||||
|
private $ioCount = 0;
|
||||||
|
|
||||||
|
// vector index in binary string.
|
||||||
|
// string decode will be faster than the map based Array.
|
||||||
|
private $vectorIndex = null;
|
||||||
|
|
||||||
|
// xdb content buffer
|
||||||
|
private $contentBuff = null;
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// static function to create searcher
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function newWithFileOnly($dbFile)
|
||||||
|
{
|
||||||
|
return new XdbSearcher($dbFile, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function newWithVectorIndex($dbFile, $vIndex)
|
||||||
|
{
|
||||||
|
return new XdbSearcher($dbFile, $vIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function newWithBuffer($cBuff)
|
||||||
|
{
|
||||||
|
return new XdbSearcher(null, null, $cBuff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- End of static creator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initialize the xdb searcher
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function __construct($dbFile, $vectorIndex = null, $cBuff = null)
|
||||||
|
{
|
||||||
|
// check the content buffer first
|
||||||
|
if ($cBuff != null) {
|
||||||
|
$this->vectorIndex = null;
|
||||||
|
$this->contentBuff = $cBuff;
|
||||||
|
} else {
|
||||||
|
// open the xdb binary file
|
||||||
|
$this->handle = fopen($dbFile, "r");
|
||||||
|
if ($this->handle === false) {
|
||||||
|
throw new \Exception("failed to open xdb file '%s'", $dbFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->vectorIndex = $vectorIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close()
|
||||||
|
{
|
||||||
|
if ($this->handle != null) {
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIOCount()
|
||||||
|
{
|
||||||
|
return $this->ioCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find the region info for the specified ip address
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function search($ip)
|
||||||
|
{
|
||||||
|
// check and convert the sting ip to a 4-bytes long
|
||||||
|
if (is_string($ip)) {
|
||||||
|
$t = self::ip2long($ip);
|
||||||
|
if ($t === null) {
|
||||||
|
throw new \Exception("invalid ip address `$ip`");
|
||||||
|
}
|
||||||
|
$ip = $t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the global counter
|
||||||
|
$this->ioCount = 0;
|
||||||
|
|
||||||
|
// locate the segment index block based on the vector index
|
||||||
|
$il0 = ($ip >> 24) & 0xFF;
|
||||||
|
$il1 = ($ip >> 16) & 0xFF;
|
||||||
|
$idx = $il0 * self::VectorIndexCols * self::VectorIndexSize + $il1 * self::VectorIndexSize;
|
||||||
|
if ($this->vectorIndex != null) {
|
||||||
|
$sPtr = self::getLong($this->vectorIndex, $idx);
|
||||||
|
$ePtr = self::getLong($this->vectorIndex, $idx + 4);
|
||||||
|
} else {
|
||||||
|
if ($this->contentBuff != null) {
|
||||||
|
$sPtr = self::getLong($this->contentBuff, self::HeaderInfoLength + $idx);
|
||||||
|
$ePtr = self::getLong($this->contentBuff, self::HeaderInfoLength + $idx + 4);
|
||||||
|
} else {
|
||||||
|
// read the vector index block
|
||||||
|
$buff = $this->read(self::HeaderInfoLength + $idx, 8);
|
||||||
|
if ($buff === null) {
|
||||||
|
throw new \Exception("failed to read vector index at ${idx}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$sPtr = self::getLong($buff, 0);
|
||||||
|
$ePtr = self::getLong($buff, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printf("sPtr: %d, ePtr: %d\n", $sPtr, $ePtr);
|
||||||
|
|
||||||
|
// binary search the segment index to get the region info
|
||||||
|
$dataLen = 0;
|
||||||
|
$dataPtr = null;
|
||||||
|
$l = 0;
|
||||||
|
$h = ($ePtr - $sPtr) / self::SegmentIndexSize;
|
||||||
|
while ($l <= $h) {
|
||||||
|
$m = ($l + $h) >> 1;
|
||||||
|
$p = $sPtr + $m * self::SegmentIndexSize;
|
||||||
|
|
||||||
|
// read the segment index
|
||||||
|
$buff = $this->read($p, self::SegmentIndexSize);
|
||||||
|
if ($buff == null) {
|
||||||
|
throw new \Exception("failed to read segment index at ${p}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$sip = self::getLong($buff, 0);
|
||||||
|
if ($ip < $sip) {
|
||||||
|
$h = $m - 1;
|
||||||
|
} else {
|
||||||
|
$eip = self::getLong($buff, 4);
|
||||||
|
if ($ip > $eip) {
|
||||||
|
$l = $m + 1;
|
||||||
|
} else {
|
||||||
|
$dataLen = self::getShort($buff, 8);
|
||||||
|
$dataPtr = self::getLong($buff, 10);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match nothing interception.
|
||||||
|
// @TODO: could this even be a case ?
|
||||||
|
// printf("dataLen: %d, dataPtr: %d\n", $dataLen, $dataPtr);
|
||||||
|
if ($dataPtr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load and return the region data
|
||||||
|
$buff = $this->read($dataPtr, $dataLen);
|
||||||
|
if ($buff == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read specified bytes from the specified index
|
||||||
|
private function read($offset, $len)
|
||||||
|
{
|
||||||
|
// check the in-memory buffer first
|
||||||
|
if ($this->contentBuff != null) {
|
||||||
|
return substr($this->contentBuff, $offset, $len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// read from the file
|
||||||
|
$r = fseek($this->handle, $offset);
|
||||||
|
if ($r == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ioCount++;
|
||||||
|
$buff = fread($this->handle, $len);
|
||||||
|
if ($buff === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($buff) != $len) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- static util functions ----
|
||||||
|
|
||||||
|
// convert a string ip to long
|
||||||
|
public static function ip2long($ip)
|
||||||
|
{
|
||||||
|
$ip = ip2long($ip);
|
||||||
|
if ($ip === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert signed int to unsigned int if on 32 bit operating system
|
||||||
|
if ($ip < 0 && PHP_INT_SIZE == 4) {
|
||||||
|
$ip = sprintf("%u", $ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a 4bytes long from a byte buffer
|
||||||
|
public static function getLong($b, $idx)
|
||||||
|
{
|
||||||
|
$val = (ord($b[$idx])) | (ord($b[$idx + 1]) << 8)
|
||||||
|
| (ord($b[$idx + 2]) << 16) | (ord($b[$idx + 3]) << 24);
|
||||||
|
|
||||||
|
// convert signed int to unsigned int if on 32 bit operating system
|
||||||
|
if ($val < 0 && PHP_INT_SIZE == 4) {
|
||||||
|
$val = sprintf("%u", $val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a 2bytes short from a byte buffer
|
||||||
|
public static function getShort($b, $idx)
|
||||||
|
{
|
||||||
|
return ((ord($b[$idx])) | (ord($b[$idx + 1]) << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// load header info from a specified file handle
|
||||||
|
public static function loadHeader($handle)
|
||||||
|
{
|
||||||
|
if (fseek($handle, 0) == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buff = fread($handle, self::HeaderInfoLength);
|
||||||
|
if ($buff === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read bytes length checking
|
||||||
|
if (strlen($buff) != self::HeaderInfoLength) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the decoded header info
|
||||||
|
return [
|
||||||
|
'version' => self::getShort($buff, 0),
|
||||||
|
'indexPolicy' => self::getShort($buff, 2),
|
||||||
|
'createdAt' => self::getLong($buff, 4),
|
||||||
|
'startIndexPtr' => self::getLong($buff, 8),
|
||||||
|
'endIndexPtr' => self::getLong($buff, 12)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// load header info from the specified xdb file path
|
||||||
|
public static function loadHeaderFromFile($dbFile)
|
||||||
|
{
|
||||||
|
$handle = fopen($dbFile, 'r');
|
||||||
|
if ($handle === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::loadHeader($handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load vector index from a file handle
|
||||||
|
public static function loadVectorIndex($handle)
|
||||||
|
{
|
||||||
|
if (fseek($handle, self::HeaderInfoLength) == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rLen = self::VectorIndexRows * self::VectorIndexCols * self::SegmentIndexSize;
|
||||||
|
$buff = fread($handle, $rLen);
|
||||||
|
if ($buff === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($buff) != $rLen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load vector index from a specified xdb file path
|
||||||
|
public static function loadVectorIndexFromFile($dbFile)
|
||||||
|
{
|
||||||
|
$handle = fopen($dbFile, 'r');
|
||||||
|
if ($handle === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::loadVectorIndex($handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the xdb content from a file handle
|
||||||
|
public static function loadContent($handle)
|
||||||
|
{
|
||||||
|
if (fseek($handle, 0, SEEK_END) == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = ftell($handle);
|
||||||
|
if ($size === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// seek to the head for reading
|
||||||
|
if (fseek($handle, 0) == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buff = fread($handle, $size);
|
||||||
|
if ($buff === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read length checking
|
||||||
|
if (strlen($buff) != $size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the xdb content from a file path
|
||||||
|
public static function loadContentFromFile($dbFile)
|
||||||
|
{
|
||||||
|
$str = file_get_contents($dbFile, false);
|
||||||
|
if ($str === false) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return $str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function now()
|
||||||
|
{
|
||||||
|
return (microtime(true) * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
tests/BenchTest.php
Normal file
85
tests/BenchTest.php
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Ip2Region package.
|
||||||
|
*
|
||||||
|
* Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
* Use of this source code is governed by a Apache2.0-style
|
||||||
|
* license that can be found in the LICENSE file.
|
||||||
|
*
|
||||||
|
* @link https://github.com/lionsoul2014/ip2region
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ip2region\Tests;
|
||||||
|
|
||||||
|
use ip2region\XdbSearcher;
|
||||||
|
|
||||||
|
class BenchTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testBench()
|
||||||
|
{
|
||||||
|
$cachePolicy = 'content';
|
||||||
|
$searcher = SearchTest::builder($cachePolicy);
|
||||||
|
|
||||||
|
$file = __DIR__ . '/../data/ip.merge.txt';
|
||||||
|
if (!file_exists($file)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($file, "r");
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
$costs = 0;
|
||||||
|
$ts = XdbSearcher::now();
|
||||||
|
while (!feof($handle)) {
|
||||||
|
$line = trim(fgets($handle, 1024));
|
||||||
|
if (strlen($line) < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ps = explode('|', $line, 3);
|
||||||
|
$this->assertCount(3, $ps);
|
||||||
|
|
||||||
|
$sip = XdbSearcher::ip2long($ps[0]);
|
||||||
|
$eip = XdbSearcher::ip2long($ps[1]);
|
||||||
|
$this->assertNotNull($sip);
|
||||||
|
$this->assertNotNull($eip);
|
||||||
|
$this->assertGreaterThanOrEqual($sip, $eip);
|
||||||
|
|
||||||
|
$mip = ($sip + $eip) >> 1;
|
||||||
|
foreach ([$sip, ($sip + $mip) >> 1, $mip, ($mip + $eip) >> 1, $eip] as $ip) {
|
||||||
|
try {
|
||||||
|
$cTime = XdbSearcher::now();
|
||||||
|
$region = $searcher->search($ip);
|
||||||
|
$costs += XdbSearcher::now() - $cTime;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
printf("failed to search ip `%s`\n", long2ip($ip));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertNotNull($region, sprintf("failed to search ip `%s`", long2ip($ip)));
|
||||||
|
|
||||||
|
// check the region info
|
||||||
|
$this->assertEquals(
|
||||||
|
$ps[2],
|
||||||
|
$region,
|
||||||
|
sprintf("failed search(%s) with (%s != %s)\n", long2ip($ip), $region, $ps[2])
|
||||||
|
);
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($handle);
|
||||||
|
$searcher->close();
|
||||||
|
printf(
|
||||||
|
"Bench finished, {cachePolicy: %s, total: %d, took: %ds, cost: %.3f ms/op}\n",
|
||||||
|
$cachePolicy,
|
||||||
|
$count,
|
||||||
|
(XdbSearcher::now() - $ts) / 1000,
|
||||||
|
$count == 0 ? 0 : $costs / $count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
tests/LoadTest.php
Normal file
43
tests/LoadTest.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Ip2Region package.
|
||||||
|
*
|
||||||
|
* Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
* Use of this source code is governed by a Apache2.0-style
|
||||||
|
* license that can be found in the LICENSE file.
|
||||||
|
*
|
||||||
|
* @link https://github.com/lionsoul2014/ip2region
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ip2region\Tests;
|
||||||
|
|
||||||
|
use ip2region\XdbSearcher;
|
||||||
|
|
||||||
|
class LoadTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testLoadHeader()
|
||||||
|
{
|
||||||
|
$header = XdbSearcher::loadHeaderFromFile(getenv('XDB_PATH'));
|
||||||
|
printf("header loaded, ");
|
||||||
|
print_r($header);
|
||||||
|
$this->assertNotNull($header);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLoadVectorIndex()
|
||||||
|
{
|
||||||
|
$vIndex = XdbSearcher::loadVectorIndexFromFile(getenv('XDB_PATH'));
|
||||||
|
printf("vector index loaded, length=%d\n", strlen($vIndex));
|
||||||
|
$this->assertNotNull($vIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLoadContent()
|
||||||
|
{
|
||||||
|
$cBuff = XdbSearcher::loadContentFromFile(getenv('XDB_PATH'));
|
||||||
|
printf("content loaded, length=%d\n", strlen($cBuff));
|
||||||
|
$this->assertNotNull($cBuff);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
tests/SearchTest.php
Normal file
64
tests/SearchTest.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Ip2Region package.
|
||||||
|
*
|
||||||
|
* Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
* Use of this source code is governed by a Apache2.0-style
|
||||||
|
* license that can be found in the LICENSE file.
|
||||||
|
*
|
||||||
|
* @link https://github.com/lionsoul2014/ip2region
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ip2region\Tests;
|
||||||
|
|
||||||
|
use ip2region\XdbSearcher;
|
||||||
|
|
||||||
|
class SearchTest extends TestCase
|
||||||
|
{
|
||||||
|
private $ips = [
|
||||||
|
'220.181.38.251' => '中国|0|北京|北京市|电信',
|
||||||
|
'123.151.137.18' => '中国|0|天津|天津市|电信',
|
||||||
|
'103.100.62.111' => '中国|0|香港|0|0',
|
||||||
|
'20.205.243.166' => '美国|0|0|0|微软',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function builder($cachePolicy)
|
||||||
|
{
|
||||||
|
$file = getenv('XDB_PATH');
|
||||||
|
if ('vectorIndex' === $cachePolicy) {
|
||||||
|
return XdbSearcher::newWithVectorIndex($file, XdbSearcher::loadVectorIndexFromFile($file));
|
||||||
|
} elseif ('content' === $cachePolicy) {
|
||||||
|
return XdbSearcher::newWithBuffer(XdbSearcher::loadContentFromFile($file));
|
||||||
|
}
|
||||||
|
return XdbSearcher::newWithFileOnly($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function search($searcher, $ip, $expected)
|
||||||
|
{
|
||||||
|
$ts = self::now();
|
||||||
|
$r = $searcher->search($ip);
|
||||||
|
printf(
|
||||||
|
"ip: %s, region: %s, ioCount: %d, took: %.5f ms\n",
|
||||||
|
$ip,
|
||||||
|
$r,
|
||||||
|
$searcher->getIOCount(),
|
||||||
|
self::now() - $ts
|
||||||
|
);
|
||||||
|
$this->assertEquals($expected, $r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSearch()
|
||||||
|
{
|
||||||
|
foreach (['file', 'vectorIndex', 'content'] as $cachePolicy) {
|
||||||
|
printf("\ncachePolicy = %s\n", $cachePolicy);
|
||||||
|
$searcher = $this->builder($cachePolicy);
|
||||||
|
foreach ($this->ips as $ip => $expected) {
|
||||||
|
$this->search($searcher, $ip, $expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
tests/TestCase.php
Normal file
25
tests/TestCase.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Ip2Region package.
|
||||||
|
*
|
||||||
|
* Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
* Use of this source code is governed by a Apache2.0-style
|
||||||
|
* license that can be found in the LICENSE file.
|
||||||
|
*
|
||||||
|
* @link https://github.com/lionsoul2014/ip2region
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ip2region\Tests;
|
||||||
|
|
||||||
|
class TestCase extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
public static function now()
|
||||||
|
{
|
||||||
|
return microtime(true) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
tests/bootstrap.php
Normal file
16
tests/bootstrap.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Ip2Region package.
|
||||||
|
*
|
||||||
|
* Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
* Use of this source code is governed by a Apache2.0-style
|
||||||
|
* license that can be found in the LICENSE file.
|
||||||
|
*
|
||||||
|
* @link https://github.com/lionsoul2014/ip2region
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
Loading…
x
Reference in New Issue
Block a user