Merge pull request #492 from Alice39s/feature/erlang-ipv6

feat(binding/erlang): implement IPv6 dual-stack support for Erlang binding
This commit is contained in:
Leon / 狮子的魂 2026-06-28 10:09:08 +08:00 committed by GitHub
commit c4eb1a14ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 944 additions and 307 deletions

3
.gitignore vendored
View File

@ -89,3 +89,6 @@ target
#vscode #vscode
.vscode .vscode
build build
# git worktrees
.worktrees/

18
binding/erlang/Makefile Normal file
View File

@ -0,0 +1,18 @@
.PHONY: all compile test clean bench-v4 bench-v6
all: compile
compile:
rebar3 compile
test:
rebar3 eunit
clean:
rebar3 clean
bench-v4:
sh benchmarks/xdb-benchmark.sh ipv4
bench-v6:
sh benchmarks/xdb-benchmark.sh ipv6

View File

@ -1,10 +1,10 @@
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md) :globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
# ip2region erlang query client # ip2region Erlang query client
### Introduction ### Introduction
This binding implements the xdb query client in Erlang, based on the Erlang OTP Application. The query logic is implemented by the `ip2region_worker` worker process, supporting multiple worker processes for load balancing. This binding implements the xdb query client in `Erlang`, based on the `Erlang OTP Application`. The query logic is implemented by the `ip2region_worker` worker process, supporting multiple worker processes for load balancing.
### Application Configuration ### Application Configuration
@ -15,13 +15,35 @@ The configurable parameters for this application are in `ip2region.app.src`, as
{poolargs, [ {poolargs, [
{size, 1}, %% Default number of worker processes {size, 1}, %% Default number of worker processes
{max_overflow, 5} %% Maximum number of worker processes {max_overflow, 5} %% Maximum number of worker processes
]},
{db, [
{ipv4, "ip2region.xdb"} %% Default IPv4 xdb file
]} ]}
]} ]}
``` ```
### Dual-stack configuration (IPv4 + IPv6)
To enable IPv6 queries, add the `ipv6` entry to the `db` list and place both xdb files under `priv/`:
```erlang
{env,[
{poolargs, [
{size, 1},
{max_overflow, 5}
]},
{db, [
{ipv4, "ip2region.xdb"},
{ipv6, "ip2region_v6.xdb"}
]}
]}
```
The `xdb:search/1` interface automatically detects IPv4 and IPv6 inputs and routes them to the correct worker pool.
### Compile ### Compile
``` ```bash
$ rebar3 compile $ rebar3 compile
``` ```
@ -29,113 +51,130 @@ $ rebar3 compile
Place the xdb file in the `priv` directory, then start the Erlang node: Place the xdb file in the `priv` directory, then start the Erlang node:
``` ```bash
$ rebar3 shell $ rebar3 shell
``` ```
Call the `xdb:search/1` interface in the Erlang shell to query IP address information. This interface supports IP addresses represented as list strings, binary strings, tuples, and integers, as follows: Call the `xdb:search/1` interface in the Erlang shell to query IP address information. This interface supports IP addresses represented as list strings, binary strings, tuples, and integers:
``` ```erlang
1> xdb:search("1.0.8.0"). 1> xdb:search("1.0.8.0").
[20013,22269,124,48,124,24191,19996,30465,124,24191,24030, [20013,22269,124,24191,19996,30465,124,24191,24030,24066,
24066,124,30005,20449] 124,20013,22269,30005,20449,124,67,78]
2> 2>
3> io:format("~ts~n", [xdb:search("1.0.8.0")]). 3> io:format("~ts~n", [xdb:search("1.0.8.0")]).
中国|0|广东省|广州市|电信 中国|广东省|广州市|中国电信|CN
io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]). 4> io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]).
中国|0|广东省|广州市|电信 中国|广东省|广州市|中国电信|CN
4> io:format("~ts~n", [xdb:search({1,0,8,0})]). 5> io:format("~ts~n", [xdb:search({1,0,8,0})]).
中国|0|广东省|广州市|电信 中国|广东省|广州市|中国电信|CN
6> io:format("~ts~n", [xdb:search(16779264)]). 6> io:format("~ts~n", [xdb:search(16779264)]).
中国|0|广东省|广州市|电信 中国|广东省|广州市|中国电信|CN
```
With dual-stack enabled, IPv6 addresses are supported in the same way:
```erlang
1> io:format("~ts~n", [xdb:search("2001:4860:4860::8888")]).
United States|Florida|Miami|Google LLC|US
2> io:format("~ts~n", [xdb:search(<<"2001:4860:4860::8888">>)]).
United States|Florida|Miami|Google LLC|US
3> io:format("~ts~n", [xdb:search({8193,18528,18528,0,0,0,0,34952})]).
United States|Florida|Miami|Google LLC|US
``` ```
### Usage ### Usage
* Add the dependency in `rebar.config` - Add the dependency in `rebar.config`
``` ```erlang
{deps, [ {deps, [
ip2region ip2region
]}. ]}.
``` ```
* Start the ip2region Application - Start the ip2region Application
``` ```erlang
...... {ok, _} = application:ensure_all_started(ip2region).
application:ensure_started(ip2region),
......
``` ```
* Call the `xdb:search/1` interface to query IP information - Call the `xdb:search/1` interface to query IP information
``` ```erlang
...... xdb:search("1.0.8.0").
ip2region:search("1.0.8.0"),
......
``` ```
### Unit Test ### Unit Test
``` ```bash
$ rebar3 eunit $ rebar3 eunit
===> Verifying dependencies... ===> Verifying dependencies...
===> Analyzing applications... ===> Analyzing applications...
===> Compiling ip2region ===> Compiling ip2region
===> Performing EUnit tests... ===> Performing EUnit tests...
=INFO REPORT==== 17-Jan-2023::11:52:59.920155 === =INFO REPORT==== 28-Jun-2026::04:53:28 ===
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb
.... ....
Finished in 0.074 seconds Finished in 0.192 seconds
4 tests, 0 failures 63 tests, 0 failures
``` ```
### Benchmark ### Benchmark
``` Both IPv4 and IPv6 benchmarks share the same script. Run it with the desired IP version:
> `cold` = first pass over the source file: each IP triggers a real search and the
> result is written into the ETS cache. `warm` = second pass over the same list,
> where every lookup is served directly from the ETS cache.
```bash
$ cd benchmarks/ $ cd benchmarks/
$ sh xdb-benchmark.sh $ sh xdb-benchmark.sh ipv4
===> Verifying dependencies... ```
===> Analyzing applications...
===> Compiling ip2region For IPv6:
Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
```bash
Eshell V12.3.2.2 (abort with ^G) $ sh xdb-benchmark.sh ipv6
1> =INFO REPORT==== 17-Jan-2023::11:37:35.631095 === ```
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/default/lib/ip2region/priv/ip2region.xdb
Or use the Makefile targets from the `binding/erlang` directory:
===> Booted ip2region
===> Evaluating: "xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()." ```bash
CPU info: $ make bench-v4
model name : AMD EPYC 7K62 48-Core Processor $ make bench-v6
cache size : 512 KB ```
cpu MHz : 2595.124
bogomips : 5190.24 #### IPv4 benchmark example
cores/threads : 2
```bash
Erlang info: System:
system_version:Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit] CPU : Apple M4
load test data use 4.835593s Cores : 10 cores / 10 threads
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
start run benchmark tests Loaded : 487169 IPs in 1.335 s
search from file: Benchmarks:
ip count:683844, cold total= 9.601s count= 487169 qps= 50740.66 avg= 0.019708 ms/op (19.708 us/op)
total time: 28.201699s, warm total= 0.160s count= 487169 qps= 3053164.29 avg= 0.000328 ms/op ( 0.328 us/op)
search 24248.326315375536 times per second,
use 41.23995969841075 micro second per search Done.
```
search from cache:
ip count:683844, #### IPv6 benchmark example
total time: 0.671801s,
search 1017926.4395259906 times per second, ```bash
use 0.9823892583688677 micro second per search System:
CPU : Apple M4
benchmark test finish Cores : 10 cores / 10 threads
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
Loaded : 638953 IPs in 2.949 s
Benchmarks:
cold total= 20.504s count= 638953 qps= 31162.52 avg= 0.032090 ms/op (32.090 us/op)
warm total= 0.444s count= 638953 qps= 1437781.56 avg= 0.000696 ms/op ( 0.696 us/op)
Done.
``` ```

View File

@ -1,128 +1,179 @@
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md) :globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
# ip2region erlang 查询客户端 # ip2region Erlang 查询客户端
### 简介 ### 简介
该bingding以erlang语言实现xdb查询客户端基于Erlang OTP Application查询逻辑由ip2region_worker工作进程实现支持配多个工作进程来进行负载均衡。
该 bingding 以 `Erlang` 语言实现 xdb 查询客户端,基于 `Erlang OTP Application`,查询逻辑由 `ip2region_worker` 工作进程实现,支持配多个工作进程来进行负载均衡。
### 应用配置 ### 应用配置
该应用可配置的参数在ip2region.app.src中,如下:
``` erlang 该应用可配置的参数在 `ip2region.app.src` 中, 如下:
```erlang
{env,[ {env,[
{poolargs, [ {poolargs, [
{size, 1}, %% 工作进程默认数量 {size, 1}, %% 工作进程默认数量
{max_overflow, 5} %% 工作进程最大数量 {max_overflow, 5} %% 工作进程最大数量
]},
{db, [
{ipv4, "ip2region.xdb"} %% 默认 IPv4 xdb 文件
]} ]}
]} ]}
``` ```
### 双栈配置IPv4 + IPv6
如需启用 IPv6 查询,在 `db` 列表中加入 `ipv6` 项,并将两个 xdb 文件放到 `priv/` 目录下:
```erlang
{env,[
{poolargs, [
{size, 1},
{max_overflow, 5}
]},
{db, [
{ipv4, "ip2region.xdb"},
{ipv6, "ip2region_v6.xdb"}
]}
]}
```
`xdb:search/1` 接口会自动识别 IPv4 与 IPv6 输入,并将其路由到对应的工作进程池。
### 编译 ### 编译
``` ```bash
$ rebar3 compile $ rebar3 compile
``` ```
### 运行 ### 运行
将xdb文件放到priv目录下然后启动erlang节点
``` 将 xdb 文件放到 `priv` 目录下,然后启动 Erlang 节点:
```bash
$ rebar3 shell $ rebar3 shell
``` ```
在erlang shell中调用xdb:search/1接口查询Ip地址信息, 该接口支持以list格式字符串、binary格式字符串、tuple和整数表示的IP地址如下
``` 在 Erlang shell 中调用 `xdb:search/1` 接口查询 IP 地址信息。该接口支持 list 字符串、binary 字符串、tuple 和整数表示的 IP 地址:
```erlang
1> xdb:search("1.0.8.0"). 1> xdb:search("1.0.8.0").
[20013,22269,124,48,124,24191,19996,30465,124,24191,24030, [20013,22269,124,24191,19996,30465,124,24191,24030,24066,
24066,124,30005,20449] 124,20013,22269,30005,20449,124,67,78]
2> 2>
3> io:format("~ts~n", [xdb:search("1.0.8.0")]). 3> io:format("~ts~n", [xdb:search("1.0.8.0")]).
中国|0|广东省|广州市|电信 中国|广东省|广州市|中国电信|CN
io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]). 4> io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]).
中国|0|广东省|广州市|电信 中国|广东省|广州市|中国电信|CN
4> io:format("~ts~n", [xdb:search({1,0,8,0})]). 5> io:format("~ts~n", [xdb:search({1,0,8,0})]).
中国|0|广东省|广州市|电信 中国|广东省|广州市|中国电信|CN
6> io:format("~ts~n", [xdb:search(16779264)]). 6> io:format("~ts~n", [xdb:search(16779264)]).
中国|0|广东省|广州市|电信 中国|广东省|广州市|中国电信|CN
```
启用双栈后IPv6 地址也按同样方式支持:
```erlang
1> io:format("~ts~n", [xdb:search("2001:4860:4860::8888")]).
United States|Florida|Miami|Google LLC|US
2> io:format("~ts~n", [xdb:search(<<"2001:4860:4860::8888">>)]).
United States|Florida|Miami|Google LLC|US
3> io:format("~ts~n", [xdb:search({8193,18528,18528,0,0,0,0,34952})]).
United States|Florida|Miami|Google LLC|US
``` ```
### 使用方法 ### 使用方法
* 在rebar.config中引入依赖
``` - 在 `rebar.config` 中引入依赖
```erlang
{deps, [ {deps, [
ip2region ip2region
]}. ]}.
``` ```
* 启动ip2region Application
```
......
application:ensure_started(ip2region), - 启动 ip2region Application
...... ```erlang
{ok, _} = application:ensure_all_started(ip2region).
``` ```
* 调用xdb:search/1接口查询IP信息 - 调用 `xdb:search/1` 接口查询 IP 信息
```
......
ip2region:search("1.0.8.0"), ```erlang
xdb:search("1.0.8.0").
......
``` ```
### 单元测试 ### 单元测试
``` ```bash
$ rebar3 eunit $ rebar3 eunit
===> Verifying dependencies... ===> Verifying dependencies...
===> Analyzing applications... ===> Analyzing applications...
===> Compiling ip2region ===> Compiling ip2region
===> Performing EUnit tests... ===> Performing EUnit tests...
=INFO REPORT==== 17-Jan-2023::11:52:59.920155 === =INFO REPORT==== 28-Jun-2026::04:53:28 ===
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb
.... ....
Finished in 0.074 seconds Finished in 0.192 seconds
4 tests, 0 failures 63 tests, 0 failures
``` ```
### 基准测试 ### 基准测试
```
IPv4 与 IPv6 共用一个脚本,通过参数指定版本:
> `cold` = 第一次遍历源文件:每个 IP 都会触发真实查询,并把结果写入 ETS 缓存。
> `warm` = 第二次遍历同一列表,所有查询都直接命中 ETS 缓存。
```bash
$ cd benchmarks/ $ cd benchmarks/
$ sh xdb-benchmark.sh $ sh xdb-benchmark.sh ipv4
===> Verifying dependencies... ```
===> Analyzing applications...
===> Compiling ip2region IPv6
Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
```bash
Eshell V12.3.2.2 (abort with ^G) $ sh xdb-benchmark.sh ipv6
1> =INFO REPORT==== 17-Jan-2023::11:37:35.631095 === ```
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/default/lib/ip2region/priv/ip2region.xdb
也可以在 `binding/erlang` 目录直接用 Makefile
===> Booted ip2region
===> Evaluating: "xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()." ```bash
CPU info: $ make bench-v4
model name : AMD EPYC 7K62 48-Core Processor $ make bench-v6
cache size : 512 KB ```
cpu MHz : 2595.124
bogomips : 5190.24 #### IPv4 基准测试示例
cores/threads : 2
```bash
Erlang info: System:
system_version:Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit] CPU : Apple M4
load test data use 4.835593s Cores : 10 cores / 10 threads
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
start run benchmark tests Loaded : 487169 IPs in 1.335 s
search from file: Benchmarks:
ip count:683844, cold total= 9.601s count= 487169 qps= 50740.66 avg= 0.019708 ms/op (19.708 us/op)
total time: 28.201699s, warm total= 0.160s count= 487169 qps= 3053164.29 avg= 0.000328 ms/op ( 0.328 us/op)
search 24248.326315375536 times per second,
use 41.23995969841075 micro second per search Done.
```
search from cache:
ip count:683844, #### IPv6 基准测试示例
total time: 0.671801s,
search 1017926.4395259906 times per second, ```bash
use 0.9823892583688677 micro second per search System:
CPU : Apple M4
benchmark test finish Cores : 10 cores / 10 threads
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
Loaded : 638953 IPs in 2.949 s
Benchmarks:
cold total= 20.504s count= 638953 qps= 31162.52 avg= 0.032090 ms/op (32.090 us/op)
warm total= 0.444s count= 638953 qps= 1437781.56 avg= 0.000696 ms/op ( 0.696 us/op)
Done.
``` ```

View File

@ -1,5 +1,37 @@
#!/bin/bash #!/bin/bash
set -e
cd .. # Run from binding/erlang so paths and rebar3 artifacts are correct.
cd "$(dirname "$0")/.."
rebar3 shell --eval="xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()." VERSION="${1:-ipv4}"
PROJECT_ROOT="$(cd ../.. && pwd)"
if [ "$VERSION" = "ipv6" ]; then
DB_FILE="${PROJECT_ROOT}/data/ip2region_v6.xdb"
DATA_FILE="${PROJECT_ROOT}/data/ipv6_source.txt"
DB_CONFIG="[{ipv6, \"${DB_FILE}\"}]"
elif [ "$VERSION" = "ipv4" ]; then
DATA_FILE="${PROJECT_ROOT}/data/ipv4_source.txt"
DB_CONFIG="[{ipv4, \"ip2region.xdb\"}]"
else
echo "Usage: $0 [ipv4|ipv6]"
exit 1
fi
rebar3 compile
# shellcheck disable=SC2089
EVAL="application:load(ip2region), \
application:set_env(ip2region, db, ${DB_CONFIG}), \
application:set_env(ip2region, poolargs, [{size,4},{max_overflow,8}]), \
{ok,_}=application:ensure_all_started(ip2region), \
xdb_benchmark:main(\"${DATA_FILE}\"), \
init:stop()."
# shellcheck disable=SC2090
erl -pa _build/default/lib/poolboy/ebin \
-pa _build/default/lib/ip2region/ebin \
-kernel logger_level error \
-noshell \
-eval "${EVAL}"

View File

@ -4,23 +4,39 @@
-define(NONE, none). -define(NONE, none).
-define(APP_NAME, ip2region). -define(APP_NAME, ip2region).
-define(XDB_VECTOR_INDEX, ets_xdb_vector_index).
-define(XDB_SEGMENT_INDEX, ets_xdb_segement_index).
-define(IP2REGION_CACHE, ets_ip2region_cache).
-define(XDB_HEADER_SIZE, 256). -define(XDB_HEADER_SIZE, 256).
-define(XDB_VECTOR_COLS, 256). -define(XDB_VECTOR_COLS, 256).
-define(XDB_VECTOR_INDEX_SIZE, 8). -define(XDB_VECTOR_INDEX_SIZE, 8).
-define(XDB_VECTOR_INDEX_COUNT, (16#10000)). %% 256*256 -define(XDB_VECTOR_INDEX_COUNT, (16#10000)). %% 256*256
-define(XDB_SEGMENT_INDEX_SIZE, 14). %% IP versions as stored in xdb header
-define(IP_VERSION_4, 4).
-define(IP_VERSION_6, 6).
-define(IP2REGION_POOL, ip2region_pool). %% Segment index sizes (bytes): start_ip + end_ip + data_len + data_ptr
-define(XDB_SEGMENT_INDEX_SIZE_V4, 14). %% 4 + 4 + 2 + 4
-define(XDB_SEGMENT_INDEX_SIZE_V6, 38). %% 16 + 16 + 2 + 4
-ifndef(IF). %% Per-version ETS tables
-define(IF(C, T, F), case (C) of true -> (T); false -> (F) end). -define(XDB_VECTOR_INDEX_V4, ets_xdb_vector_index_v4).
-define(IF(C, T), ?IF(C, T, skip)). -define(XDB_VECTOR_INDEX_V6, ets_xdb_vector_index_v6).
-endif. -define(XDB_SEGMENT_INDEX_V4, ets_xdb_segment_index_v4).
-define(XDB_SEGMENT_INDEX_V6, ets_xdb_segment_index_v6).
-define(IP2REGION_CACHE_V4, ets_ip2region_cache_v4).
-define(IP2REGION_CACHE_V6, ets_ip2region_cache_v6).
%% Per-version pool names.
-define(IP2REGION_POOL_V4, ip2region_pool_v4).
-define(IP2REGION_POOL_V6, ip2region_pool_v6).
-record(xdb_header, {
version :: non_neg_integer(),
index_policy :: non_neg_integer(),
created_at :: non_neg_integer(),
start_index_ptr :: non_neg_integer(),
end_index_ptr :: non_neg_integer(),
ip_version :: non_neg_integer(),
runtime_ptr_bytes :: non_neg_integer()
}).
-endif. -endif.

View File

@ -18,7 +18,7 @@
{ex_doc, [ {ex_doc, [
{extras, ["README.md"]}, {extras, ["README.md"]},
{main, "README.md"}, {main, "README.md"},
{source_url, "https://github.com/leihua996/ip2region/tree/master/binding/erlang"} {source_url, "https://github.com/lionsoul2014/ip2region/tree/master/binding/erlang"}
]}. ]}.
{hex, [{doc, ex_doc}]}. {hex, [{doc, ex_doc}]}.

View File

@ -5,16 +5,20 @@
{mod, {ip2region_app, []}}, {mod, {ip2region_app, []}},
{applications, {applications,
[kernel, [kernel,
stdlib stdlib,
poolboy
]}, ]},
{env,[ {env,[
{poolargs, [ {poolargs, [
{size, 1}, {size, 1},
{max_overflow, 5} {max_overflow, 5}
]},
{db, [
{ipv4, "ip2region.xdb"}
]} ]}
]}, ]},
{modules, []}, {modules, []},
{licenses, ["Apache-2.0"]}, {licenses, ["Apache-2.0"]},
{links, [{"Github", "https://github.com/leihua996/ip2region/tree/master/binding/erlang"}]} {links, [{"Github", "https://github.com/lionsoul2014/ip2region/tree/master/binding/erlang"}]}
]}. ]}.

View File

@ -18,7 +18,6 @@
start_link() -> start_link() ->
{ok, SupPid} = supervisor:start_link({local, ?SERVER}, ?MODULE, []), {ok, SupPid} = supervisor:start_link({local, ?SERVER}, ?MODULE, []),
{ok, _PoolPid} = start_ip2region_pool(SupPid),
{ok, SupPid}. {ok, SupPid}.
%% sup_flags() = #{strategy => strategy(), % optional %% sup_flags() = #{strategy => strategy(), % optional
@ -35,21 +34,45 @@ init([]) ->
SupFlags = #{strategy => one_for_one, SupFlags = #{strategy => one_for_one,
intensity => 10, intensity => 10,
period => 5}, period => 5},
ChildSpecs = [], ChildSpecs = pool_child_specs(),
{ok, {SupFlags, ChildSpecs}}. {ok, {SupFlags, ChildSpecs}}.
%% internal functions %% internal functions
%% %%
create_table() -> create_table() ->
Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}], Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}],
ets:new(?XDB_VECTOR_INDEX, Opts), %% Version-specific tables for dual-stack support
ets:new(?XDB_SEGMENT_INDEX, Opts), ensure_table(?XDB_VECTOR_INDEX_V4, Opts),
ets:new(?IP2REGION_CACHE, Opts). ensure_table(?XDB_VECTOR_INDEX_V6, Opts),
ensure_table(?XDB_SEGMENT_INDEX_V4, Opts),
ensure_table(?XDB_SEGMENT_INDEX_V6, Opts),
ensure_table(?IP2REGION_CACHE_V4, Opts),
ensure_table(?IP2REGION_CACHE_V6, Opts).
start_ip2region_pool(Sup) -> ensure_table(Name, Opts) ->
case ets:whereis(Name) of
undefined -> ets:new(Name, Opts);
_ -> ok
end.
pool_child_specs() ->
{ok, DbConfig} = application:get_env(db),
{ok, PoolArgsCfg} = application:get_env(poolargs), {ok, PoolArgsCfg} = application:get_env(poolargs),
PoolName = ?IP2REGION_POOL, lists:foldl(
PoolArgs = [{strategy, fifo}, {name, {local, PoolName}}, {worker_module, ip2region_worker} | PoolArgsCfg], fun({ipv4, File}, Acc) ->
WorkerArgs = [], [make_pool_spec(?IP2REGION_POOL_V4, ipv4, File, PoolArgsCfg) | Acc];
ChildSpecs = poolboy:child_spec(PoolName, PoolArgs, WorkerArgs), ({ipv6, File}, Acc) ->
supervisor:start_child(Sup, ChildSpecs). [make_pool_spec(?IP2REGION_POOL_V6, ipv6, File, PoolArgsCfg) | Acc];
(_, Acc) ->
Acc
end, [], DbConfig).
make_pool_spec(PoolName, Version, File, PoolArgsCfg) ->
PoolArgs = [
{strategy, fifo},
{name, {local, PoolName}},
{worker_module, ip2region_worker}
| PoolArgsCfg
],
WorkerArgs = [{xdb_file, File}, {expected_version, Version}],
poolboy:child_spec(PoolName, PoolArgs, WorkerArgs).

View File

@ -4,23 +4,68 @@
%% license that can be found in the LICENSE file. %% license that can be found in the LICENSE file.
%% %%
%% @doc %% @doc
%% ip2region utils %% ip2region utils: IPv4/IPv6 parsing and version detection.
%% @end %% @end
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(ip2region_util). -module(ip2region_util).
-export([ipv4_to_n/1]).
-export([ipv4_to_n/1, ip_version/1, ip_to_bytes/1]).
ipv4_to_n(IntIp) when is_integer(IntIp) -> IntIp; -define(IS_UINT8(V), is_integer(V), V >= 0, V =< 255).
ipv4_to_n({A, B, C, D}) -> -define(IS_UINT16(V), is_integer(V), V >= 0, V =< 65535).
<<N:32>> = <<A, B, C, D>>,
N; -spec ip_version(Ip :: tuple() | list() | binary() | integer()) ->
ipv4_to_n(Ip) when is_binary(Ip) -> ipv4 | ipv6 | {error, atom()}.
ipv4_to_n(binary_to_list(Ip)); ip_version(Ip) when is_integer(Ip), Ip >= 0, Ip =< 16#FFFFFFFF -> ipv4;
ipv4_to_n(Ip) when is_list(Ip) -> ip_version({A, B, C, D}) when ?IS_UINT8(A), ?IS_UINT8(B), ?IS_UINT8(C), ?IS_UINT8(D) -> ipv4;
ip_version({A, B, C, D, E, F, G, H}) when ?IS_UINT16(A), ?IS_UINT16(B), ?IS_UINT16(C), ?IS_UINT16(D),
?IS_UINT16(E), ?IS_UINT16(F), ?IS_UINT16(G), ?IS_UINT16(H) -> ipv6;
ip_version(Ip) when is_binary(Ip) ->
ip_version(binary_to_list(Ip));
ip_version(Ip) when is_list(Ip) ->
case inet_parse:address(Ip) of case inet_parse:address(Ip) of
{ok, Addr} -> {ok, {_, _, _, _}} -> ipv4;
ipv4_to_n(Addr); {ok, {_, _, _, _, _, _, _, _}} -> ipv6;
_ -> {error, bad_ip_format}
end;
ip_version(_) ->
{error, bad_ip_format}.
-spec ip_to_bytes(Ip :: tuple() | list() | binary() | integer()) ->
{ok, ipv4 | ipv6, binary()} | {error, atom()}.
ip_to_bytes(Ip) when is_integer(Ip), Ip >= 0, Ip =< 16#FFFFFFFF ->
{ok, ipv4, <<Ip:32>>};
ip_to_bytes({A, B, C, D}) when ?IS_UINT8(A), ?IS_UINT8(B), ?IS_UINT8(C), ?IS_UINT8(D) ->
{ok, ipv4, <<A, B, C, D>>};
ip_to_bytes({A, B, C, D, E, F, G, H}) when ?IS_UINT16(A), ?IS_UINT16(B), ?IS_UINT16(C), ?IS_UINT16(D),
?IS_UINT16(E), ?IS_UINT16(F), ?IS_UINT16(G), ?IS_UINT16(H) ->
{ok, ipv6, <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>};
ip_to_bytes(Ip) when is_binary(Ip) ->
ip_to_bytes(binary_to_list(Ip));
ip_to_bytes(Ip) when is_list(Ip) ->
case inet_parse:address(Ip) of
{ok, {A, B, C, D}} ->
{ok, ipv4, <<A, B, C, D>>};
{ok, {A, B, C, D, E, F, G, H}} ->
{ok, ipv6, <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>};
_ -> _ ->
{error, bad_ip_format} {error, bad_ip_format}
end. end;
ip_to_bytes(_) ->
{error, bad_ip_format}.
-spec ipv4_to_n(Ip :: tuple() | list() | binary() | integer()) ->
non_neg_integer() | {error, atom()}.
ipv4_to_n(IntIp) when is_integer(IntIp), IntIp >= 0, IntIp =< 16#FFFFFFFF -> IntIp;
ipv4_to_n({A, B, C, D}) when ?IS_UINT8(A), ?IS_UINT8(B), ?IS_UINT8(C), ?IS_UINT8(D) ->
<<N:32>> = <<A, B, C, D>>,
N;
ipv4_to_n(Ip) when is_binary(Ip) ->
ipv4_to_n(binary_to_list(Ip));
ipv4_to_n(Ip) when is_list(Ip) ->
case ip_to_bytes(Ip) of
{ok, ipv4, <<N:32>>} -> N;
_ -> {error, bad_ip_format}
end;
ipv4_to_n(_) ->
{error, bad_ip_format}.

View File

@ -4,7 +4,7 @@
%% license that can be found in the LICENSE file. %% license that can be found in the LICENSE file.
%% %%
%% @doc %% @doc
%% ip2region xdb client worker %% ip2region xdb client worker, now version-aware (IPv4/IPv6).
%% @end %% @end
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(ip2region_worker). -module(ip2region_worker).
@ -15,7 +15,11 @@
-export([start/1, stop/1, start_link/1]). -export([start/1, stop/1, start_link/1]).
-export([search/2]). -export([search/2]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-record(state, {xdb_fd}).
-record(state, {
xdb_fd,
version :: ipv4 | ipv6
}).
%%========================================== %%==========================================
%% API %% API
@ -28,7 +32,6 @@ start_link(Args) ->
Opts = [{spawn_opt, [{min_heap_size, 6000}]}], Opts = [{spawn_opt, [{min_heap_size, 6000}]}],
gen_server:start_link(?MODULE, Args, Opts). gen_server:start_link(?MODULE, Args, Opts).
stop(Pid) -> stop(Pid) ->
gen_server:call(Pid, stop). gen_server:call(Pid, stop).
@ -38,7 +41,7 @@ search(Pid, Ip) ->
%%========================================== %%==========================================
%% gen_server callbacks %% gen_server callbacks
%% ========================================= %% =========================================
init(_Args) -> init(Args) ->
process_flag(trap_exit, true), process_flag(trap_exit, true),
AppName = AppName =
case application:get_application() of case application:get_application() of
@ -46,11 +49,31 @@ init(_Args) ->
_ -> ?APP_NAME _ -> ?APP_NAME
end, end,
PrivDir = code:priv_dir(AppName), PrivDir = code:priv_dir(AppName),
XdbFileName = filename:join([PrivDir, "ip2region.xdb"]), XdbFileName =
case proplists:get_value(xdb_file, Args) of
undefined -> filename:join([PrivDir, "ip2region.xdb"]);
Path ->
case filename:pathtype(Path) of
absolute -> Path;
_ -> filename:join([PrivDir, Path])
end
end,
error_logger:info_report(io_lib:format("XdbFile:~s~n", [XdbFileName])), error_logger:info_report(io_lib:format("XdbFile:~s~n", [XdbFileName])),
{ok, IoDevice} = file:open(XdbFileName, [read, binary]), {ok, IoDevice} = file:open(XdbFileName, [read, binary]),
load_vector_index(IoDevice), {ok, HeaderBin} = file:read(IoDevice, ?XDB_HEADER_SIZE),
{ok, #state{xdb_fd = IoDevice}}. {ok, Header} = ip2region_xdb:parse_header(HeaderBin),
ExpectedVersion = proplists:get_value(expected_version, Args),
case resolve_version(Header) of
{ok, Version} when ExpectedVersion =:= undefined; ExpectedVersion =:= Version ->
load_vector_index(IoDevice, Version),
{ok, #state{xdb_fd = IoDevice, version = Version}};
{ok, Version} ->
file:close(IoDevice),
{stop, {xdb_version_mismatch, ExpectedVersion, Version}};
{error, Reason} ->
file:close(IoDevice),
{stop, Reason}
end.
handle_call(Request, From, State) -> handle_call(Request, From, State) ->
try try
@ -85,22 +108,19 @@ handle_info(Info, State) ->
terminate(_Reason, State) -> terminate(_Reason, State) ->
#state{xdb_fd = XdbFd} = State, #state{xdb_fd = XdbFd} = State,
case is_pid(XdbFd) of case is_pid(XdbFd) of
true -> true -> file:close(XdbFd);
file:close(XdbFd); _ -> skip
_ ->
skip
end, end,
ok. ok.
code_change(_OldVsn, State, _Extra) -> code_change(_OldVsn, State, _Extra) ->
{ok, State}. {ok, State}.
%%========================================== %%==========================================
%% Internal function %% Internal function
%% ========================================= %% =========================================
do_call({search, Ip}, _From, #state{xdb_fd = IoDevice} = State) -> do_call({search, Ip}, _From, #state{xdb_fd = IoDevice} = State) ->
Reply = search_ip(IoDevice, Ip), Reply = search_ip(IoDevice, Ip, State),
{reply, Reply, State}; {reply, Reply, State};
do_call(stop, _From, State) -> do_call(stop, _From, State) ->
@ -108,7 +128,7 @@ do_call(stop, _From, State) ->
do_call(Request, From, State) -> do_call(Request, From, State) ->
error_logger:error_report(io_lib:format("unknown request: ~p, from:~p", [Request, From])), error_logger:error_report(io_lib:format("unknown request: ~p, from:~p", [Request, From])),
{noreply, State}. {reply, {error, unknown_request}, State}.
do_cast(Msg, State) -> do_cast(Msg, State) ->
error_logger:error_report(io_lib:format("unknown msg: ~p", [Msg])), error_logger:error_report(io_lib:format("unknown msg: ~p", [Msg])),
@ -118,66 +138,104 @@ do_info(Info, State) ->
error_logger:error_report(io:format("unknown info: ~p", [Info])), error_logger:error_report(io:format("unknown info: ~p", [Info])),
{noreply, State}. {noreply, State}.
-spec resolve_version(#xdb_header{}) -> {ok, ipv4 | ipv6} | {error, term()}.
load_vector_index(IoDevice) -> resolve_version(Header) ->
Key = ip2region_header_loaded, case ip2region_xdb:header_version(Header) of
case persistent_term:get(Key, false) of 2 -> {ok, ipv4};
true -> ok; 3 ->
_ -> case ip2region_xdb:header_ip_version(Header) of
{ok, <<_Header:?XDB_HEADER_SIZE/binary, VectorIndexBin/binary>> } = ?IP_VERSION_4 -> {ok, ipv4};
file:read(IoDevice, ?XDB_HEADER_SIZE + ?XDB_VECTOR_INDEX_COUNT*8), ?IP_VERSION_6 -> {ok, ipv6};
load_vector_index_aux(VectorIndexBin, 0), V -> {error, {invalid_xdb_ip_version, V}}
persistent_term:put(Key, true) end;
V -> {error, {invalid_xdb_version, V}}
end. end.
load_vector_index_aux(<<>>, _Index) -> ok; load_vector_index(IoDevice, Version) ->
load_vector_index_aux(<<SPtr:32/little, EPtr:32/little, VectorIndexBin/binary>>, Index) -> %% The vector index ETS table is global and shared by all pool workers.
Term = {Index, SPtr, EPtr}, %% Only the first worker (or the first worker after a restart) populates it.
ets:insert(?XDB_VECTOR_INDEX, Term), %% Subsequent workers skip the file:read here; search_ip/2 uses file:pread/3,
load_vector_index_aux(VectorIndexBin, Index + 1). %% so the current file pointer position does not affect correctness.
Table = ip2region_xdb:vector_index_table(Version),
case ets:info(Table, size) of
search_ip(IoDevice, Ip) -> ?XDB_VECTOR_INDEX_COUNT ->
IntIp = ip2region_util:ipv4_to_n(Ip), ok;
case ets:lookup(?IP2REGION_CACHE, IntIp) of undefined ->
[{_IntIp, RegionInfo}] -> Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}],
RegionInfo; ets:new(Table, Opts),
load_vector_index_data(IoDevice, Table);
_ -> _ ->
<<A:8, B:8, _Rest/binary>> = <<IntIp:32>>, %% A previous worker may have crashed midway; reload to be safe.
VectorIdx = A * ?XDB_VECTOR_COLS + B, load_vector_index_data(IoDevice, Table)
[{_, SPtr, EPtr}] = ets:lookup(?XDB_VECTOR_INDEX, VectorIdx),
RegionInfo = search_ip(IoDevice, IntIp, SPtr, EPtr, 0, (EPtr - SPtr) div ?XDB_SEGMENT_INDEX_SIZE),
ets:insert_new(?IP2REGION_CACHE, {IntIp, RegionInfo}),
RegionInfo
end. end.
search_ip(IoDevice, IntIp, SPtr, EPtr, Low, High) when Low =< High -> load_vector_index_data(IoDevice, Table) ->
{ok, VectorIndexBin} =
file:read(IoDevice, ?XDB_VECTOR_INDEX_COUNT * ?XDB_VECTOR_INDEX_SIZE),
load_vector_index_aux(VectorIndexBin, 0, Table).
load_vector_index_aux(<<>>, _Index, _Table) -> ok;
load_vector_index_aux(<<SPtr:32/little, EPtr:32/little, VectorIndexBin/binary>>, Index, Table) ->
ets:insert(Table, {Index, SPtr, EPtr}),
load_vector_index_aux(VectorIndexBin, Index + 1, Table).
search_ip(IoDevice, Ip, #state{version = Version}) ->
SegSize = ip2region_xdb:segment_index_size(Version),
VectorTable = ip2region_xdb:vector_index_table(Version),
SegmentTable = ip2region_xdb:segment_index_table(Version),
<<A:8, B:8, _/binary>> = Ip,
VectorIdx = A * ?XDB_VECTOR_COLS + B,
[{_, SPtr, EPtr}] = ets:lookup(VectorTable, VectorIdx),
search_ip(IoDevice, Ip, SPtr, EPtr, 0,
(EPtr - SPtr) div SegSize, SegSize, Version, SegmentTable).
search_ip(IoDevice, Ip, SPtr, EPtr, Low, High, SegSize, Version, SegmentTable) when Low =< High ->
Middle = (Low + High) bsr 1, Middle = (Low + High) bsr 1,
SPtr2 = SPtr + Middle * ?XDB_SEGMENT_INDEX_SIZE, SPtr2 = SPtr + Middle * SegSize,
{SIp, EIp, DataLen, DataPtr} = read_segement_index(IoDevice, SPtr2), {SIp, EIp, DataLen, DataPtr} = read_segment_index(IoDevice, SPtr2, SegSize, SegmentTable),
if case ip_in_range(Ip, SIp, EIp, Version) of
IntIp < SIp -> below ->
search_ip(IoDevice, IntIp, SPtr, EPtr, Low, Middle - 1); search_ip(IoDevice, Ip, SPtr, EPtr, Low, Middle - 1, SegSize, Version, SegmentTable);
IntIp > EIp -> above ->
search_ip(IoDevice, IntIp, SPtr, EPtr, Middle + 1, High); search_ip(IoDevice, Ip, SPtr, EPtr, Middle + 1, High, SegSize, Version, SegmentTable);
true -> inside ->
{ok, DataBin} = read_file(IoDevice, DataPtr, DataLen), {ok, DataBin} = read_file(IoDevice, DataPtr, DataLen),
unicode:characters_to_nfc_list(DataBin) unicode:characters_to_nfc_list(DataBin)
end; end;
search_ip(_IoDevice, _IntIp, _SPtr, _EPtr, _Low, _High) -> search_ip(_IoDevice, _Ip, _SPtr, _EPtr, _Low, _High, _SegSize, _Version, _SegmentTable) ->
{error, unknown}. {error, unknown}.
read_file(IoDevice, Position, DataLength) -> ip_in_range(Ip, SIp, EIp, ipv4) ->
file:position(IoDevice, {bof, Position}), <<InputInt:32>> = Ip,
file:read(IoDevice, DataLength). if
InputInt < SIp -> below;
InputInt > EIp -> above;
true -> inside
end;
ip_in_range(Ip, SIp, EIp, ipv6) ->
if
Ip < SIp -> below;
Ip > EIp -> above;
true -> inside
end.
read_segement_index(IoDevice, SPtr) -> read_file(IoDevice, Position, DataLength) ->
case ets:lookup(?XDB_SEGMENT_INDEX, SPtr) of file:pread(IoDevice, Position, DataLength).
read_segment_index(IoDevice, SPtr, SegSize, SegmentTable) ->
case ets:lookup(SegmentTable, SPtr) of
[{_SPtr, SIp, EIp, DataLen, DataPtr}] -> [{_SPtr, SIp, EIp, DataLen, DataPtr}] ->
{SIp, EIp, DataLen, DataPtr}; {SIp, EIp, DataLen, DataPtr};
_ -> _ ->
{ok, <<SIp:32/little, EIp:32/little, DataLen:16/little, DataPtr:32/little>>} = {ok, Bin} = read_file(IoDevice, SPtr, SegSize),
read_file(IoDevice, SPtr, ?XDB_SEGMENT_INDEX_SIZE), {SIp, EIp, DataLen, DataPtr} = decode_segment_index(Bin, SegSize),
ets:insert_new(?XDB_SEGMENT_INDEX, {SPtr, SIp, EIp, DataLen, DataPtr}), ets:insert_new(SegmentTable, {SPtr, SIp, EIp, DataLen, DataPtr}),
{SIp, EIp, DataLen, DataPtr} {SIp, EIp, DataLen, DataPtr}
end. end.
decode_segment_index(Bin, ?XDB_SEGMENT_INDEX_SIZE_V4) ->
<<SIp:32/little, EIp:32/little, DataLen:16/little, DataPtr:32/little>> = Bin,
{SIp, EIp, DataLen, DataPtr};
decode_segment_index(Bin, ?XDB_SEGMENT_INDEX_SIZE_V6) ->
<<SIp:16/binary, EIp:16/binary, DataLen:16/little, DataPtr:32/little>> = Bin,
{SIp, EIp, DataLen, DataPtr}.

View File

@ -0,0 +1,60 @@
%%%-------------------------------------------------------------------
%% 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.
%%
%% @doc
%% ip2region xdb binary format helpers.
%% @end
%%%-------------------------------------------------------------------
-module(ip2region_xdb).
-export([parse_header/1]).
-export([header_version/1, header_index_policy/1, header_created_at/1,
header_start_index_ptr/1, header_end_index_ptr/1,
header_ip_version/1, header_runtime_ptr_bytes/1]).
-export([segment_index_size/1]).
-export([vector_index_table/1, segment_index_table/1, cache_table/1]).
-include("ip2region.hrl").
-spec parse_header(binary()) -> {ok, #xdb_header{}} | {error, invalid_header}.
parse_header(Bin) when is_binary(Bin), byte_size(Bin) >= 20 ->
<<Version:16/little, IndexPolicy:16/little, CreatedAt:32/little,
StartIndexPtr:32/little, EndIndexPtr:32/little,
IpVersion:16/little, RuntimePtrBytes:16/little, _/binary>> = Bin,
{ok, #xdb_header{
version = Version,
index_policy = IndexPolicy,
created_at = CreatedAt,
start_index_ptr = StartIndexPtr,
end_index_ptr = EndIndexPtr,
ip_version = IpVersion,
runtime_ptr_bytes = RuntimePtrBytes
}};
parse_header(_) ->
{error, invalid_header}.
header_version(#xdb_header{version = V}) -> V.
header_index_policy(#xdb_header{index_policy = P}) -> P.
header_created_at(#xdb_header{created_at = T}) -> T.
header_start_index_ptr(#xdb_header{start_index_ptr = P}) -> P.
header_end_index_ptr(#xdb_header{end_index_ptr = P}) -> P.
header_ip_version(#xdb_header{ip_version = V}) -> V.
header_runtime_ptr_bytes(#xdb_header{runtime_ptr_bytes = B}) -> B.
-spec segment_index_size(ipv4 | ipv6) -> pos_integer().
segment_index_size(ipv4) -> ?XDB_SEGMENT_INDEX_SIZE_V4;
segment_index_size(ipv6) -> ?XDB_SEGMENT_INDEX_SIZE_V6.
-spec vector_index_table(ipv4 | ipv6) -> atom().
vector_index_table(ipv4) -> ?XDB_VECTOR_INDEX_V4;
vector_index_table(ipv6) -> ?XDB_VECTOR_INDEX_V6.
-spec segment_index_table(ipv4 | ipv6) -> atom().
segment_index_table(ipv4) -> ?XDB_SEGMENT_INDEX_V4;
segment_index_table(ipv6) -> ?XDB_SEGMENT_INDEX_V6.
-spec cache_table(ipv4 | ipv6) -> atom().
cache_table(ipv4) -> ?IP2REGION_CACHE_V4;
cache_table(ipv6) -> ?IP2REGION_CACHE_V6.

View File

@ -1,11 +1,10 @@
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
%% Copyright 2022 The Ip2Region Authors. All rights reserved. %% Copyright 2022 The Ip2Region Authors. All rights reserved.
%% Use of this source code is governed by a Apache2.0-style %% Use of this source code is governed by a Apache2.0-style
%% license that can be found in the LICENSE file. %% license that can be found in the LICENSE file.
%% %%
%% @doc %% @doc
%% ip2region xdb client search api %% ip2region xdb client search api with IPv4/IPv6 auto-routing.
%% @end %% @end
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(xdb). -module(xdb).
@ -13,20 +12,37 @@
-export([search/1]). -export([search/1]).
-spec search(Ip :: tuple() | list() | binary()) -> Result :: binary | {error, Reason::atom()}. -spec search(Ip :: tuple() | list() | binary() | integer()) ->
search(Ip) when is_integer(Ip); is_list(Ip); is_tuple(Ip); is_binary(Ip) -> Result :: list() | {error, term()}.
case ip2region_util:ipv4_to_n(Ip) of search(Ip) ->
IntIp when is_integer(IntIp) -> case ip2region_util:ip_to_bytes(Ip) of
case ets:lookup(?IP2REGION_CACHE, IntIp) of {ok, ipv4, IpBin} ->
[{_IntIp, Region}] -> Region; do_search(?IP2REGION_POOL_V4, ipv4, IpBin);
_ -> {ok, ipv6, IpBin} ->
Worker = poolboy:checkout(?IP2REGION_POOL, true, infinity), do_search(?IP2REGION_POOL_V6, ipv6, IpBin);
try Ret ->
ip2region_worker:search(Worker, IntIp) Ret
after end.
poolboy:checkin(?IP2REGION_POOL, Worker)
end do_search(PoolName, Version, IpBin) ->
end; CacheTable = ip2region_xdb:cache_table(Version),
Ret -> case ets:lookup(CacheTable, IpBin) of
Ret [{_, Region}] -> Region;
_ ->
case whereis(PoolName) of
undefined ->
{error, pool_not_configured};
_ ->
Worker = poolboy:checkout(PoolName, true, infinity),
try
case ip2region_worker:search(Worker, IpBin) of
{error, _} = Err -> Err;
Region ->
ets:insert(CacheTable, {IpBin, Region}),
Region
end
after
poolboy:checkin(PoolName, Worker)
end
end
end. end.

View File

@ -11,29 +11,84 @@
-export([main/1]). -export([main/1]).
main(DataFile) -> main(DataFile) ->
application:ensure_started(ip2region), %% Keep benchmark output clean while still surfacing real errors.
_ = logger:set_handler_config(default, level, error),
_ = logger:set_primary_config(level, error),
{ok, _} = application:ensure_all_started(ip2region),
show_hw_sw_info(), show_hw_sw_info(),
IpList = load_test_data(DataFile), IpList = load_test_data(DataFile),
run(IpList). run(IpList).
show_hw_sw_info() -> show_hw_sw_info() ->
io:format("CPU info:~n", []), {Model, Clock, Cores, Threads} = cpu_info(),
io:format("~s", [os:cmd("egrep '^model name' /proc/cpuinfo | head -1")]), io:format("~nSystem:~n", []),
io:format("~s", [os:cmd("egrep '^cache' /proc/cpuinfo | head -1")]), io:format(" CPU : ~s", [Model]),
io:format("~s", [os:cmd("egrep '^cpu MHz' /proc/cpuinfo | head -1")]), case Clock of
io:format("~s", [os:cmd("egrep '^bogomips' /proc/cpuinfo | head -1")]), "" -> io:format("~n", []);
io:format("cores/threads : ~s~n", [os:cmd("egrep -c '^processor' /proc/cpuinfo")]), _ -> io:format(" @ ~s~n", [Clock])
io:format("Erlang info:~n", []), end,
io:format("system_version:~s", [erlang:system_info(system_version)]), io:format(" Cores : ~s cores / ~s threads~n", [Cores, Threads]),
io:format(" Erlang : ~s~n", [string:trim(erlang:system_info(system_version))]),
ok. ok.
cpu_info() ->
case os:type() of
{unix, darwin} ->
Model = sysctl("machdep.cpu.brand_string"),
Clock = first_non_empty([
format_clock(sysctl("hw.cpufrequency")),
format_clock(sysctl("hw.perflevel0.frequency")),
format_clock(sysctl("hw.perflevel1.frequency"))
]),
Cores = sysctl("hw.physicalcpu"),
Threads = sysctl("hw.logicalcpu"),
{Model, Clock, Cores, Threads};
{unix, linux} ->
Model = linux_cpu_field("model name"),
Clock = format_clock_mhz(linux_cpu_field("cpu MHz")),
Cores = string:trim(os:cmd("grep -c '^processor' /proc/cpuinfo 2>/dev/null")),
Threads = Cores,
{Model, Clock, Cores, Threads};
_ ->
{"unknown", "", "?", "?"}
end.
sysctl(Key) ->
string:trim(os:cmd("sysctl -n " ++ Key ++ " 2>/dev/null")).
linux_cpu_field(Key) ->
Cmd = "grep -m1 '^" ++ Key ++ "' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | sed 's/^ *//'",
string:trim(os:cmd(Cmd)).
first_non_empty(["" | Rest]) -> first_non_empty(Rest);
first_non_empty([Val | _]) -> Val;
first_non_empty([]) -> "".
format_clock(HzStr) ->
case string:to_integer(string:trim(HzStr)) of
{ok, Hz, _} when Hz > 1000000000 ->
lists:flatten(io_lib:format("~.2f GHz", [Hz / 1000000000]));
{ok, Hz, _} when Hz > 1000000 ->
lists:flatten(io_lib:format("~.2f MHz", [Hz / 1000000]));
_ ->
""
end.
format_clock_mhz(MhzStr) ->
case string:to_float(string:trim(MhzStr)) of
{ok, Mhz, _} ->
lists:flatten(io_lib:format("~.3f GHz", [Mhz / 1000]));
_ ->
""
end.
load_test_data(DataFile) -> load_test_data(DataFile) ->
{ok, Fd} = file:open(DataFile, [read]), {ok, Fd} = file:open(DataFile, [read]),
T0 = os:timestamp(), T0 = os:timestamp(),
IpList = load_test_data(Fd, []), IpList = load_test_data(Fd, []),
T1 = os:timestamp(), T1 = os:timestamp(),
Sec = timer:now_diff(T1, T0) / 1000000, Sec = timer:now_diff(T1, T0) / 1000000,
io:format("load test data use ~ps~n", [Sec]), io:format(" Loaded : ~p IPs in ~.3f s~n", [length(IpList), Sec]),
IpList. IpList.
load_test_data(Fd, IpList) -> load_test_data(Fd, IpList) ->
@ -46,30 +101,30 @@ load_test_data(Fd, IpList) ->
load_test_data(Fd, IpList) load_test_data(Fd, IpList)
end; end;
_ -> _ ->
file:close(Fd), file:close(Fd),
IpList IpList
end. end.
run(IpList) -> run(IpList) ->
garbage_collect(), garbage_collect(),
io:format("~nstart run benchmark tests~n", []), io:format("~nBenchmarks:~n", []),
io:format("~nsearch from file:~n", []), run_test("cold", IpList),
run_test(IpList), run_test("warm", IpList),
io:format("~nsearch from cache:~n", []), io:format("~nDone.~n", []).
run_test(IpList),
io:format("~nbenchmark test finish~n", []).
run_test(IpList) -> run_test(Label, IpList) ->
T0 = os:timestamp(), T0 = os:timestamp(),
run_test_aux(IpList), run_test_aux(IpList),
T1 = os:timestamp(), T1 = os:timestamp(),
Sec = timer:now_diff(T1, T0) / 1000000, Sec = timer:now_diff(T1, T0) / 1000000,
IpCount = length(IpList), Count = length(IpList),
io:format("ip count:~p,~ntotal time: ~ps,~nsearch ~p times per second,~nuse ~p micro second per search~n", Qps = Count / Sec,
[IpCount, Sec, IpCount / Sec, Sec * 1000000/IpCount]). MsOp = Sec * 1000 / Count,
UsOp = Sec * 1000000 / Count,
io:format(" ~-8s total=~7.3fs count=~7w qps=~12.2f avg=~9.6f ms/op (~6.3f us/op)~n",
[Label, Sec, Count, Qps, MsOp, UsOp]).
run_test_aux([]) -> ok; run_test_aux([]) -> ok;
run_test_aux([Ip | Tail]) -> run_test_aux([Ip | Tail]) ->
xdb:search(Ip), xdb:search(Ip),
run_test_aux(Tail). run_test_aux(Tail).

View File

@ -0,0 +1,8 @@
-module(ip2region_sup_test).
-include_lib("eunit/include/eunit.hrl").
-include("ip2region.hrl").
pools_started_test() ->
{ok, _} = ip2region_test_util:start_app(),
?assert(is_pid(whereis(?IP2REGION_POOL_V4))),
?assert(is_pid(whereis(?IP2REGION_POOL_V6))).

View File

@ -0,0 +1,51 @@
-module(ip2region_test_util).
-export([
repo_root/0,
v6_xdb_path/0,
default_db_config/0,
reset_app/0,
load_app/0,
set_db_config/1,
start_app/0,
start_app/1,
stop_app/0
]).
repo_root() ->
TestDir = filename:dirname(?FILE),
ErlangDir = filename:dirname(TestDir),
BindingDir = filename:dirname(ErlangDir),
filename:dirname(BindingDir).
v6_xdb_path() ->
filename:join([repo_root(), "data", "ip2region_v6.xdb"]).
default_db_config() ->
[
{ipv4, "ip2region.xdb"},
{ipv6, v6_xdb_path()}
].
reset_app() ->
try application:stop(ip2region) catch _:_ -> ok end,
try application:unload(ip2region) catch _:_ -> ok end,
ok.
load_app() ->
ok = application:load(ip2region).
set_db_config(Config) ->
ok = application:set_env(ip2region, db, Config).
start_app() ->
start_app(default_db_config()).
start_app(Config) ->
reset_app(),
load_app(),
set_db_config(Config),
application:ensure_all_started(ip2region).
stop_app() ->
application:stop(ip2region).

View File

@ -0,0 +1,66 @@
-module(ip2region_util_test).
-include_lib("eunit/include/eunit.hrl").
ipv4_inputs_test_() ->
[
?_assertEqual({ok, ipv4, <<1,0,8,0>>}, ip2region_util:ip_to_bytes("1.0.8.0")),
?_assertEqual({ok, ipv4, <<1,0,8,0>>}, ip2region_util:ip_to_bytes(<<"1.0.8.0">>)),
?_assertEqual({ok, ipv4, <<1,0,8,0>>}, ip2region_util:ip_to_bytes({1,0,8,0})),
?_assertEqual({ok, ipv4, <<1,0,8,0>>}, ip2region_util:ip_to_bytes(16779264)),
?_assertEqual(ipv4, ip2region_util:ip_version("1.0.8.0")),
?_assertEqual(ipv4, ip2region_util:ip_version({1,0,8,0}))
].
ipv6_inputs_test_() ->
[
?_assertEqual({ok, ipv6, <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1>>},
ip2region_util:ip_to_bytes("::1")),
?_assertEqual({ok, ipv6, <<0,0,0,0,0,0,0,0,0,0,255,255,1,0,8,0>>},
ip2region_util:ip_to_bytes("::ffff:1.0.8.0")),
?_assertEqual({ok, ipv6, <<0,0,0,0,0,0,0,0,0,0,255,255,1,0,8,0>>},
ip2region_util:ip_to_bytes(<<"::ffff:1.0.8.0">>)),
?_assertEqual({ok, ipv6, <<0,0,0,0,0,0,0,0,0,0,255,255,1,0,8,0>>},
ip2region_util:ip_to_bytes({0,0,0,0,0,65535,256,2048})),
?_assertEqual(ipv6, ip2region_util:ip_version("::1")),
?_assertEqual(ipv6, ip2region_util:ip_version({0,0,0,0,0,0,0,1}))
].
invalid_inputs_test_() ->
[
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes("xxx.0.8.0")),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes("::ggg")),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes({1,2,3})),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes({1,2,3,4,5,6,7})),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes(atom)),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes(-1)),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes(16#100000000)),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes({256, 0, 0, 1})),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes({-1, 0, 0, 0})),
?_assertEqual({error, bad_ip_format},
ip2region_util:ip_to_bytes({0, 0, 0, 0, 0, 0, 0, 65536})),
?_assertEqual({error, bad_ip_format},
ip2region_util:ip_to_bytes({0, 0, 0, 0, 0, 0, 0, -1})),
?_assertEqual({error, bad_ip_format}, ip2region_util:ipv4_to_n(-1)),
?_assertEqual({error, bad_ip_format}, ip2region_util:ipv4_to_n(16#100000000)),
?_assertEqual({error, bad_ip_format}, ip2region_util:ipv4_to_n({256, 0, 0, 1}))
].
ip_version_test_() ->
[
?_assertEqual(ipv4, ip2region_util:ip_version(<<"1.0.8.0">>)),
?_assertEqual(ipv6, ip2region_util:ip_version(<<"::1">>)),
?_assertEqual(ipv4, ip2region_util:ip_version(16779264 + 1)),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version("::ggg")),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version({1,2,3})),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version({256,0,0,1})),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version({0,0,0,0,0,0,0,65536})),
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version(16#100000000))
].
legacy_ipv4_to_n_test_() ->
[
?_assertEqual(16779264, ip2region_util:ipv4_to_n("1.0.8.0")),
?_assertEqual(16779264, ip2region_util:ipv4_to_n(<<"1.0.8.0">>)),
?_assertEqual(16779264, ip2region_util:ipv4_to_n({1,0,8,0})),
?_assertEqual(16779264, ip2region_util:ipv4_to_n(16779264))
].

View File

@ -0,0 +1,13 @@
-module(ip2region_worker_test).
-include_lib("eunit/include/eunit.hrl").
-include("ip2region.hrl").
worker_search_v4_binary_test() ->
ip2region_sup:create_table(),
{ok, Pid} = ip2region_worker:start_link([{xdb_file, "ip2region.xdb"}]),
try
Region = ip2region_worker:search(Pid, <<1,0,8,0>>),
?assert(is_list(Region))
after
ip2region_worker:stop(Pid)
end.

View File

@ -0,0 +1,43 @@
-module(ip2region_xdb_test).
-include_lib("eunit/include/eunit.hrl").
-include("ip2region.hrl").
valid_v4_header_test() ->
HeaderBin = <<2:16/little, 1:16/little, 0:32/little,
256:32/little, 1024:32/little,
?IP_VERSION_4:16/little, 4:16/little>>,
{ok, Header} = ip2region_xdb:parse_header(HeaderBin),
?assertEqual(2, ip2region_xdb:header_version(Header)),
?assertEqual(1, ip2region_xdb:header_index_policy(Header)),
?assertEqual(256, ip2region_xdb:header_start_index_ptr(Header)),
?assertEqual(1024, ip2region_xdb:header_end_index_ptr(Header)),
?assertEqual(?IP_VERSION_4, ip2region_xdb:header_ip_version(Header)),
?assertEqual(4, ip2region_xdb:header_runtime_ptr_bytes(Header)).
valid_v6_header_test() ->
HeaderBin = <<3:16/little, 1:16/little, 0:32/little,
256:32/little, 1024:32/little,
?IP_VERSION_6:16/little, 4:16/little>>,
{ok, Header} = ip2region_xdb:parse_header(HeaderBin),
?assertEqual(3, ip2region_xdb:header_version(Header)),
?assertEqual(?IP_VERSION_6, ip2region_xdb:header_ip_version(Header)).
invalid_header_test() ->
?assertEqual({error, invalid_header}, ip2region_xdb:parse_header(<<0:128>>)).
table_helpers_test_() ->
[
?_assertEqual(?XDB_VECTOR_INDEX_V4, ip2region_xdb:vector_index_table(ipv4)),
?_assertEqual(?XDB_VECTOR_INDEX_V6, ip2region_xdb:vector_index_table(ipv6)),
?_assertEqual(?XDB_SEGMENT_INDEX_V4, ip2region_xdb:segment_index_table(ipv4)),
?_assertEqual(?XDB_SEGMENT_INDEX_V6, ip2region_xdb:segment_index_table(ipv6)),
?_assertEqual(?IP2REGION_CACHE_V4, ip2region_xdb:cache_table(ipv4)),
?_assertEqual(?IP2REGION_CACHE_V6, ip2region_xdb:cache_table(ipv6))
].
segment_index_size_test_() ->
[
?_assertEqual(14, ip2region_xdb:segment_index_size(ipv4)),
?_assertEqual(38, ip2region_xdb:segment_index_size(ipv6))
].

View File

@ -2,9 +2,11 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-define(IPV6_RESULT, "United States|Florida|Miami|Google LLC|US").
search_test_() -> search_test_() ->
application:ensure_started(ip2region), {ok, _} = ip2region_test_util:start_app(),
A = "中国|0|广东省|广州市|电信", A = "中国|广东省|广州市|中国电信|CN",
Region0 = xdb:search("1.0.8.0"), Region0 = xdb:search("1.0.8.0"),
Region1 = xdb:search(<<"1.0.8.0">>), Region1 = xdb:search(<<"1.0.8.0">>),
Region2 = xdb:search({1,0,8,0}), Region2 = xdb:search({1,0,8,0}),
@ -14,5 +16,39 @@ search_test_() ->
?_assert(A =:= Region1), ?_assert(A =:= Region1),
?_assert(A =:= Region2), ?_assert(A =:= Region2),
?_assert({error, bad_ip_format} =:= Region3) ?_assert({error, bad_ip_format} =:= Region3)
]. ].
ipv6_search_test_() ->
{ok, _} = ip2region_test_util:start_app(),
[
?_assert(?IPV6_RESULT =:= xdb:search("2001:4860:4860::8888")),
?_assert(?IPV6_RESULT =:= xdb:search(<<"2001:4860:4860::8888">>)),
?_assert(?IPV6_RESULT =:= xdb:search({8193, 18528, 18528, 0, 0, 0, 0, 34952}))
].
invalid_search_test_() ->
{ok, _} = ip2region_test_util:start_app(),
[
?_assertEqual({error, bad_ip_format}, xdb:search("xxx.0.8.0")),
?_assertEqual({error, bad_ip_format}, xdb:search("::ggg")),
?_assertEqual({error, bad_ip_format}, xdb:search({1,2,3}))
].
ipv6_pool_not_configured_test() ->
{ok, _} = ip2region_test_util:start_app([{ipv4, "ip2region.xdb"}]),
?assertEqual({error, pool_not_configured}, xdb:search("2001:4860:4860::8888")).
xdb_version_mismatch_test() ->
{ok, _} = ip2region_test_util:start_app(),
V4File = filename:join([
ip2region_test_util:repo_root(), "binding", "erlang", "priv", "ip2region.xdb"
]),
error_logger:tty(false),
try
?assertEqual(
{error, {xdb_version_mismatch, ipv6, ipv4}},
ip2region_worker:start([{xdb_file, V4File}, {expected_version, ipv6}])
)
after
error_logger:tty(true)
end.