diff --git a/.gitignore b/.gitignore index a276f0f..e918a0d 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,6 @@ target /maker/golang/dbmaker /maker/golang/xdb_maker /maker/golang/golang + +#erlang +/binding/erlang/_build diff --git a/binding/erlang/README.md b/binding/erlang/README.md new file mode 100644 index 0000000..8b35fd3 --- /dev/null +++ b/binding/erlang/README.md @@ -0,0 +1,9 @@ +ip2region +===== + +An OTP application + +Build +----- + + $ rebar3 compile diff --git a/binding/erlang/include/ip2region.hrl b/binding/erlang/include/ip2region.hrl new file mode 100644 index 0000000..a0e91e7 --- /dev/null +++ b/binding/erlang/include/ip2region.hrl @@ -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. \ No newline at end of file diff --git a/binding/erlang/rebar.config b/binding/erlang/rebar.config new file mode 100644 index 0000000..099d6ab --- /dev/null +++ b/binding/erlang/rebar.config @@ -0,0 +1,10 @@ +{erl_opts, [debug_info, export_all, nowarn_export_all]}. +{deps, [ + poolboy +]}. + +{shell, [ + % {config, "config/sys.config"}, + {apps, [ip2region]} +]}. + diff --git a/binding/erlang/rebar.lock b/binding/erlang/rebar.lock new file mode 100644 index 0000000..1ab8da9 --- /dev/null +++ b/binding/erlang/rebar.lock @@ -0,0 +1,8 @@ +{"1.2.0", +[{<<"poolboy">>,{pkg,<<"poolboy">>,<<"1.5.2">>},0}]}. +[ +{pkg_hash,[ + {<<"poolboy">>, <<"392B007A1693A64540CEAD79830443ABF5762F5D30CF50BC95CB2C1AAAFA006B">>}]}, +{pkg_hash_ext,[ + {<<"poolboy">>, <<"DAD79704CE5440F3D5A3681C8590B9DC25D1A561E8F5A9C995281012860901E3">>}]} +]. diff --git a/binding/erlang/src/ip2region.app.src b/binding/erlang/src/ip2region.app.src new file mode 100644 index 0000000..a0b595b --- /dev/null +++ b/binding/erlang/src/ip2region.app.src @@ -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, []} + ]}. diff --git a/binding/erlang/src/ip2region.erl b/binding/erlang/src/ip2region.erl new file mode 100644 index 0000000..e39ecaa --- /dev/null +++ b/binding/erlang/src/ip2region.erl @@ -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. \ No newline at end of file diff --git a/binding/erlang/src/ip2region_app.erl b/binding/erlang/src/ip2region_app.erl new file mode 100644 index 0000000..2874d68 --- /dev/null +++ b/binding/erlang/src/ip2region_app.erl @@ -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 diff --git a/binding/erlang/src/ip2region_sup.erl b/binding/erlang/src/ip2region_sup.erl new file mode 100644 index 0000000..4f57512 --- /dev/null +++ b/binding/erlang/src/ip2region_sup.erl @@ -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). \ No newline at end of file diff --git a/binding/erlang/src/ip2region_worker.erl b/binding/erlang/src/ip2region_worker.erl new file mode 100644 index 0000000..c903067 --- /dev/null +++ b/binding/erlang/src/ip2region_worker.erl @@ -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(<>, 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; + _ -> + <> = <>, + 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, <>} = + read_file(IoDevice, SPtr, ?XDB_SEGMENT_INDEX_SIZE), + ets:insert_new(?XDB_SEGMENT_INDEX, {SPtr, SIp, EIp, DataLen, DataPtr}), + {SIp, EIp, DataLen, DataPtr} + end. \ No newline at end of file diff --git a/binding/erlang/src/util.erl b/binding/erlang/src/util.erl new file mode 100644 index 0000000..0890ef0 --- /dev/null +++ b/binding/erlang/src/util.erl @@ -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; +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). \ No newline at end of file