├── Emakefile ├── ebin └── kdht.app ├── include └── vlog.hrl ├── test └── testdht.erl ├── src ├── conf.hrl ├── kdht_sup.erl ├── dht_id.erl ├── vlog.erl ├── bencode.erl ├── storage.erl ├── timer_monitor.erl ├── bucket.erl ├── msg.erl ├── search.erl ├── dht_state.erl └── dht_net.erl └── README.md /Emakefile: -------------------------------------------------------------------------------- 1 | {'src/*', 2 | [{i, "include"}, 3 | {outdir,"ebin"}]}. 4 | {'test/*', 5 | [{i, "include"}, 6 | {outdir,"ebin"}]}. 7 | -------------------------------------------------------------------------------- /ebin/kdht.app: -------------------------------------------------------------------------------- 1 | {application, kdht, [ 2 | {description, "DHT library"}, 3 | {vsn, git}, 4 | {registered, []}, 5 | {applications, [kernel, stdlib]}, 6 | {modules, [bencode, bucket, dht_id, dht_net, dht_state, kdht_sup, msg, 7 | search, storage, timer_monitor, vlog]}, 8 | {mod, {}} 9 | ]}. 10 | 11 | -------------------------------------------------------------------------------- /include/vlog.hrl: -------------------------------------------------------------------------------- 1 | %% 2 | %% vlog.hrl 3 | %% Kevin Lynx 4 | %% 06.05.2013 5 | %% 6 | -ifndef(VLOGHRL). 7 | -define(VLOGHRL, true). 8 | 9 | -define(TRACE, 0). 10 | -define(INFO, 1). 11 | -define(WARN, 2). 12 | -define(ERROR, 3). 13 | -define(OFF, 4). 14 | 15 | -define(LVLS(L), 16 | case L of 17 | ?TRACE -> "trac"; 18 | ?INFO -> "info"; 19 | ?WARN -> "warn"; 20 | ?ERROR -> "error" 21 | end). 22 | -define(LOG(X, Lvl), 23 | vlog:format(Lvl, "~s [~s] {~p, ~p}: ~p~n", 24 | [?LVLS(Lvl), vlog:time_string(), ?MODULE, ?LINE, X])). 25 | -define(T(X), ?LOG(X, ?TRACE)). 26 | -define(I(X), ?LOG(X, ?INFO)). 27 | -define(W(X), ?LOG(X, ?WARN)). 28 | -define(E(X), ?LOG(X, ?ERROR)). 29 | 30 | -define(FMT(S, A), lists:flatten(io_lib:format(S, A))). 31 | 32 | -endif. 33 | -------------------------------------------------------------------------------- /test/testdht.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% testdht.erl 3 | %% Kevin Lynx 4 | %% 5 | -module(testdht). 6 | -include("vlog.hrl"). 7 | -export([start/0, stop/1, handle_event/2]). 8 | -export([tell_more_nodes/1]). 9 | 10 | start() -> 11 | vlog:start_link("kdht.txt", ?TRACE), 12 | random:seed(now()), 13 | kdht_sup:start_link("dhtstate.dat", 6882, ?MODULE, dht_id:random()). 14 | 15 | stop(Pid) -> 16 | kdht_sup:stop(Pid). 17 | 18 | handle_event(announce_peer, {InfoHash, _IP, _BTPort}) -> 19 | MagHash = dht_id:tohex(InfoHash), 20 | {ok, FP} = file:open("magnet-link.txt", [append]), 21 | io:format(FP, "~s~n", [MagHash]), 22 | file:close(FP); 23 | 24 | handle_event(startup, {MyID}) -> 25 | spawn_link(?MODULE, tell_more_nodes, [MyID]). 26 | 27 | tell_more_nodes(MyID) -> 28 | [search:get_peers(MyID, dht_id:random()) || _ <- lists:seq(1, 10)], 29 | ?I("tell more nodes done"). 30 | -------------------------------------------------------------------------------- /src/conf.hrl: -------------------------------------------------------------------------------- 1 | %% 2 | %% conf.hrl 3 | %% Kevin Lynx 4 | %% 06.11.2013 5 | %% 6 | -ifndef(KDHTCONFHRL). 7 | -define(KDHTCONFHRL, true). 8 | 9 | %% The time waiting for a response for a query 10 | -define(QUERY_TIMEOUT, 2000). 11 | -define(K, 8). 12 | 13 | % system monitor flags 14 | -define(MAX_NODE_TIMER, 160*8). 15 | 16 | -ifdef(debug). 17 | 18 | -define(NODE_TIMEOUT, 1*60*1000). 19 | %% Not implemented yet 20 | -define(BUCKET_TIMEOUT, 1*60*1000). 21 | %% InfoHash expire time 22 | -define(EXPIRETIME, 1000*5*60). 23 | %% InfoHash checking interval time 24 | -define(CHECK_INTERVAL, 1000*2*60). 25 | -define(SAVE_INTERVAL, 2*60*1000). 26 | -else. 27 | 28 | -define(NODE_TIMEOUT, 10*60*1000). 29 | %% Not implemented yet 30 | -define(BUCKET_TIMEOUT, 10*60*1000). 31 | -define(EXPIRETIME, 1000*30*60). 32 | %% InfoHash checking interval time 33 | -define(CHECK_INTERVAL, 1000*10*60). 34 | -define(SAVE_INTERVAL, 5*60*1000). 35 | 36 | -endif. 37 | -endif. 38 | -------------------------------------------------------------------------------- /src/kdht_sup.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% kdht_sup.erl 3 | %% Kevin Lynx 4 | %% 5 | -module(kdht_sup). 6 | -include("vlog.hrl"). 7 | -include("conf.hrl"). 8 | -behaviour(supervisor). 9 | -export([init/1]). 10 | -export([start_link/4, 11 | save_state/1, 12 | stop/1]). 13 | 14 | start_link(StateFile, Port, Mod, OptID) -> 15 | supervisor:start_link(?MODULE, [StateFile, Port, Mod, OptID]). 16 | 17 | stop(Pid) -> 18 | save_state(Pid), 19 | exit(Pid, normal). 20 | 21 | save_state(Pid) -> 22 | MyID = get_dhtid(Pid), 23 | dht_state:save(MyID). 24 | 25 | init([StateFile, Port, Mod, OptID]) -> 26 | random:seed(now()), 27 | {MyID, NodeAddrs} = case dht_state:load(StateFile) of 28 | {error, _} -> 29 | {OptID, []}; 30 | Ret -> 31 | Ret 32 | end, 33 | save_dhtid(MyID), 34 | Spec = {one_for_all, 1, 600}, 35 | Children = [{timer_monitor, {timer_monitor, start_link, [MyID]}, permanent, 1000, worker, dynamic}, 36 | {storage, {storage, start_link, [MyID]}, permanent, 1000, worker, dynamic}, 37 | {dht_net, {dht_net, start_link, [MyID, Port]}, permanent, 1000, worker, dynamic}, 38 | {dht_state, {dht_state, start_link, [MyID, NodeAddrs, StateFile, Mod]}, permanent, 1000, worker, dynamic}], 39 | {ok, {Spec, Children}}. 40 | 41 | save_dhtid(ID) -> 42 | put(dhtid, ID). 43 | 44 | get_dhtid(Pid) -> 45 | PInfo = process_info(Pid), 46 | Dict = proplists:get_value(dictionary, PInfo), 47 | proplists:get_value(dhtid, Dict). 48 | -------------------------------------------------------------------------------- /src/dht_id.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% dht_id.erl 3 | %% Kevin Lynx 4 | %% 06.05.2013 5 | %% 6 | -module(dht_id). 7 | -export([random/0, 8 | distance/2, 9 | min/0, 10 | max/0, 11 | id_cmp/2, 12 | is_valid/1, 13 | integer_id/1, 14 | list_id/1, 15 | tohex/1]). 16 | 17 | random() -> 18 | Byte = fun() -> random:uniform(256) - 1 end, 19 | Bytes = [Byte() || _ <- lists:seq(1, 20)], 20 | integer_id(list_to_binary(Bytes)). 21 | 22 | distance(BID0, BID1) when is_binary(BID0), is_binary(BID1) -> 23 | <> = BID0, 24 | <> = BID1, 25 | ID0 bxor ID1; 26 | 27 | distance(ID0, ID1) when is_integer(ID0), is_integer(ID1) -> 28 | ID0 bxor ID1. 29 | 30 | min() -> 31 | 0. 32 | 33 | max() -> 34 | integer_id(list_to_binary([255 || _ <- lists:seq(1, 20)])). 35 | 36 | id_cmp(ID1, ID2) when is_binary(ID1) and is_binary(ID2) -> 37 | if 38 | ID1 < ID2 -> -1; 39 | ID1 > ID2 -> 1; 40 | true -> 0 41 | end. 42 | 43 | integer_id(BID) when is_binary(BID) -> 44 | <> = BID, 45 | ID. 46 | 47 | list_id(ID) -> 48 | <>. 49 | 50 | tohex(ID) when is_binary(ID) -> 51 | lists:flatten([io_lib:format("~2.16.0B", [X]) || X <- binary_to_list(ID)]); 52 | 53 | tohex(ID) -> 54 | tohex(list_id(ID)). 55 | 56 | is_valid(ID) when is_binary(ID) -> 57 | is_valid(integer_id(ID)); 58 | is_valid(ID) -> 59 | (ID > min()) and (ID < max()). 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## kdht 2 | 3 | kdht is an erlang [DHT](http://en.wikipedia.org/wiki/Distributed_hash_table) implementation. You can use this library to join a DHT network, and search torrent info-hash in the network. 4 | 5 | ## Usage 6 | 7 | * Download this library source code 8 | * Compile it by `erl` in the library root directory: 9 | 10 | erl -make 11 | 12 | * Start `erl` and specify the module beam path: 13 | 14 | erl -pa ebin 15 | 16 | * Run test 17 | 18 | testdht:start(). 19 | 20 | ## Example 21 | 22 | -module(testdht). 23 | -include("vlog.hrl"). 24 | -export([start/0, stop/1, handle_event/2]). 25 | -export([tell_more_nodes/1]). 26 | 27 | start() -> 28 | vlog:start_link("kdht.txt", ?TRACE), 29 | random:seed(now()), 30 | kdht_sup:start_link("dhtstate.dat", 6882, ?MODULE, dht_id:random()). 31 | 32 | stop(Pid) -> 33 | kdht_sup:stop(Pid). 34 | 35 | handle_event(announce_peer, {InfoHash, _IP, _BTPort}) -> 36 | MagHash = dht_id:tohex(InfoHash), 37 | {ok, FP} = file:open("magnet-link.txt", [append]), 38 | io:format(FP, "~s~n", [MagHash]), 39 | file:close(FP); 40 | 41 | handle_event(startup, {MyID}) -> 42 | spawn_link(?MODULE, tell_more_nodes, [MyID]). 43 | 44 | tell_more_nodes(MyID) -> 45 | [search:get_peers(MyID, dht_id:random()) || _ <- lists:seq(1, 10)], 46 | ?I("tell more nodes done"). 47 | 48 | ## TODO 49 | 50 | * token implementation 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/vlog.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% vlog.erl 3 | %% Kevin Lynx 4 | %% 06.05.2013 5 | %% 6 | -module(vlog). 7 | -behaviour(gen_server). 8 | -export([init/1, 9 | handle_call/3, 10 | handle_cast/2, 11 | handle_info/2, 12 | terminate/2, 13 | code_change/3]). 14 | -export([start_link/2, 15 | format/3, 16 | set_level/1, 17 | sync_format/3, 18 | time_string/0, 19 | stop/0]). 20 | -record(state, {name, level}). 21 | -include("vlog.hrl"). 22 | 23 | start_link(Name, Lvl) -> 24 | gen_server:start_link({local, srv_name()}, ?MODULE, [Name, Lvl], []). 25 | 26 | stop() -> 27 | gen_server:cast(srv_name(), stop). 28 | 29 | set_level(Level) -> 30 | gen_server:call(srv_name(), {set_level, Level}). 31 | 32 | format(Lvl, Fmt, Arg) -> 33 | gen_server:cast(srv_name(), {write, Lvl, Fmt, Arg}). 34 | 35 | sync_format(Lvl, Fmt, Arg) -> 36 | gen_server:call(srv_name(), {write, Lvl, Fmt, Arg}). 37 | 38 | time_string() -> 39 | {{_Year, _Month, _Day}, {Hour, Min, Sec}} = erlang:localtime(), 40 | lists:flatten(io_lib:format("~2.10.0b:~2.10.0b:~2.10.0b", [Hour, Min, Sec])). 41 | 42 | srv_name() -> 43 | ?MODULE. 44 | 45 | init([Name, Lvl]) -> 46 | {ok, FP} = file:open(Name, [write]), 47 | file:close(FP), 48 | {ok, #state{name = Name, level = Lvl}}. 49 | 50 | handle_info(_, State) -> 51 | {noreply, State}. 52 | 53 | handle_cast({write, Level, Fmt, Arg}, State) -> 54 | do_log(State, Level, Fmt, Arg), 55 | {noreply, State}; 56 | 57 | handle_cast(stop, State) -> 58 | {stop, normal, State}; 59 | 60 | handle_cast(_, State) -> 61 | {noreply, State}. 62 | 63 | terminate(_, State) -> 64 | {ok, State}. 65 | 66 | code_change(_, _, State) -> 67 | {ok, State}. 68 | 69 | handle_call({write, Level, Fmt, Arg}, _From, State) -> 70 | do_log(State, Level, Fmt, Arg), 71 | {reply, ok, State}; 72 | 73 | handle_call({set_level, Level}, _From, State) -> 74 | {reply, ok, State#state{level = Level}}; 75 | 76 | handle_call(_, _From, State) -> 77 | {reply, not_implemented, State}. 78 | 79 | do_log(State, Level, Fmt, Arg) -> 80 | #state{name = Name, level = MaxLevel} = State, 81 | case Level >= MaxLevel of 82 | true -> append(Name, Fmt, Arg); 83 | false -> false 84 | end. 85 | 86 | append(Name, Fmt, Arg) -> 87 | {ok, FP} = file:open(Name, [append]), 88 | io:format(FP, Fmt, Arg), 89 | file:close(FP). 90 | 91 | -------------------------------------------------------------------------------- /src/bencode.erl: -------------------------------------------------------------------------------- 1 | -module(bencode). 2 | 3 | %% API 4 | -export([decode/1, encode/1]). 5 | -export([dec/1]). 6 | 7 | %% You are able to choose the dict implementation 8 | -define(DICT, dict). 9 | %%-define(DICT, orddict). 10 | 11 | %%==================================================================== 12 | %% API 13 | %%==================================================================== 14 | decode(Data) -> 15 | case catch dec(Data) of 16 | {'EXIT', _} -> 17 | {error, unparsed}; 18 | {Res, _} -> 19 | {ok, Res} 20 | end. 21 | 22 | encode(Struct) -> 23 | iolist_to_binary(enc(Struct)). 24 | 25 | %%==================================================================== 26 | %% Internal functions 27 | %%==================================================================== 28 | %%-------------------------------------------------------------------- 29 | %% Decoding 30 | %%-------------------------------------------------------------------- 31 | dec(<<$l, Tail/binary>>) -> 32 | dec_list(Tail, []); 33 | dec(<<$d, Tail/binary>>) -> 34 | dec_dict(Tail, ?DICT:new()); 35 | dec(<<$i, Tail/binary>>) -> 36 | dec_int(Tail, []); 37 | dec(Data) -> 38 | dec_string(Data, []). 39 | 40 | dec_int(<<$e, Tail/binary>>, Acc) -> 41 | {list_to_integer(lists:reverse(Acc)), Tail}; 42 | dec_int(<>, Acc) -> 43 | dec_int(Tail, [X|Acc]). 44 | 45 | dec_string(<<$:, Tail/binary>>, Acc) -> 46 | Int = list_to_integer(lists:reverse(Acc)), 47 | <> = Tail, 48 | {Str, Rest}; 49 | dec_string(<>, Acc) -> 50 | dec_string(Tail, [X|Acc]). 51 | 52 | dec_list(<<$e, Tail/binary>>, Acc) -> 53 | {{list, lists:reverse(Acc)}, Tail}; 54 | dec_list(Data, Acc) -> 55 | {Res, Tail} = dec(Data), 56 | dec_list(Tail, [Res|Acc]). 57 | 58 | dec_dict(<<$e, Tail/binary>>, Acc) -> 59 | {{dict, Acc}, Tail}; 60 | dec_dict(Data, Acc) -> 61 | {Key, Tail1} = dec(Data), 62 | {Val, Tail2} = dec(Tail1), 63 | dec_dict(Tail2, ?DICT:store(Key, Val, Acc)). 64 | 65 | %%-------------------------------------------------------------------- 66 | %% Encoding 67 | %%-------------------------------------------------------------------- 68 | enc(Int) when is_integer(Int) -> 69 | IntBin = list_to_binary(integer_to_list(Int)), 70 | [$i, IntBin, $e]; 71 | enc(Str) when is_list(Str) -> 72 | enc(list_to_binary(Str)); 73 | enc(Str) when is_binary(Str) -> 74 | IntBin = list_to_binary(integer_to_list(size(Str))), 75 | [IntBin, $:, Str]; 76 | enc({list, List}) when is_list(List) -> 77 | [$l, [enc(Elem) || Elem <- List], $e]; 78 | enc({dict, Dict}) -> 79 | Data = lists:map( 80 | fun({Key, Val}) when is_list(Key) or is_binary(Key) -> 81 | [enc(Key), enc(Val)] 82 | end, lists:keysort(1, ?DICT:to_list(Dict))), 83 | [$d, Data, $e]. 84 | 85 | -------------------------------------------------------------------------------- /src/storage.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% storage.erl 3 | %% Kevin Lynx 4 | %% 06.09.2013 5 | %% 6 | -module(storage). 7 | -behaviour(gen_server). 8 | -include("vlog.hrl"). 9 | -export([init/1, 10 | handle_info/2, 11 | handle_cast/2, 12 | handle_call/3, 13 | code_change/3, 14 | terminate/2]). 15 | -export([start_link/1, 16 | stop/1, 17 | query/2, 18 | insert/4]). 19 | -record(state, {hashes}). 20 | -include("conf.hrl"). 21 | 22 | start_link(MyID) -> 23 | gen_server:start_link({local, srv_name(MyID)}, ?MODULE, [], []). 24 | 25 | stop(MyID) -> 26 | gen_server:cast(srv_name(MyID), stop). 27 | 28 | query(MyID, InfoHash) -> 29 | gen_server:call(srv_name(MyID), {query, InfoHash}). 30 | 31 | insert(MyID, InfoHash, IP, Port) -> 32 | gen_server:call(srv_name(MyID), {insert, InfoHash, IP, Port}). 33 | 34 | srv_name(MyID) -> 35 | list_to_atom("dht_storage" ++ dht_id:tohex(MyID)). 36 | 37 | init([]) -> 38 | {ok, #state{hashes = gb_trees:empty()}}. 39 | 40 | handle_cast(stop, State) -> 41 | {stop, normal, State}; 42 | 43 | handle_cast(_, State) -> 44 | {noreply, State}. 45 | 46 | terminate(_, State) -> 47 | {ok, State}. 48 | 49 | code_change(_, _, State) -> 50 | {ok, State}. 51 | 52 | handle_call({query, InfoHash}, _From, State) -> 53 | #state{hashes = Hashes} = State, 54 | Peers = case gb_trees:is_defined(InfoHash, Hashes) of 55 | false -> 56 | []; 57 | true -> 58 | {_, Exist} = gb_trees:get(InfoHash, Hashes), 59 | Exist 60 | end, 61 | OnlyAddr = [{IP, Port} || {IP, Port, _} <- Peers], 62 | {reply, OnlyAddr, State}; 63 | 64 | handle_call({insert, InfoHash, IP, Port}, _From, #state{hashes = Hashes} = State) -> 65 | {T, Exist} = case gb_trees:is_defined(InfoHash, Hashes) of 66 | false -> 67 | {ok, TRef} = timer:send_interval(?CHECK_INTERVAL, {expire, InfoHash}), 68 | ?T(?FMT("start a new hash timer ~p", [TRef])), 69 | {TRef, []}; 70 | true -> 71 | gb_trees:get(InfoHash, Hashes) 72 | end, 73 | ?I(?FMT("insert a peer ~p:~p for hash ~s", [IP, Port, dht_id:tohex(InfoHash)])), 74 | New = Exist ++ [{IP, Port, now()}], 75 | NewTree = gb_trees:enter(InfoHash, {T, New}, Hashes), 76 | {reply, ok, State#state{hashes = NewTree}}. 77 | 78 | handle_info({expire, InfoHash}, State) -> 79 | #state{hashes = Hashes} = State, 80 | {TRef, Peers} = gb_trees:get(InfoHash, Hashes), 81 | NewPeers = [{IP, Port, Time} || {IP, Port, Time} <- Peers, 82 | (timer:now_diff(now(), Time) div 1000) < ?EXPIRETIME], 83 | NewHashes = case length(NewPeers) == 0 of 84 | true -> 85 | timer:cancel(TRef), 86 | ?T(?FMT("expire ~p peers (all) for infohash ~s", [length(Peers), dht_id:tohex(InfoHash)])), 87 | gb_trees:delete(InfoHash, Hashes); 88 | false -> 89 | ?T(?FMT("expire ~p peers for infohash ~s", 90 | [length(Peers) - length(NewPeers), dht_id:tohex(InfoHash)])), 91 | gb_trees:update(InfoHash, {TRef, NewPeers}, Hashes) 92 | end, 93 | {noreply, State#state{hashes = NewHashes}}; 94 | 95 | handle_info(_, State) -> 96 | {noreply, State}. 97 | 98 | -------------------------------------------------------------------------------- /src/timer_monitor.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% timer_monitor.erl 3 | %% Kevin Lynx 4 | %% 06.05.2013 5 | %% 6 | -module(timer_monitor). 7 | -include("vlog.hrl"). 8 | -include("conf.hrl"). 9 | -behaviour(gen_server). 10 | -export([init/1, 11 | handle_call/3, 12 | handle_cast/2, 13 | handle_info/2, 14 | terminate/2, 15 | code_change/3]). 16 | -export([start_link/1, 17 | stop/1, 18 | add/4, 19 | get/2, 20 | active/2, 21 | remove/2]). 22 | -record(state, {timers}). 23 | 24 | start_link(MyID) -> 25 | gen_server:start_link({local, srv_name(MyID)}, ?MODULE, [], []). 26 | 27 | stop(MyID) -> 28 | gen_server:cast(srv_name(MyID), stop). 29 | 30 | add(MyID, Key, Timeout, Msg) -> 31 | gen_server:call(srv_name(MyID), {add, Key, Timeout, Msg}). 32 | 33 | get(MyID, Key) -> 34 | gen_server:call(srv_name(MyID), {get, Key}). 35 | 36 | remove(MyID, Key) -> 37 | gen_server:call(srv_name(MyID), {remove, Key}). 38 | 39 | active(MyID, Key) -> 40 | gen_server:call(srv_name(MyID), {active, Key}). 41 | 42 | srv_name(MyID) -> 43 | list_to_atom("timer_monitor" ++ dht_id:tohex(MyID)). 44 | 45 | init([]) -> 46 | {ok, #state{timers = gb_trees:empty()}}. 47 | 48 | handle_info({check, Key, Timeout, From, Msg}, State) -> 49 | #state{timers = Timers} = State, 50 | {Active, _} = gb_trees:get(Key, Timers), 51 | case (timer:now_diff(now(), Active) div 1000) >= Timeout of 52 | true -> 53 | From ! Msg; 54 | false -> continue 55 | end, 56 | {noreply, State}; 57 | 58 | handle_info(_, State) -> 59 | {noreply, State}. 60 | 61 | handle_cast(stop, State) -> 62 | {stop, normal, State}; 63 | 64 | handle_cast(_, State) -> 65 | {noreply, State}. 66 | 67 | terminate(_, State) -> 68 | {ok, State}. 69 | 70 | code_change(_, _, State) -> 71 | {ok, State}. 72 | 73 | handle_call({add, Key, Timeout, Msg}, From, State) -> 74 | #state{timers = Timers} = State, 75 | {Pid, _} = From, 76 | ThisMsg = {check, Key, Timeout, Pid, Msg}, 77 | {ok, NewRef} = timer:send_interval(Timeout, ThisMsg), 78 | NewTimers = gb_trees:insert(Key, {now(), NewRef}, Timers), 79 | check_stats(gb_trees:size(NewTimers)), 80 | {reply, ok, State#state{timers = NewTimers}}; 81 | 82 | handle_call({active, Key}, _From, State) -> 83 | #state{timers = Timers} = State, 84 | false = gb_trees:is_empty(Timers), 85 | {_, TRef} = gb_trees:get(Key, Timers), 86 | NewTimers = gb_trees:update(Key, {now(), TRef}, Timers), 87 | {reply, ok, State#state{timers = NewTimers}}; 88 | 89 | handle_call({remove, Key}, _From, State) -> 90 | #state{timers = Timers} = State, 91 | {_, TRef} = gb_trees:get(Key, Timers), 92 | timer:cancel(TRef), 93 | NewTimers = gb_trees:delete(Key, Timers), 94 | {reply, ok, State#state{timers = NewTimers}}; 95 | 96 | handle_call({get, Key}, _From, State) -> 97 | #state{timers = Timers} = State, 98 | Active = case gb_trees:is_defined(Key, Timers) of 99 | true -> 100 | {A, _} = gb_trees:get(Key, Timers), 101 | A; 102 | false -> 103 | error 104 | end, 105 | {reply, Active, State}; 106 | 107 | handle_call(_, _From, State) -> 108 | {reply, not_implemented, State}. 109 | 110 | check_stats(TimerSize) -> 111 | case TimerSize > ?MAX_NODE_TIMER of 112 | true -> ?E(?FMT("~p node timer ~p > ~p", [self(), TimerSize, ?MAX_NODE_TIMER])); 113 | false -> ok 114 | end. 115 | 116 | -------------------------------------------------------------------------------- /src/bucket.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% bucket.erl 3 | %% Kevin Lynx 4 | %% 06.05.2013 5 | %% 6 | -module(bucket). 7 | -export([new/0, 8 | members/2, 9 | all/1, 10 | closest/2, 11 | closest/3, 12 | closest_to/3, 13 | is_member/4, 14 | delete/4, 15 | dump/1, 16 | insert/5]). 17 | -define(in_range(ID, Min, Max), ((ID >= Min) and (ID < Max))). 18 | -include("conf.hrl"). 19 | 20 | new() -> 21 | Max = dht_id:max(), 22 | Min = dht_id:min(), 23 | [{Min, Max, []}]. 24 | 25 | % return new Buckets 26 | insert(Self, ID, IP, Port, Buckets) -> 27 | do_insert(Self, ID, IP, Port, Buckets). 28 | 29 | members(ID, [{Min, Max, Members}|_]) when ?in_range(ID, Min, Max) -> 30 | Members; 31 | 32 | members(ID, [_|T]) -> 33 | members(ID, T). 34 | 35 | % return all nodes in all buckets 36 | all([]) -> 37 | []; 38 | 39 | all([{_, _, Members}|T]) -> 40 | Members++all(T). 41 | 42 | % find ?K closest nodes to target in all buckets 43 | closest(Target, Buckets) -> 44 | closest(Target, Buckets, ?K). 45 | 46 | closest(Target, Buckets, Num) -> 47 | Nodes = all(Buckets), 48 | closest_to(Target, Nodes, Num). 49 | 50 | closest_to(Target, Nodes, Num) -> 51 | WithDist = [{dht_id:distance(Target, ID), {ID, IP, Port}} || 52 | {ID, IP, Port} <- Nodes], 53 | Sorted = lists:sort(WithDist), 54 | Limited = case length(Sorted) > Num of 55 | true -> 56 | {Prev, _} = lists:split(Num, Sorted), 57 | Prev; 58 | false -> 59 | Sorted 60 | end, 61 | [Node || {_, Node} <- Limited]. 62 | 63 | % check if a node is a member of a bucket list 64 | is_member(_, _, _, []) -> 65 | false; 66 | is_member(ID, IP, Port, [{Min, Max, Members}|_]) 67 | when ?in_range(ID, Min, Max) -> 68 | lists:member({ID, IP, Port}, Members); 69 | is_member(ID, IP, Port, [_|T]) -> 70 | is_member(ID, IP, Port, T). 71 | 72 | % delete a Node from bucket list 73 | delete(_, _, _, []) -> 74 | []; 75 | delete(ID, IP, Port, [{Min, Max, Members}|T]) 76 | when ?in_range(ID, Min, Max) -> 77 | NewMembers = ordsets:del_element({ID, IP, Port}, Members), 78 | [{Min, Max, NewMembers}|T]; 79 | delete(ID, IP, Port, [H|T]) -> 80 | [H|delete(ID, IP, Port, T)]. 81 | 82 | dump(Buckets) -> 83 | {ok, FP} = file:open("buckets.txt", [write]), 84 | io:format(FP, "~p buckets~n", [length(Buckets)]), 85 | dump_bucket(FP, Buckets), 86 | file:close(FP). 87 | 88 | dump_bucket(FP, [{Min, Max, Members}|T]) -> 89 | io:format(FP, "~nBucket [~s, ~s) ~p nodes~n", 90 | [dht_id:tohex(Min), dht_id:tohex(Max), length(Members)]), 91 | [io:format(FP, "~s ~p:~p~n", [dht_id:tohex(ID), IP, Port]) 92 | || {ID, IP, Port} <- Members], 93 | dump_bucket(FP, T); 94 | 95 | dump_bucket(_FP, []) -> 96 | ok. 97 | 98 | do_insert(Self, ID, IP, Port, [{Min, Max, Members}|T]) 99 | when ?in_range(ID, Min, Max), ?in_range(Self, Min, Max) -> 100 | NumMembers = length(Members), 101 | if 102 | NumMembers < ?K -> 103 | % ordsets will keep the element unique 104 | NewMembers = ordsets:add_element({ID, IP, Port}, Members), 105 | [{Min, Max, NewMembers}|T]; 106 | 107 | NumMembers == ?K, (Max - Min) > 2 -> 108 | Diff = Max - Min, 109 | Half = Max - (Diff div 2), 110 | Lower = [N || {MID, _, _}=N <- Members, ?in_range(MID, Min, Half)], 111 | Upper = [N || {MID, _, _}=N <- Members, ?in_range(MID, Half, Max)], 112 | WithSplit = [{Min, Half, Lower}, {Half, Max, Upper}|T], 113 | do_insert(Self, ID, IP, Port, WithSplit); 114 | 115 | NumMembers == ?K -> 116 | [{Min, Max, Members}|T] 117 | end; 118 | 119 | do_insert(_, ID, IP, Port, [{Min, Max, Members}|T]) 120 | when ?in_range(ID, Min, Max) -> 121 | NumMembers = length(Members), 122 | if 123 | NumMembers < ?K -> 124 | NewMembers = ordsets:add_element({ID, IP, Port}, Members), 125 | [{Min, Max, NewMembers}|T]; 126 | NumMembers == ?K -> 127 | [{Min, Max, Members}|T] 128 | end; 129 | 130 | do_insert(Self, ID, IP, Port, [H|T]) -> 131 | [H|do_insert(Self, ID, IP, Port, T)]. 132 | -------------------------------------------------------------------------------- /src/msg.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% msg.erl 3 | %% Kevin Lynx 4 | %% dht message(protocol) encode/decode 5 | %% 06.06.2013 6 | %% 7 | -module(msg). 8 | -compile(export_all). 9 | -export([encode/1, 10 | ping/2, 11 | res_ping/2, 12 | find_node/3, 13 | res_find_node/3, 14 | get_peer/3, 15 | decode_nodes/1, 16 | decode_peers/1, 17 | res_get_peer/5, 18 | announce_peer/5, 19 | res_announce_peer/2]). 20 | -define(ADAPTER(L), to_dict(L)). 21 | -define(IDSIZE, 20). 22 | -define(ADDRSIZE, 6). 23 | -define(NODESIZE, 26). 24 | 25 | encode({ping, Tid, MyID, _Arg}) -> 26 | ping(Tid, MyID); 27 | 28 | encode({find_node, Tid, MyID, {Target}}) -> 29 | find_node(Tid, MyID, Target); 30 | 31 | encode({get_peers, Tid, MyID, {InfoHash}}) -> 32 | get_peer(Tid, MyID, InfoHash); 33 | 34 | encode({announce_peer, Tid, MyID, {InfoHash, Token, Port}}) -> 35 | announce_peer(Tid, MyID, InfoHash, Token, Port). 36 | 37 | %% These functions below are used to adapter to `bencode' module 38 | convert_value(V) -> 39 | if 40 | is_atom(V) -> list_to_binary(atom_to_list(V)); 41 | is_list(V) -> list_to_binary(V); 42 | true -> V 43 | end. 44 | 45 | convert_pair({K, V}) -> 46 | {convert_value(K), convert_value(V)}. 47 | 48 | to_list(L) -> 49 | lists:map(fun({K, V}) -> convert_pair({K, V}) end, L). 50 | 51 | to_dict(L) -> 52 | {dict, dict:from_list(to_list(L))}. 53 | 54 | query(Tid, Type, Arg) -> 55 | ?ADAPTER([{t, Tid}, {y, q}, {q, Type}, {a, Arg}]). 56 | 57 | response(Tid, Arg) -> 58 | ?ADAPTER([{t, Tid}, {y, r}, {r, Arg}]). 59 | 60 | bjoin(List) -> 61 | lists:foldr(fun(A, B) -> <> end, <<>>, List). 62 | 63 | % ownid 64 | ping(Tid, Id) -> 65 | Arg = ?ADAPTER([{id, Id}]), 66 | bencode:encode(query(Tid, ping, Arg)). 67 | 68 | % ownid 69 | res_ping(Tid, Id) -> 70 | Arg = ?ADAPTER([{id, Id}]), 71 | bencode:encode(response(Tid, Arg)). 72 | 73 | % Id: ownid 74 | find_node(Tid, Id, Target) -> 75 | Arg = ?ADAPTER([{id, Id}, {target, Target}]), 76 | bencode:encode(query(Tid, find_node, Arg)). 77 | 78 | % encode_compact_addr({127, 0, 0, 1}, 1024) 79 | encode_compact_addr(IP, Port) when is_tuple(IP) -> 80 | {A1, A2, A3, A4} = IP, 81 | <>. 82 | 83 | decode_compact_addr(B) -> 84 | <> = B, 85 | {{A1, A2, A3, A4}, Port}. 86 | 87 | encode_compact_node({Id, IP, Port}) when is_binary(Id) -> 88 | bjoin([Id, encode_compact_addr(IP, Port)]); 89 | 90 | encode_compact_node({Id, IP, Port}) when is_integer(Id) -> 91 | BID = <>, 92 | encode_compact_node({BID, IP, Port}). 93 | 94 | % return as: {<>, {127, 0, 0, 1}, 1111} 95 | decode_compact_node(B) -> 96 | <> = B, 97 | {IP, Port} = decode_compact_addr(B1), 98 | {Node, IP, Port}. 99 | 100 | % [{Id, IP, Port}, {Id, IP, Port}] 101 | encode_nodes([Node|Rest]) -> 102 | bjoin([encode_compact_node(Node), encode_nodes(Rest)]); 103 | 104 | encode_nodes([]) -> 105 | <<>>. 106 | 107 | % return as: [{<>, {127, 0, 0, 1}, 1111},... 108 | decode_nodes(<>) -> 109 | [decode_compact_node(N)]++decode_nodes(R); 110 | 111 | decode_nodes(<<>>) -> 112 | []. 113 | 114 | % Id: ownid 115 | res_find_node(Tid, Id, Nodes) -> 116 | NodesStr = encode_nodes(Nodes), 117 | Arg = ?ADAPTER([{id, Id}, {nodes, NodesStr}]), 118 | bencode:encode(response(Tid, Arg)). 119 | 120 | get_peer(Tid, Id, Info) -> 121 | Arg = ?ADAPTER([{id, Id}, {info_hash, Info}]), 122 | bencode:encode(query(Tid, get_peers, Arg)). 123 | 124 | % encode_peers([{{127, 0, 0, 1}, 1111}, {{127, 0, 0, 1}, 1111}] 125 | encode_peers([Peer|Rest]) -> 126 | {IP, Port} = Peer, 127 | [encode_compact_addr(IP, Port)]++encode_peers(Rest); 128 | 129 | encode_peers([]) -> 130 | []. 131 | 132 | % return as: [{{127, 0, 0, 1}, 111},... 133 | decode_peers(<>) -> 134 | [decode_compact_addr(P)]++decode_peers(R); 135 | 136 | decode_peers(<<>>) -> 137 | []. 138 | 139 | % Id: ownid 140 | res_get_peer(Tid, Id, Token, peers, Peers) -> 141 | Arg = ?ADAPTER([{id, Id}, {token, Token}, {values, encode_peers(Peers)}]), 142 | bencode:encode(response(Tid, Arg)); 143 | 144 | res_get_peer(Tid, Id, Token, nodes, Nodes) -> 145 | Arg = ?ADAPTER([{id, Id}, {token, Token}, {nodes, encode_nodes(Nodes)}]), 146 | bencode:encode(response(Tid, Arg)). 147 | 148 | announce_peer(Tid, Id, Info, Token, Port) -> 149 | Arg = ?ADAPTER([{id, Id}, {token, Token}, {info_hash, Info}, {port, Port}]), 150 | bencode:encode(query(Tid, announce_peer, Arg)). 151 | 152 | res_announce_peer(Tid, Id) -> 153 | Arg = ?ADAPTER([{id, Id}]), 154 | bencode:encode(response(Tid, Arg)). 155 | 156 | -------------------------------------------------------------------------------- /src/search.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% search.erl 3 | %% copied/modified from etorrent dht implementation 4 | %% Kevin Lynx 5 | %% 06.07.2013 6 | %% 7 | -module(search). 8 | -include("vlog.hrl"). 9 | -export([find_node/2, 10 | get_peers/2]). 11 | 12 | find_node(MyID, NodeID) -> 13 | Width = search_width(), 14 | Retry = search_retries(), 15 | dht_iter_search(MyID, find_node, NodeID, Width, Retry). 16 | 17 | % {Trackers, Peers, AliveNodes} 18 | % `Trackers' are nodes which track the infohash 19 | % `Peers' are peers which are downloading the infohash 20 | get_peers(MyID, InfoHash) -> 21 | Width = search_width(), 22 | Retry = search_retries(), 23 | dht_iter_search(MyID, get_peers, InfoHash, Width, Retry). 24 | 25 | search_width() -> 26 | 32. 27 | 28 | search_retries() -> 29 | 4. 30 | 31 | dht_iter_search(MyID, SearchType, Target, Width, Retry) -> 32 | ?T(?FMT("start a ~p search to ~s", [SearchType, dht_id:tohex(Target)])), 33 | % query these nodes in the next iteration 34 | Next = dht_state:closest(MyID, Target, Width), 35 | WithDist = [{dht_id:distance(ID, Target), ID, IP, Port} || {ID, IP, Port} <- Next], 36 | % nodes which have been queried already 37 | Queried = gb_sets:empty(), 38 | % responsed nodes are alive 39 | Alive = gb_sets:empty(), 40 | dht_iter_search(MyID, SearchType, Target, Width, Retry, 0, WithDist, Queried, Alive, []). 41 | 42 | % reach the max retries, stop the search 43 | dht_iter_search(_MyID, SearchType, _, _, Retry, Retry, _, _, Alive, WithPeers) -> 44 | TmpAlive = gb_sets:to_list(Alive), 45 | AliveList = [{ID, IP, Port} || {_, ID, IP, Port} <- TmpAlive], 46 | case SearchType of 47 | find_node -> 48 | AliveList; 49 | get_peers -> 50 | Trackers = [{ID, IP, Port, Token} || {ID, IP, Port, Token, _} <- WithPeers], 51 | Peers = [Peers || {_, _, _, _, Peers} <- WithPeers], 52 | {Trackers, Peers, AliveList} 53 | end; 54 | 55 | % `WithPeers' are received peers in this search 56 | dht_iter_search(MyID, SearchType, Target, Width, Retry, Retries, 57 | Next, Queried, Alive, WithPeers) -> 58 | ?T(?FMT("ready to query ~p nodes in this query", [length(Next)])), 59 | % the next nodes will be queried in this iteration, mark them 60 | AddQueried = [{ID, IP, Port} || {_, ID, IP, Port} <- Next], 61 | % the all queried nodes in this search 62 | NewQueried = gb_sets:union(Queried, gb_sets:from_list(AddQueried)), 63 | % Query all nodes in the queue and generate a list of 64 | % {Dist, ID, IP, Port, Nodes} elements 65 | SearchCalls = [case SearchType of 66 | find_node -> {dht_net, find_node, [MyID, IP, Port, Target]}; 67 | get_peers -> {dht_net, get_peers, [MyID, IP, Port, Target]} 68 | end || {_, _, IP, Port} <- Next], 69 | % call dht_net functions, send queries to remote node and 70 | % wait here until the node responsed 71 | ReturnValues = rpc:parallel_eval(SearchCalls), 72 | WithArgs = lists:zip(Next, ReturnValues), 73 | 74 | FailedCall = make_ref(), 75 | % filter successful query, a query got responsed 76 | TmpSuccessful = [case {repack, SearchType, RetVal} of 77 | {repack, _, {error, timeout}} -> 78 | FailedCall; 79 | {repack, find_node, {NID, Nodes}} -> 80 | {{Dist, NID, IP, Port}, Nodes}; 81 | {repack, get_peers, {NID, Token, Peers, Nodes}} -> 82 | {{Dist, NID, IP, Port}, {Token, Peers, Nodes}}; 83 | {repack, _, Error} -> 84 | ?W(?FMT("search error ~p", [Error])), 85 | FailedCall % process terminate etc... 86 | end || {{Dist, _ID, IP, Port}, RetVal} <- WithArgs], 87 | % and now `Successful' contains valid values from `find_node' or `get_peers' 88 | Successful = [E || E <- TmpSuccessful, E =/= FailedCall], 89 | % mark all nodes that responded as alive 90 | AddAlive = [N || {{_, _, _, _}=N, _} <- Successful], 91 | % all alive nodes in this search 92 | NewAlive = gb_sets:union(Alive, gb_sets:from_list(AddAlive)), 93 | 94 | % Accumulate all nodes from the successful responses. 95 | % Calculate the relative distance to all of these nodes 96 | % and keep the closest nodes which has not already been 97 | % queried in a previous iteration 98 | NodeLists = [case {acc_nodes, {SearchType, Res}} of 99 | {acc_nodes, {find_node, Nodes}} -> 100 | Nodes; 101 | {acc_nodes, {get_peers, {_, _, Nodes}}} -> 102 | Nodes 103 | end || {_, Res} <- Successful], 104 | AllNodes = lists:flatten(NodeLists), 105 | NewNodes = [Node || Node <- AllNodes, not gb_sets:is_member(Node, NewQueried)], 106 | % the next queried nodes are the closest nodes in the responsed nodes replied 107 | % from node queried in this search iteration. 108 | NewNext = [{dht_id:distance(ID, Target), ID, IP, Port} 109 | ||{ID, IP, Port} <- bucket:closest_to(Target, NewNodes, Width)], 110 | 111 | % Check if the closest node in the work queue is closer 112 | % to the target than the closest responsive node that was 113 | % found in this iteration. 114 | MinAliveDist = case gb_sets:size(NewAlive) of 115 | 0 -> 116 | infinity; 117 | _ -> 118 | {IMinAliveDist, _, _, _} = gb_sets:smallest(NewAlive), 119 | IMinAliveDist 120 | end, 121 | MinQueueDist = case NewNext of 122 | [] -> 123 | infinity; 124 | Other -> 125 | {MinDist, _, _, _} = lists:min(Other), 126 | MinDist 127 | end, 128 | % Check if the closest node in the work queue is closer 129 | % to the infohash than the closest responsive node. 130 | NewRetries = if 131 | (MinQueueDist < MinAliveDist) -> 0; 132 | (MinQueueDist >= MinAliveDist) -> Retries + 1 133 | end, 134 | % Accumulate the trackers and peers found if this is a get_peers search. 135 | NewWithPeers = case SearchType of 136 | find_node -> [] = WithPeers; 137 | get_peers -> 138 | Tmp = [{ID, IP, Port, Token, Peers} 139 | || {{_, ID, IP, Port}, {Token, Peers, _}} <- Successful, Peers > []], 140 | ?T(?FMT("got ~p peers in this iteation", [length(Tmp)])), 141 | WithPeers ++ Tmp 142 | end, 143 | dht_iter_search(MyID, SearchType, Target, Width, Retry, NewRetries, 144 | NewNext, NewQueried, NewAlive, NewWithPeers). 145 | -------------------------------------------------------------------------------- /src/dht_state.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% dht_state.erl 3 | %% Kevin Lynx 4 | %% 06.06.2013 5 | %% 6 | -module(dht_state). 7 | -behaviour(gen_server). 8 | -include("vlog.hrl"). 9 | -export([init/1, 10 | handle_call/3, 11 | handle_cast/2, 12 | handle_info/2, 13 | terminate/2, 14 | code_change/3]). 15 | -export([start_link/3, 16 | start_link/4, 17 | insert/4, 18 | stop/1, 19 | load/1, 20 | save/1, 21 | notify_event/3, 22 | dump/1, 23 | active_node/4, 24 | closest/3]). 25 | % private callbacks 26 | -export([keep_node_alive/4, 27 | join/3, 28 | do_startup_insert/2, 29 | find_self/1]). 30 | -record(state, {savefile, ownid, mod, buckets, savetimer}). 31 | -include("conf.hrl"). 32 | -define(NODE_ID(ID, IP, Port), {node, ID, IP, Port}). 33 | 34 | closest(MyID, Target, Num) -> 35 | gen_server:call(srv_name(MyID), {closest, Target, Num}). 36 | 37 | insert(MyID, ID, IP, Port) -> 38 | gen_server:cast(srv_name(MyID), {insert, ID, IP, Port}). 39 | 40 | % mark the active time for a node 41 | active_node(MyID, ID, IP, Port) -> 42 | Key = ?NODE_ID(ID, IP, Port), 43 | case timer_monitor:get(MyID, Key) of 44 | error -> 45 | ?W(?FMT("timer monitor node not exist ~s", [dht_id:tohex(ID)])); 46 | _ -> 47 | timer_monitor:active(MyID, Key) 48 | end. 49 | 50 | notify_event(MyID, Event, Args) -> 51 | gen_server:cast(srv_name(MyID), {event, Event, Args}). 52 | 53 | dump(MyID) -> 54 | gen_server:cast(srv_name(MyID), dump). 55 | 56 | start_link(MyID, Filename, Mod) -> 57 | start_link(MyID, [], Filename, Mod). 58 | 59 | start_link(MyID, NodeAddrs, Filename, Mod) -> 60 | gen_server:start_link({local, srv_name(MyID)}, ?MODULE, [MyID, NodeAddrs, Filename, Mod], []). 61 | 62 | stop(MyID) -> 63 | gen_server:cast(srv_name(MyID), stop). 64 | 65 | save(MyID) -> 66 | gen_server:cast(srv_name(MyID), save). 67 | 68 | srv_name(MyID) -> 69 | list_to_atom("dht_state" ++ dht_id:tohex(MyID)). 70 | 71 | init([MyID, NodeAddrs, Filename, Mod]) -> 72 | Buckets = bucket:new(), 73 | startup_insert_nodes(MyID, NodeAddrs), 74 | {ok, TRef} = timer:send_interval(?SAVE_INTERVAL, {interval_save}), 75 | {ok, #state{ownid = MyID, savefile = Filename, 76 | savetimer = TRef, mod = Mod, buckets = Buckets}}. 77 | 78 | handle_cast(save, State) -> 79 | save_state(State), 80 | {noreply, State}; 81 | 82 | handle_cast(dump, State) -> 83 | #state{buckets = Buckets} = State, 84 | bucket:dump(Buckets), 85 | {noreply, State}; 86 | 87 | handle_cast({event, Event, Args}, State) -> 88 | #state{mod = M} = State, 89 | M:handle_event(Event, Args), 90 | {noreply, State}; 91 | 92 | handle_cast({insert, ID, IP, Port}, State) -> 93 | #state{ownid = MyID, buckets = Buckets} = State, 94 | Exist = bucket:is_member(ID, IP, Port, Buckets), 95 | NewBuckets = bucket:insert(MyID, ID, IP, Port, Buckets), 96 | % TODO: it's a CPU waste 97 | InsertOk = bucket:is_member(ID, IP, Port, NewBuckets), 98 | case InsertOk of 99 | true -> monitor_node(MyID, ID, IP, Port, Exist); 100 | false -> ok 101 | end, 102 | ?T(?FMT("inserted a node, bucket size ~p", [length(NewBuckets)])), 103 | case length(NewBuckets) > 160 of 104 | true -> ?E(?FMT("fatal error, bucket size ~p > 160", [length(NewBuckets)])); 105 | false -> ok 106 | end, 107 | {noreply, State#state{buckets = NewBuckets}}; 108 | 109 | handle_cast(stop, State) -> 110 | {stop, normal, State}; 111 | 112 | handle_cast(_, State) -> 113 | {noreply, State}. 114 | 115 | terminate(_, State) -> 116 | #state{savetimer = TRef} = State, 117 | timer:cancel(TRef), 118 | save_state(State), 119 | {ok, State}. 120 | 121 | code_change(_, _, State) -> 122 | {ok, State}. 123 | 124 | handle_call({get_ownid}, _From, State) -> 125 | #state{ownid = MyID} = State, 126 | {reply, MyID, State}; 127 | 128 | handle_call({closest, Target, Num}, _From, State) -> 129 | #state{buckets = Buckets} = State, 130 | Closest = bucket:closest(Target, Buckets, Num), 131 | {reply, Closest, State}; 132 | 133 | handle_call({delete, ID, IP, Port}, _From, State) -> 134 | #state{ownid = MyID, buckets = Buckets} = State, 135 | NewBuckets = bucket:delete(ID, IP, Port, Buckets), 136 | stop_monitor_node(MyID, ID, IP, Port), 137 | ?T(?FMT("delete a node ~s", [dht_id:tohex(ID)])), 138 | {reply, ok, State#state{buckets = NewBuckets}}; 139 | 140 | handle_call(_, _From, State) -> 141 | {reply, not_implemented, State}. 142 | 143 | handle_info({interval_save}, State) -> 144 | save_state(State), 145 | {noreply, State}; 146 | 147 | handle_info({node_timeout, ID, IP, Port}, State) -> 148 | #state{ownid = MyID} = State, 149 | ?I(?FMT("node timeout ~s", [dht_id:tohex(ID)])), 150 | spawn_link(?MODULE, keep_node_alive, [MyID, ID, IP, Port]), 151 | {noreply, State}; 152 | 153 | handle_info(_, State) -> 154 | {noreply, State}. 155 | 156 | % TODO: monitor bucket 157 | monitor_node(MyID, ID, IP, Port, false) -> 158 | Msg = {node_timeout, ID, IP, Port}, 159 | Key = ?NODE_ID(ID, IP, Port), 160 | ?T(?FMT("timer monitor a new node ~s", [dht_id:tohex(ID)])), 161 | case timer_monitor:get(MyID, Key) of 162 | error -> 163 | timer_monitor:add(MyID, Key, ?NODE_TIMEOUT, Msg); 164 | _ -> 165 | ?W(?FMT("timer monitor on node ~s exist", [dht_id:tohex(ID)])) 166 | end; 167 | 168 | monitor_node(MyID, ID, IP, Port, true) -> 169 | Key = ?NODE_ID(ID, IP, Port), 170 | ?T(?FMT("active a node ~s", [dht_id:tohex(ID)])), 171 | timer_monitor:active(MyID, Key). 172 | 173 | stop_monitor_node(MyID, ID, IP, Port) -> 174 | Key = ?NODE_ID(ID, IP, Port), 175 | ?T(?FMT("stop a node ~s timer monitor", [dht_id:tohex(ID)])), 176 | timer_monitor:remove(MyID, Key). 177 | 178 | keep_node_alive(MyID, ID, IP, Port) -> 179 | ping_node(MyID, ID, IP, Port, 3). 180 | 181 | ping_node(MyID, ID, IP, Port, Count) when Count > 0 -> 182 | % dht_net:ping will update the node active time if the query get response 183 | case dht_net:ping(MyID, IP, Port) of 184 | timeout -> 185 | ping_node(MyID, ID, IP, Port, Count - 1); 186 | _ -> ok 187 | end; 188 | 189 | ping_node(MyID, ID, IP, Port, 0) -> 190 | ?I(?FMT("node keepalive failed, inactive the node ~s", [dht_id:tohex(ID)])), 191 | % TODO: keep the node in the bucket until another node arrives 192 | % If we delete the node, the bucket will be empty 193 | gen_server:call(srv_name(MyID), {delete, ID, IP, Port}), 194 | ok. 195 | 196 | save_state(State) -> 197 | #state{ownid = MyID, savefile = Filename, buckets = Buckets} = State, 198 | WithID = bucket:all(Buckets), 199 | Addrs = [{IP, Port} || {_ID, IP, Port} <- WithID], 200 | save(Filename, MyID, Addrs). 201 | 202 | save(Filename, Self, NodeList) -> 203 | PersistentState = [{node_id, Self}, {node_set, NodeList}], 204 | file:write_file(Filename, term_to_binary(PersistentState)). 205 | 206 | load(Filename) -> 207 | case file:read_file(Filename) of 208 | {ok, BinState} -> 209 | PersistentState = binary_to_term(BinState), 210 | {value, {_, Self}} = lists:keysearch(node_id, 1, PersistentState), 211 | {value, {_, Nodes}} = lists:keysearch(node_set, 1, PersistentState), 212 | ?I(?FMT("load state from ~s success", [Filename])), 213 | ?I(?FMT("self: ~s, nodes count: ~p", [dht_id:tohex(Self), length(Nodes)])), 214 | {Self, Nodes}; 215 | {error, Reason} -> 216 | ?I(?FMT("load state from ~s failed ~p", [Filename, Reason])), 217 | {error, Reason} 218 | end. 219 | 220 | startup_insert_nodes(MyID, []) -> 221 | {ok, IP} = inet:getaddr("dht.transmissionbt.com", inet), 222 | Port = 6881, 223 | ?I(?FMT("initial startup, try to join 1st node ~p:~p", [IP, Port])), 224 | spawn_link(?MODULE, join, [MyID, IP, Port]); 225 | 226 | startup_insert_nodes(MyID, Addrs) -> 227 | spawn_link(?MODULE, do_startup_insert, [MyID, Addrs]). 228 | 229 | do_startup_insert(MyID, Addrs) -> 230 | PingRets = [dht_net:ping(MyID, IP, Port) || {IP, Port} <- Addrs], 231 | case [Ret || Ret <- PingRets, Ret =/= timeout] of 232 | [] -> 233 | ?W(?FMT("not even insert one node ~p", [length(Addrs)])), 234 | startup_insert_nodes(MyID, []); 235 | _ -> 236 | notify_event(MyID, startup, {MyID}) 237 | end. 238 | 239 | join(MyID, IP, Port) -> 240 | case dht_net:ping(MyID, IP, Port) of 241 | timeout -> 242 | join(MyID, IP, Port); 243 | _ -> 244 | ?I(?FMT("join to ~p:~p success", [IP, Port])), 245 | spawn_link(?MODULE, find_self, [MyID]) 246 | end. 247 | 248 | find_self(MyID) -> 249 | search:find_node(MyID, MyID), 250 | notify_event(MyID, startup, {MyID}). 251 | 252 | 253 | -------------------------------------------------------------------------------- /src/dht_net.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% dht_net.erl 3 | %% Kevin Lynx 4 | %% 06.06.2013 5 | %% 6 | -module(dht_net). 7 | -behaviour(gen_server). 8 | -export([init/1, 9 | handle_info/2, 10 | handle_cast/2, 11 | handle_call/3, 12 | code_change/3, 13 | terminate/2]). 14 | -export([start_link/2, 15 | ping/3, 16 | find_node/4, 17 | get_peers/4, 18 | announce_peer/5, 19 | stop/1]). 20 | -include("vlog.hrl"). 21 | -include("conf.hrl"). 22 | -record(state, {ownid, sock, sents, port, tidseed = 0}). 23 | 24 | start_link(MyID, Port) -> 25 | gen_server:start_link({local, srv_name(MyID)}, ?MODULE, [MyID, Port], []). 26 | 27 | stop(MyID) -> 28 | gen_server:cast(srv_name(MyID), stop). 29 | 30 | ping(MyID, IP, Port) -> 31 | Query = {query, ping, IP, Port, {}}, 32 | case gen_server:call(srv_name(MyID), Query) of 33 | timeout -> timeout; 34 | Values -> 35 | ID = decode_response(ping, Values), 36 | dht_state:active_node(MyID, ID, IP, Port), 37 | ID 38 | end. 39 | 40 | find_node(MyID, IP, Port, Target) -> 41 | Query = {query, find_node, IP, Port, {dht_id:list_id(Target)}}, 42 | case gen_server:call(srv_name(MyID), Query) of 43 | timeout -> {error, timeout}; 44 | Values -> 45 | {ResID, Nodes} = decode_response(find_node, Values), 46 | dht_state:active_node(MyID, ResID, IP, Port), 47 | {ResID, Nodes} 48 | end. 49 | 50 | get_peers(MyID, IP, Port, Target) -> 51 | Query = {query, get_peers, IP, Port, {dht_id:list_id(Target)}}, 52 | case gen_server:call(srv_name(MyID), Query) of 53 | timeout -> {error, timeout}; 54 | Values -> 55 | {ResID, Token, Peers, Nodes} = decode_response(get_peers, Values), 56 | dht_state:active_node(MyID, ResID, IP, Port), 57 | {ResID, Token, Peers, Nodes} 58 | end. 59 | 60 | announce_peer(MyID, IP, Port, InfoHash, BTPort) -> 61 | Query = {query, announce, IP, Port, {dht_id:list_id(InfoHash), BTPort}}, 62 | case gen_server:call(srv_name(MyID), Query) of 63 | timeout -> {error, timeout}; 64 | Values -> 65 | ResID = decode_response(announce_peer, Values), 66 | dht_state:active_node(MyID, ResID, IP, Port), 67 | ResID 68 | end. 69 | 70 | srv_name(MyID) -> 71 | list_to_atom("dht_net" ++ dht_id:tohex(MyID)). 72 | 73 | decode_response(ping, Values) -> 74 | {ok, ID} = dict:find(<<"id">>, Values), 75 | dht_id:integer_id(ID); 76 | 77 | decode_response(find_node, Values) -> 78 | {ok, ResID} = dict:find(<<"id">>, Values), 79 | Nodes = case dict:find(<<"nodes">>, Values) of 80 | {ok, NodesBuf} -> 81 | decode_compact_nodes(NodesBuf); 82 | _ -> [] 83 | end, 84 | ?T(?FMT("~s response find_node ~p nodes", [dht_id:tohex(ResID), length(Nodes)])), 85 | {dht_id:integer_id(ResID), Nodes}; 86 | 87 | decode_response(get_peers, Values) -> 88 | {ok, ResID} = dict:find(<<"id">>, Values), 89 | IntID = dht_id:integer_id(ResID), 90 | Token = case dict:find(<<"token">>, Values) of 91 | {ok, T} -> T; 92 | _ -> 93 | ?W(?FMT("get_peers response not found token ~s", [dht_id:tohex(ResID)])), 94 | <<0:32>> 95 | end, 96 | case dict:find(<<"values">>, Values) of 97 | {ok, {list, [PeersBuf]}} -> 98 | Peers = msg:decode_peers(PeersBuf), 99 | ?T(?FMT("~s response values with ~p peers", [dht_id:tohex(IntID), length(Peers)])), 100 | {IntID, Token, Peers, []}; 101 | _ -> 102 | Nodes = decode_compact_nodes(Values), 103 | ?T(?FMT("~s response ~p nodes", [dht_id:tohex(IntID), length(Nodes)])), 104 | {IntID, Token, [], Nodes} 105 | end; 106 | 107 | decode_response(announce_peer, Values) -> 108 | {ok, ResID} = dict:find(<<"id">>, Values), 109 | dht_id:integer_id(ResID). 110 | 111 | decode_compact_nodes(Values) -> 112 | case dict:find(<<"nodes">>, Values) of 113 | {ok, NodesBuf} -> 114 | Nodes = msg:decode_nodes(NodesBuf), 115 | [{dht_id:integer_id(ID), IP, Port} || {ID, IP, Port} <- Nodes]; 116 | _ -> [] 117 | end. 118 | 119 | init([MyID, Port]) -> 120 | ?I(?FMT("dht_net start at port ~p", [Port])), 121 | {ok, Sock} = gen_udp:open(Port, [binary, {active, once}]), 122 | {ok, #state{ownid = MyID, sock = Sock, port = Port, sents = gb_trees:empty()}}. 123 | 124 | handle_info({udp, Socket, IP, Port, RawData}, State) -> 125 | NewState = handle_msg(Socket, IP, Port, RawData, State), 126 | inet:setopts(Socket, [{active,once}]), 127 | {noreply, NewState}; 128 | 129 | handle_info({timeout, _, TidNum}, State) -> 130 | #state{sents = Sents} = State, 131 | NewSents = response_timeout(TidNum, Sents), 132 | {noreply, State#state{sents = NewSents}}; 133 | 134 | handle_info(_Msg, _S) -> 135 | {noreply, _S}. 136 | 137 | handle_cast(stop, State) -> 138 | {stop, normal, State}; 139 | 140 | handle_cast(_, State) -> 141 | {noreply, State}. 142 | 143 | terminate(_, State) -> 144 | {ok, State}. 145 | 146 | code_change(_, _, State) -> 147 | {ok, State}. 148 | 149 | handle_call({query, Type, {127, 0, 0, 1}, _SelfPort, _Arg}, _From, #state{port = _SelfPort} = State) -> 150 | ?E(?FMT("send query ~p to self", [Type])), 151 | {reply, timeout, State}; 152 | 153 | handle_call({query, Type, IP, Port, Arg}, From, State) -> 154 | #state{ownid = MyID, tidseed = TidNum, sock = Sock, sents = Sents} = State, 155 | Tid = make_tid(Type, TidNum), 156 | Msg = msg:encode({Type, Tid, dht_id:list_id(MyID), Arg}), 157 | case gen_udp:send(Sock, IP, Port, Msg) of 158 | ok -> 159 | ?T(?FMT("send query ~p to ~p:~p", [Type, IP, Port])), 160 | NewSents = add_query_process(TidNum, From, Sents), 161 | NewState = State#state{tidseed = TidNum + 1, sents = NewSents}, 162 | {noreply, NewState}; 163 | {error, Reason} -> 164 | ?E(?FMT("udp send error: ~p", [Reason])), 165 | {reply, timeout, State} 166 | end; 167 | 168 | handle_call(_, _From, State) -> 169 | {reply, not_implemented, State}. 170 | 171 | handle_msg(Socket, IP, Port, Data, State) -> 172 | #state{ownid = MyID, sents = Sents} = State, 173 | case (catch parse_message(Data)) of 174 | {'EXIT', Rea} -> 175 | ?W(?FMT("parse message failed ~p:~p ~p", [IP, Port, Rea])), 176 | State; 177 | {error, Tid, Err} -> 178 | ?W(?FMT("received an error ~p ~p from ~p:~p", [Tid, Err, IP, Port])), 179 | NewSents = response_timeout(Tid, Sents), 180 | State#state{sents = NewSents}; 181 | {response, Tid, Args} -> 182 | ?T(?FMT("received a response ~p", [Tid])), 183 | new_node(MyID, response, Args, IP, Port), 184 | NewSents = response_ok(Tid, Args, Sents), 185 | State#state{sents = NewSents}; 186 | {Query, Tid, Args} -> 187 | ?T(?FMT("received a query ~p from ~p:~p", [Query, IP, Port])), 188 | new_node(MyID, query, Args, IP, Port), 189 | MyListID = dht_id:list_id(MyID), 190 | case handle_query(Query, MyListID, Tid, Args, IP, Port) of 191 | {ok, Msg} -> 192 | gen_udp:send(Socket, IP, Port, Msg); 193 | F -> 194 | ?W(?FMT("failed with response ~p", [F])) 195 | end, 196 | State 197 | end. 198 | 199 | handle_query(ping, MyID, Tid, _Args, _IP, _Port) -> 200 | {ok, msg:res_ping(Tid, MyID)}; 201 | 202 | handle_query(find_node, MyID, Tid, Args, _IP, _Port) -> 203 | {ok, Target} = dict:find(<<"id">>, Args), 204 | ?T(?FMT("recv find_node ~s", [dht_id:tohex(Target)])), 205 | TID = dht_id:integer_id(Target), 206 | Closest = dht_state:closest(MyID, TID, 8), 207 | Msg = msg:res_find_node(Tid, MyID, Closest), 208 | {ok, Msg}; 209 | 210 | handle_query(get_peers, MyID, Tid, Args, IP, Port) -> 211 | {ok, InfoHash} = dict:find(<<"info_hash">>, Args), 212 | ?T(?FMT("recv get_peers ~s", [dht_id:tohex(InfoHash)])), 213 | Token = token_value(IP, Port), 214 | TID = dht_id:integer_id(InfoHash), 215 | dht_state:notify_event(MyID, get_peers, {InfoHash, IP, Port}), 216 | case storage:query(MyID, TID) of 217 | [] -> 218 | Closest = dht_state:closest(MyID, TID, 8), 219 | ?T(?FMT("response get_peers with ~p nodes", [length(Closest)])), 220 | {ok, msg:res_get_peer(Tid, MyID, Token, nodes, Closest)}; 221 | Peers -> 222 | ?T(?FMT("response get_peers with ~p peers", [length(Peers)])), 223 | {ok, msg:res_get_peer(Tid, MyID, Token, peers, Peers)} 224 | end; 225 | 226 | handle_query(announce_peer, MyID, Tid, Args, IP, Port) -> 227 | {ok, InfoHash} = dict:find(<<"info_hash">>, Args), 228 | {ok, BTPort} = dict:find(<<"port">>, Args), 229 | {ok, Token} = dict:find(<<"token">>, Args), 230 | ?T(?FMT("recv announce_peer ~s, ~p", [dht_id:tohex(InfoHash), BTPort])), 231 | case token_match(IP, Port, Token) of 232 | false -> 233 | % TODO: response a token error 234 | ?W("token not match"), 235 | {error, token_not_match}; 236 | true -> 237 | dht_state:notify_event(MyID, announce_peer, {InfoHash, IP, BTPort}), 238 | storage:insert(MyID, dht_id:integer_id(InfoHash), IP, BTPort), 239 | {ok, msg:res_announce_peer(Tid, MyID)} 240 | end. 241 | 242 | new_node(MyID, Reason, ArgDict, IP, Port) -> 243 | {ok, ID} = dict:find(<<"id">>, ArgDict), 244 | ?T(?FMT("insert a node ~s ~p:~p by ~p", [dht_id:tohex(ID), IP, Port, Reason])), 245 | dht_state:insert(MyID, dht_id:integer_id(ID), IP, Port). 246 | 247 | parse_message(Data) -> 248 | {ok, {dict, Dict}} = bencode:decode(Data), 249 | {ok, Tid} = dict:find(<<"t">>, Dict), 250 | case dict:find(<<"y">>, Dict) of 251 | {ok, <<"q">>} -> 252 | {ok, QS} = dict:find(<<"q">>, Dict), 253 | Type = query_type(QS), 254 | {ok, {dict, Args}} = dict:find(<<"a">>, Dict), 255 | assert_query_args(Type, Args), 256 | assert_args(Args), 257 | {Type, Tid, Args}; 258 | {ok, <<"r">>} -> 259 | {ok, {dict, Args}} = dict:find(<<"r">>, Dict), 260 | assert_args(Args), 261 | {response, Tid, Args}; 262 | {ok, <<"e">>} -> 263 | {ok, {list, Err}} = dict:find(<<"e">>, Dict), 264 | {error, Tid, Err} 265 | end. 266 | 267 | assert_query_args(Type, Args) when Type == get_peers; Type == announce_peer -> 268 | {ok, InfoHash} = dict:find(<<"info_hash">>, Args), 269 | 20 = byte_size(InfoHash); 270 | 271 | assert_query_args(_Type, _Args) -> 272 | ok. 273 | 274 | assert_args(Args) -> 275 | {ok, ID} = dict:find(<<"id">>, Args), 276 | 20 = byte_size(ID), 277 | true = dht_id:is_valid(ID). 278 | 279 | % return new sents 280 | response_ok(<<_, _, TidNum:16>> = _, Values, Sents) -> 281 | case find_query_process(TidNum, Sents) of 282 | undefined -> 283 | ?W(?FMT("response to a non-exist query ~p", [TidNum])), 284 | Sents; 285 | From -> 286 | gen_server:reply(From, Values), 287 | remove_query_process(TidNum, Sents) 288 | end; 289 | 290 | % an invalid remote node will reply invalid tid, and the timer 291 | % will remove the query 292 | response_ok(Tid, _Values, Sents) -> 293 | ?W(?FMT("invalid tid in response ~p", [Tid])), 294 | Sents. 295 | 296 | response_timeout(<<_, _, TidNum:16>> = _, Sents) -> 297 | response_timeout(TidNum, Sents); 298 | 299 | response_timeout(TidNum, Sents) -> 300 | case find_query_process(TidNum, Sents) of 301 | undefined -> Sents; 302 | From -> 303 | gen_server:reply(From, timeout), 304 | ?T(?FMT("query ~p timeout", [TidNum])), 305 | remove_query_process(TidNum, Sents) 306 | end. 307 | 308 | add_query_process(TidNum, From, Sents) -> 309 | ?T(?FMT("add query ~p", [TidNum])), 310 | Msg = {timeout, self(), TidNum}, 311 | TRef = erlang:send_after(?QUERY_TIMEOUT, self(), Msg), 312 | gb_trees:insert(TidNum, {From, TRef}, Sents). 313 | 314 | find_query_process(TidNum, Sents) -> 315 | case gb_trees:is_defined(TidNum, Sents) of 316 | true -> 317 | {From, _} = gb_trees:get(TidNum, Sents), 318 | From; 319 | false -> 320 | undefined 321 | end. 322 | 323 | remove_query_process(TidNum, Sents) -> 324 | ?T(?FMT("remove query ~p", [TidNum])), 325 | {_, TRef} = gb_trees:get(TidNum, Sents), 326 | erlang:cancel_timer(TRef), 327 | gb_trees:delete(TidNum, Sents). 328 | 329 | make_tid(Type, TidNum) -> 330 | D = case Type of 331 | ping -> <<"pn">>; 332 | find_node -> <<"fn">>; 333 | get_peers -> <<"gp">>; 334 | announce_peer -> <<"an">> 335 | end, 336 | <>. 337 | 338 | query_type(<<"ping">>) -> ping; 339 | query_type(<<"find_node">>) -> find_node; 340 | query_type(<<"get_peers">>) -> get_peers; 341 | query_type(<<"announce_peer">>) -> announce_peer. 342 | 343 | %% TODO: implement token 344 | token_value(_IP, _Port) -> 345 | list_to_binary([0 || _ <- lists:seq(1, 32)]). 346 | 347 | token_match(_IP, _Port, _Token) -> 348 | true. 349 | --------------------------------------------------------------------------------