增加erlang语言xdb查询客户端
This commit is contained in:
parent
1e88ed8163
commit
18abb9d999
|
|
@ -81,3 +81,6 @@ target
|
||||||
/maker/golang/dbmaker
|
/maker/golang/dbmaker
|
||||||
/maker/golang/xdb_maker
|
/maker/golang/xdb_maker
|
||||||
/maker/golang/golang
|
/maker/golang/golang
|
||||||
|
|
||||||
|
#erlang
|
||||||
|
/binding/erlang/_build
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
ip2region
|
||||||
|
=====
|
||||||
|
|
||||||
|
An OTP application
|
||||||
|
|
||||||
|
Build
|
||||||
|
-----
|
||||||
|
|
||||||
|
$ rebar3 compile
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
-ifndef(IP2REGION_HRL).
|
||||||
|
-define(IP2REGION_HRL, true).
|
||||||
|
|
||||||
|
-define(NONE, none).
|
||||||
|
-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_VECTOR_COLS, 256).
|
||||||
|
-define(XDB_VECTOR_INDEX_SIZE, 8).
|
||||||
|
-define(XDB_VECTOR_INDEX_COUNT, (16#10000)). %% 256*256
|
||||||
|
|
||||||
|
-define(XDB_SEGMENT_INDEX_SIZE, 14).
|
||||||
|
|
||||||
|
-define(IP2REGION_POOL, ip2region_pool).
|
||||||
|
|
||||||
|
-ifndef(IF).
|
||||||
|
-define(IF(C, T, F), case (C) of true -> (T); false -> (F) end).
|
||||||
|
-define(IF(C, T), ?IF(C, T, skip)).
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{erl_opts, [debug_info, export_all, nowarn_export_all]}.
|
||||||
|
{deps, [
|
||||||
|
poolboy
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{shell, [
|
||||||
|
% {config, "config/sys.config"},
|
||||||
|
{apps, [ip2region]}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{"1.2.0",
|
||||||
|
[{<<"poolboy">>,{pkg,<<"poolboy">>,<<"1.5.2">>},0}]}.
|
||||||
|
[
|
||||||
|
{pkg_hash,[
|
||||||
|
{<<"poolboy">>, <<"392B007A1693A64540CEAD79830443ABF5762F5D30CF50BC95CB2C1AAAFA006B">>}]},
|
||||||
|
{pkg_hash_ext,[
|
||||||
|
{<<"poolboy">>, <<"DAD79704CE5440F3D5A3681C8590B9DC25D1A561E8F5A9C995281012860901E3">>}]}
|
||||||
|
].
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{application, ip2region,
|
||||||
|
[{description, "An OTP application"},
|
||||||
|
{vsn, "0.1.0"},
|
||||||
|
{registered, []},
|
||||||
|
{mod, {ip2region_app, []}},
|
||||||
|
{applications,
|
||||||
|
[kernel,
|
||||||
|
stdlib
|
||||||
|
]},
|
||||||
|
{env,[
|
||||||
|
{poolargs, [
|
||||||
|
{size, 1},
|
||||||
|
{max_overflow, 5}
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
{modules, []},
|
||||||
|
|
||||||
|
{licenses, ["Apache-2.0"]},
|
||||||
|
{links, []}
|
||||||
|
]}.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
-module(ip2region).
|
||||||
|
-include("ip2region.hrl").
|
||||||
|
|
||||||
|
-export([search/1]).
|
||||||
|
|
||||||
|
-spec search(Ip :: tuple() | list() | binary()) -> Result :: {error, Reason::atom()} | map().
|
||||||
|
search(Ip) ->
|
||||||
|
case check_ip(Ip) of
|
||||||
|
{false, Reason} -> {error, Reason};
|
||||||
|
_ ->
|
||||||
|
Worker = poolboy:checkout(?IP2REGION_POOL, true, infinity),
|
||||||
|
try
|
||||||
|
ip2region_worker:search(Worker, Ip)
|
||||||
|
after
|
||||||
|
poolboy:checkin(?IP2REGION_POOL, Worker)
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
check_ip({_A, _B, _C, _D}) -> true;
|
||||||
|
check_ip(Ip) when is_list(Ip); is_binary(Ip) ->
|
||||||
|
IpRegx = "^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\.){3}(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])$",
|
||||||
|
case re:run(Ip, IpRegx) of
|
||||||
|
{match, _Captured} ->
|
||||||
|
ok;
|
||||||
|
_ ->
|
||||||
|
{false, bad_ip_format}
|
||||||
|
end.
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%% @doc ip2region public API
|
||||||
|
%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(ip2region_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
|
||||||
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
|
start(_StartType, _StartArgs) ->
|
||||||
|
ip2region_sup:start_link().
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% internal functions
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%% @doc ip2region top level supervisor.
|
||||||
|
%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(ip2region_sup).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
-include("ip2region.hrl").
|
||||||
|
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
-export([init/1, create_table/0]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
{ok, SupPid} = supervisor:start_link({local, ?SERVER}, ?MODULE, []),
|
||||||
|
{ok, _PoolPid} = start_ip2region_pool(SupPid),
|
||||||
|
{ok, SupPid}.
|
||||||
|
|
||||||
|
%% sup_flags() = #{strategy => strategy(), % optional
|
||||||
|
%% intensity => non_neg_integer(), % optional
|
||||||
|
%% period => pos_integer()} % optional
|
||||||
|
%% child_spec() = #{id => child_id(), % mandatory
|
||||||
|
%% start => mfargs(), % mandatory
|
||||||
|
%% restart => restart(), % optional
|
||||||
|
%% shutdown => shutdown(), % optional
|
||||||
|
%% type => worker(), % optional
|
||||||
|
%% modules => modules()} % optional
|
||||||
|
init([]) ->
|
||||||
|
create_table(),
|
||||||
|
SupFlags = #{strategy => one_for_one,
|
||||||
|
intensity => 10,
|
||||||
|
period => 5},
|
||||||
|
ChildSpecs = [],
|
||||||
|
{ok, {SupFlags, ChildSpecs}}.
|
||||||
|
|
||||||
|
%% internal functions
|
||||||
|
%%
|
||||||
|
create_table() ->
|
||||||
|
Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}],
|
||||||
|
ets:new(?XDB_VECTOR_INDEX, Opts),
|
||||||
|
ets:new(?XDB_SEGMENT_INDEX, Opts),
|
||||||
|
ets:new(?IP2REGION_CACHE, Opts).
|
||||||
|
|
||||||
|
start_ip2region_pool(Sup) ->
|
||||||
|
{ok, PoolArgsCfg} = application:get_env(poolargs),
|
||||||
|
PoolName = ?IP2REGION_POOL,
|
||||||
|
PoolArgs = [{strategy, fifo}, {name, {local, PoolName}}, {worker_module, ip2region_worker} | PoolArgsCfg],
|
||||||
|
WorkerArgs = [],
|
||||||
|
ChildSpecs = poolboy:child_spec(PoolName, PoolArgs, WorkerArgs),
|
||||||
|
supervisor:start_child(Sup, ChildSpecs).
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
-module(ip2region_worker).
|
||||||
|
-behaviour(gen_server).
|
||||||
|
-include("ip2region.hrl").
|
||||||
|
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start/1, stop/1, start_link/1]).
|
||||||
|
-export([search/2]).
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
-record(state, {xdb_fd}).
|
||||||
|
|
||||||
|
%%==========================================
|
||||||
|
%% API
|
||||||
|
%% =========================================
|
||||||
|
start(Args) ->
|
||||||
|
Opts = [{spawn_opt, [{min_heap_size, 6000}]}],
|
||||||
|
gen_server:start(?MODULE, Args, Opts).
|
||||||
|
|
||||||
|
start_link(Args) ->
|
||||||
|
Opts = [{spawn_opt, [{min_heap_size, 6000}]}],
|
||||||
|
gen_server:start_link(?MODULE, Args, Opts).
|
||||||
|
|
||||||
|
|
||||||
|
stop(Pid) ->
|
||||||
|
gen_server:call(Pid, stop).
|
||||||
|
|
||||||
|
search(Pid, Ip) ->
|
||||||
|
gen_server:call(Pid, {search, Ip}).
|
||||||
|
|
||||||
|
%%==========================================
|
||||||
|
%% gen_server callbacks
|
||||||
|
%% =========================================
|
||||||
|
init(_Args) ->
|
||||||
|
PrivDir = code:priv_dir(?APP_NAME),
|
||||||
|
XdbFileName = filename:join([PrivDir, "ip2region.xdb"]),
|
||||||
|
error_logger:info_report(io_lib:format("XdbFile:~s~n", [XdbFileName])),
|
||||||
|
{ok, IoDevice} = file:open(XdbFileName, [read, binary]),
|
||||||
|
load_vector_index(IoDevice),
|
||||||
|
{ok, #state{xdb_fd = IoDevice}}.
|
||||||
|
|
||||||
|
handle_call(Request, From, State) ->
|
||||||
|
try
|
||||||
|
do_call(Request, From, State)
|
||||||
|
catch
|
||||||
|
Class:Error:Stacktrace ->
|
||||||
|
error_logger:error_report(io_lib:format("~p handle call error, Req:~p ~p, stacktrace:~p~n",
|
||||||
|
[?MODULE, Request, {Class, Error}, Stacktrace])),
|
||||||
|
{reply, {error, {Class, Error}}, State}
|
||||||
|
end.
|
||||||
|
|
||||||
|
handle_cast(Msg, State) ->
|
||||||
|
try
|
||||||
|
do_cast(Msg, State)
|
||||||
|
catch
|
||||||
|
Class:Error:Stacktrace ->
|
||||||
|
error_logger:error_report(io_lib:format("~p handle cast error, Msg:~p, ~p, stacktrace:~w~n",
|
||||||
|
[?MODULE, Msg, {Class, Error}, Stacktrace])),
|
||||||
|
{noreply, State}
|
||||||
|
end.
|
||||||
|
|
||||||
|
handle_info(Info, State) ->
|
||||||
|
try
|
||||||
|
do_info(Info, State)
|
||||||
|
catch
|
||||||
|
Class:Error:Stacktrace ->
|
||||||
|
error_logger:error_report(io_lib:format("~p handle info error, Info:~p, ~p, stacktrace:~p~n",
|
||||||
|
[?MODULE, Info, {Class, Error}, Stacktrace])),
|
||||||
|
{noreply, State}
|
||||||
|
end.
|
||||||
|
|
||||||
|
terminate(_Reason, State) ->
|
||||||
|
#state{xdb_fd = XdbFd} = State,
|
||||||
|
case is_pid(XdbFd) of
|
||||||
|
true ->
|
||||||
|
file:close(XdbFd);
|
||||||
|
_ ->
|
||||||
|
skip
|
||||||
|
end,
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
|
||||||
|
%%==========================================
|
||||||
|
%% Internal function
|
||||||
|
%% =========================================
|
||||||
|
do_call({search, Ip}, _From, #state{xdb_fd = IoDevice} = State) ->
|
||||||
|
Reply = search_ip(IoDevice, Ip),
|
||||||
|
{reply, Reply, State};
|
||||||
|
|
||||||
|
do_call(stop, _From, State) ->
|
||||||
|
{stop, normal, stopped, State};
|
||||||
|
|
||||||
|
do_call(Request, From, State) ->
|
||||||
|
error_logger:error_report(io_lib:format("unknown request: ~p, from:~p", [Request, From])),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
do_cast(Msg, State) ->
|
||||||
|
error_logger:error_report(io_lib:format("unknown msg: ~p", [Msg])),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
do_info(Info, State) ->
|
||||||
|
error_logger:error_report(io:format("unknown info: ~p", [Info])),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
|
||||||
|
load_vector_index(IoDevice) ->
|
||||||
|
Key = ip2region_header_loaded,
|
||||||
|
case persistent_term:get(Key, false) of
|
||||||
|
true -> ok;
|
||||||
|
_ ->
|
||||||
|
{ok, <<_Header:?XDB_HEADER_SIZE/binary, VectorIndexBin/binary>> } =
|
||||||
|
file:read(IoDevice, ?XDB_HEADER_SIZE + ?XDB_VECTOR_INDEX_COUNT*8),
|
||||||
|
load_vector_index_aux(VectorIndexBin, 0),
|
||||||
|
persistent_term:put(Key, true)
|
||||||
|
end.
|
||||||
|
|
||||||
|
load_vector_index_aux(<<>>, _Index) -> ok;
|
||||||
|
load_vector_index_aux(<<SPtr:32/little, EPtr:32/little, VectorIndexBin/binary>>, Index) ->
|
||||||
|
Term = {Index, SPtr, EPtr},
|
||||||
|
ets:insert(?XDB_VECTOR_INDEX, Term),
|
||||||
|
load_vector_index_aux(VectorIndexBin, Index + 1).
|
||||||
|
|
||||||
|
|
||||||
|
search_ip(IoDevice, Ip) ->
|
||||||
|
IntIp = util:ipv4_to_n(Ip),
|
||||||
|
case ets:lookup(?IP2REGION_CACHE, IntIp) of
|
||||||
|
[{_IntIp, RegionInfo}] ->
|
||||||
|
RegionInfo;
|
||||||
|
_ ->
|
||||||
|
<<A:8, B:8, _Rest/binary>> = <<IntIp:32>>,
|
||||||
|
VectorIdx = A * ?XDB_VECTOR_COLS + B,
|
||||||
|
[{_, 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.
|
||||||
|
|
||||||
|
search_ip(IoDevice, IntIp, SPtr, EPtr, Low, High) when Low =< High ->
|
||||||
|
Middle = (Low + High) bsr 1,
|
||||||
|
SPtr2 = SPtr + Middle * ?XDB_SEGMENT_INDEX_SIZE,
|
||||||
|
{SIp, EIp, DataLen, DataPtr} = read_segement_index(IoDevice, SPtr2),
|
||||||
|
if
|
||||||
|
IntIp < SIp ->
|
||||||
|
search_ip(IoDevice, IntIp, SPtr, EPtr, Low, Middle - 1);
|
||||||
|
IntIp > EIp ->
|
||||||
|
search_ip(IoDevice, IntIp, SPtr, EPtr, Middle + 1, High);
|
||||||
|
true ->
|
||||||
|
{ok, DataBin} = read_file(IoDevice, DataPtr, DataLen),
|
||||||
|
[Country, Region, Province, City, ISP] = string:tokens(binary_to_list(DataBin), "|"),
|
||||||
|
#{
|
||||||
|
country => ?IF(Country == "0", <<>>, list_to_binary(Country)),
|
||||||
|
region => ?IF(Region == "0", <<>>, list_to_binary(Region)),
|
||||||
|
province => ?IF(Province == "0", <<>>, list_to_binary(Province)),
|
||||||
|
city => ?IF(City == "0", <<>>, list_to_binary(City)),
|
||||||
|
isp => ?IF(ISP == "0", <<>>, list_to_binary(ISP))
|
||||||
|
}
|
||||||
|
end;
|
||||||
|
search_ip(_IoDevice, _IntIp, _SPtr, _EPtr, _Low, _High) ->
|
||||||
|
{error, unknown}.
|
||||||
|
|
||||||
|
read_file(IoDevice, Position, DataLength) ->
|
||||||
|
file:position(IoDevice, {bof, Position}),
|
||||||
|
file:read(IoDevice, DataLength).
|
||||||
|
|
||||||
|
read_segement_index(IoDevice, SPtr) ->
|
||||||
|
case ets:lookup(?XDB_SEGMENT_INDEX, SPtr) of
|
||||||
|
[{_SPtr, SIp, EIp, DataLen, DataPtr}] ->
|
||||||
|
{SIp, EIp, DataLen, DataPtr};
|
||||||
|
_ ->
|
||||||
|
{ok, <<SIp:32/little, EIp:32/little, DataLen:16/little, DataPtr:32/little>>} =
|
||||||
|
read_file(IoDevice, SPtr, ?XDB_SEGMENT_INDEX_SIZE),
|
||||||
|
ets:insert_new(?XDB_SEGMENT_INDEX, {SPtr, SIp, EIp, DataLen, DataPtr}),
|
||||||
|
{SIp, EIp, DataLen, DataPtr}
|
||||||
|
end.
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
-module(util).
|
||||||
|
-export([ipv4_to_n/1]).
|
||||||
|
|
||||||
|
|
||||||
|
ip_aton(Ip) ->
|
||||||
|
{ok, Addr} = inet_parse:address(Ip),
|
||||||
|
Addr.
|
||||||
|
|
||||||
|
ipv4_to_n({A, B, C, 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) ->
|
||||||
|
Addr = ip_aton(Ip),
|
||||||
|
ipv4_to_n(Addr).
|
||||||
Loading…
Reference in New Issue