|
@@ -11,7 +11,6 @@
|
|
|
%% Developer of the eMQTT Code is <ery.lee@gmail.com>
|
|
%% Developer of the eMQTT Code is <ery.lee@gmail.com>
|
|
|
%% Copyright (c) 2012 Ery Lee. All rights reserved.
|
|
%% Copyright (c) 2012 Ery Lee. All rights reserved.
|
|
|
%%
|
|
%%
|
|
|
-
|
|
|
|
|
-module(emqtt_router).
|
|
-module(emqtt_router).
|
|
|
|
|
|
|
|
-include("emqtt.hrl").
|
|
-include("emqtt.hrl").
|
|
@@ -22,11 +21,12 @@
|
|
|
|
|
|
|
|
-export([start_link/0]).
|
|
-export([start_link/0]).
|
|
|
|
|
|
|
|
--export([topics/1,
|
|
|
|
|
|
|
+-export([topics/0,
|
|
|
subscribe/2,
|
|
subscribe/2,
|
|
|
unsubscribe/2,
|
|
unsubscribe/2,
|
|
|
publish/2,
|
|
publish/2,
|
|
|
route/2,
|
|
route/2,
|
|
|
|
|
+ match/1,
|
|
|
down/1]).
|
|
down/1]).
|
|
|
|
|
|
|
|
-behaviour(gen_server).
|
|
-behaviour(gen_server).
|
|
@@ -43,20 +43,17 @@
|
|
|
start_link() ->
|
|
start_link() ->
|
|
|
gen_server2:start_link({local, ?MODULE}, ?MODULE, [], []).
|
|
gen_server2:start_link({local, ?MODULE}, ?MODULE, [], []).
|
|
|
|
|
|
|
|
-topics(direct) ->
|
|
|
|
|
- mnesia:dirty_all_keys(direct_topic);
|
|
|
|
|
-
|
|
|
|
|
-topics(wildcard) ->
|
|
|
|
|
- mnesia:dirty_all_keys(wildcard_topic).
|
|
|
|
|
|
|
+topics() ->
|
|
|
|
|
+ mnesia:dirty_all_keys(topic).
|
|
|
|
|
|
|
|
subscribe({Topic, Qos}, Client) when is_pid(Client) ->
|
|
subscribe({Topic, Qos}, Client) when is_pid(Client) ->
|
|
|
gen_server2:call(?MODULE, {subscribe, {Topic, Qos}, Client}).
|
|
gen_server2:call(?MODULE, {subscribe, {Topic, Qos}, Client}).
|
|
|
|
|
|
|
|
-unsubscribe(Topic, Client) when is_binary(Topic) and is_pid(Client) ->
|
|
|
|
|
|
|
+unsubscribe(Topic, Client) when is_list(Topic) and is_pid(Client) ->
|
|
|
gen_server2:cast(?MODULE, {unsubscribe, Topic, Client}).
|
|
gen_server2:cast(?MODULE, {unsubscribe, Topic, Client}).
|
|
|
|
|
|
|
|
%publish to cluster node.
|
|
%publish to cluster node.
|
|
|
-publish(Topic, Msg) when is_binary(Topic) and is_record(Msg, mqtt_msg) ->
|
|
|
|
|
|
|
+publish(Topic, Msg) when is_list(Topic) and is_record(Msg, mqtt_msg) ->
|
|
|
lists:foreach(fun(#topic{name=Name, node=Node}) ->
|
|
lists:foreach(fun(#topic{name=Name, node=Node}) ->
|
|
|
case Node == node() of
|
|
case Node == node() of
|
|
|
true -> route(Name, Msg);
|
|
true -> route(Name, Msg);
|
|
@@ -68,63 +65,54 @@ publish(Topic, Msg) when is_binary(Topic) and is_record(Msg, mqtt_msg) ->
|
|
|
route(Topic, Msg) ->
|
|
route(Topic, Msg) ->
|
|
|
[Client ! {route, Msg} || #subscriber{client=Client} <- ets:lookup(subscriber, Topic)].
|
|
[Client ! {route, Msg} || #subscriber{client=Client} <- ets:lookup(subscriber, Topic)].
|
|
|
|
|
|
|
|
-match(Topic) when is_binary(Topic) ->
|
|
|
|
|
- DirectMatches = mnesia:dirty_read(direct_topic, Topic),
|
|
|
|
|
- TopicWords = emqtt_topic:words(Topic),
|
|
|
|
|
- WildcardQuery = qlc:q([T || T = #topic{words=Words}
|
|
|
|
|
- <- mnesia:table(wildcard_topic),
|
|
|
|
|
- emqtt_topic:match(TopicWords, Words)]), %
|
|
|
|
|
-
|
|
|
|
|
- {atomic, WildcardMatches} = mnesia:transaction(fun() -> qlc:e(WildcardQuery) end),
|
|
|
|
|
- %mnesia:async_dirty(fun qlc:e/1, WildcardQuery),
|
|
|
|
|
- %?INFO("~p", [WildcardMatches]),
|
|
|
|
|
- DirectMatches ++ WildcardMatches.
|
|
|
|
|
|
|
+match(Topic) when is_list(Topic) ->
|
|
|
|
|
+ TrieNodes = mnesia:async_dirty(fun trie_match/1, [emqtt_topic:words(Topic)]),
|
|
|
|
|
+ Names = [Name || #trie_node{topic=Name} <- TrieNodes, Name=/= undefined],
|
|
|
|
|
+ lists:flatten([mnesia:dirty_read(topic, Name) || Name <- Names]).
|
|
|
|
|
|
|
|
down(Client) when is_pid(Client) ->
|
|
down(Client) when is_pid(Client) ->
|
|
|
gen_server2:cast(?MODULE, {down, Client}).
|
|
gen_server2:cast(?MODULE, {down, Client}).
|
|
|
|
|
|
|
|
init([]) ->
|
|
init([]) ->
|
|
|
- mnesia:create_table(direct_topic, [
|
|
|
|
|
- {type, bag},
|
|
|
|
|
- {record_name, topic},
|
|
|
|
|
- {ram_copies, [node()]},
|
|
|
|
|
- {attributes, record_info(fields, topic)}]),
|
|
|
|
|
- mnesia:add_table_copy(direct_topic, node(), ram_copies),
|
|
|
|
|
- mnesia:create_table(wildcard_topic, [
|
|
|
|
|
|
|
+ mnesia:create_table(trie, [
|
|
|
|
|
+ {ram_copies, [node()]},
|
|
|
|
|
+ {attributes, record_info(fields, trie)}]),
|
|
|
|
|
+ mnesia:add_table_copy(trie, node(), ram_copies),
|
|
|
|
|
+ mnesia:create_table(trie_node, [
|
|
|
|
|
+ {ram_copies, [node()]},
|
|
|
|
|
+ {attributes, record_info(fields, trie_node)}]),
|
|
|
|
|
+ mnesia:add_table_copy(trie_node, node(), ram_copies),
|
|
|
|
|
+ mnesia:create_table(topic, [
|
|
|
{type, bag},
|
|
{type, bag},
|
|
|
- {index, [#topic.words]},
|
|
|
|
|
{record_name, topic},
|
|
{record_name, topic},
|
|
|
{ram_copies, [node()]},
|
|
{ram_copies, [node()]},
|
|
|
{attributes, record_info(fields, topic)}]),
|
|
{attributes, record_info(fields, topic)}]),
|
|
|
- mnesia:add_table_copy(wildcard_topic, node(), ram_copies),
|
|
|
|
|
|
|
+ mnesia:add_table_copy(topic, node(), ram_copies),
|
|
|
ets:new(subscriber, [bag, named_table, {keypos, 2}]),
|
|
ets:new(subscriber, [bag, named_table, {keypos, 2}]),
|
|
|
?INFO_MSG("emqtt_router is started."),
|
|
?INFO_MSG("emqtt_router is started."),
|
|
|
{ok, #state{}}.
|
|
{ok, #state{}}.
|
|
|
|
|
|
|
|
-handle_call({subscribe, {Name, Qos}, Client}, _From, State) ->
|
|
|
|
|
- Topic = #topic{name=Name, node=node(), words=emqtt_topic:words(Name)},
|
|
|
|
|
- case emqtt_topic:type(Topic) of
|
|
|
|
|
- direct ->
|
|
|
|
|
- ok = mnesia:dirty_write(direct_topic, Topic);
|
|
|
|
|
- wildcard ->
|
|
|
|
|
- ok = mnesia:dirty_write(wildcard_topic, Topic)
|
|
|
|
|
- end,
|
|
|
|
|
- ets:insert(subscriber, #subscriber{topic=Name, qos=Qos, client=Client}),
|
|
|
|
|
- emqtt_retained:send(Name, Client),
|
|
|
|
|
- {reply, ok, State};
|
|
|
|
|
|
|
+handle_call({subscribe, {Topic, Qos}, Client}, _From, State) ->
|
|
|
|
|
+ case mnesia:transaction(fun trie_add/1, [Topic]) of
|
|
|
|
|
+ {atomic, _} ->
|
|
|
|
|
+ ets:insert(subscriber, #subscriber{topic=Topic, qos=Qos, client=Client}),
|
|
|
|
|
+ emqtt_retained:send(Topic, Client),
|
|
|
|
|
+ {reply, ok, State};
|
|
|
|
|
+ {aborted, Reason} ->
|
|
|
|
|
+ {reply, {error, Reason}, State}
|
|
|
|
|
+ end;
|
|
|
|
|
|
|
|
handle_call(Req, _From, State) ->
|
|
handle_call(Req, _From, State) ->
|
|
|
{stop, {badreq, Req}, State}.
|
|
{stop, {badreq, Req}, State}.
|
|
|
|
|
|
|
|
handle_cast({unsubscribe, Topic, Client}, State) ->
|
|
handle_cast({unsubscribe, Topic, Client}, State) ->
|
|
|
- ets:match_delete(subscriber, {subscriber, Topic, '_', Client}),
|
|
|
|
|
|
|
+ ets:match_delete(subscriber, #subscriber{topic=Topic, client=Client, _='_'}),
|
|
|
try_remove_topic(Topic),
|
|
try_remove_topic(Topic),
|
|
|
{noreply, State};
|
|
{noreply, State};
|
|
|
|
|
|
|
|
handle_cast({down, Client}, State) ->
|
|
handle_cast({down, Client}, State) ->
|
|
|
- case ets:match_object(subscriber, {subscriber, '_', '_', Client}) of
|
|
|
|
|
- [] ->
|
|
|
|
|
- ignore;
|
|
|
|
|
|
|
+ case ets:match_object(subscriber, #subscriber{client=Client, _='_'}) of
|
|
|
|
|
+ [] -> ignore;
|
|
|
Subs ->
|
|
Subs ->
|
|
|
[ets:delete_object(subscriber, Sub) || Sub <- Subs],
|
|
[ets:delete_object(subscriber, Sub) || Sub <- Subs],
|
|
|
[try_remove_topic(Topic) || #subscriber{topic=Topic} <- Subs]
|
|
[try_remove_topic(Topic) || #subscriber{topic=Topic} <- Subs]
|
|
@@ -134,7 +122,6 @@ handle_cast({down, Client}, State) ->
|
|
|
handle_cast(Msg, State) ->
|
|
handle_cast(Msg, State) ->
|
|
|
{stop, {badmsg, Msg}, State}.
|
|
{stop, {badmsg, Msg}, State}.
|
|
|
|
|
|
|
|
-
|
|
|
|
|
handle_info(Info, State) ->
|
|
handle_info(Info, State) ->
|
|
|
{stop, {badinfo, Info}, State}.
|
|
{stop, {badinfo, Info}, State}.
|
|
|
|
|
|
|
@@ -151,12 +138,93 @@ try_remove_topic(Name) ->
|
|
|
case ets:member(subscriber, Name) of
|
|
case ets:member(subscriber, Name) of
|
|
|
false ->
|
|
false ->
|
|
|
Topic = emqtt_topic:new(Name),
|
|
Topic = emqtt_topic:new(Name),
|
|
|
- case emqtt_topic:type(Topic) of
|
|
|
|
|
- direct ->
|
|
|
|
|
- mnesia:dirty_delete_object(direct_topic, Topic);
|
|
|
|
|
- wildcard ->
|
|
|
|
|
- mnesia:dirty_delete_object(wildcard_topic, Topic)
|
|
|
|
|
|
|
+ Fun = fun() ->
|
|
|
|
|
+ mnesia:delete_object(topic, Topic),
|
|
|
|
|
+ case mnesia:read(topic, Topic) of
|
|
|
|
|
+ [] -> trie_delete(Name);
|
|
|
|
|
+ _ -> ignore
|
|
|
|
|
+ end
|
|
|
|
|
+ end,
|
|
|
|
|
+ mnesia:transaction(Fun);
|
|
|
|
|
+ true ->
|
|
|
|
|
+ ok
|
|
|
|
|
+ end.
|
|
|
|
|
+
|
|
|
|
|
+trie_add(Topic) ->
|
|
|
|
|
+ mnesia:write(emqtt_topic:new(Topic)),
|
|
|
|
|
+ case mnesia:read(trie_node, Topic) of
|
|
|
|
|
+ [TrieNode=#trie_node{topic=undefined}] ->
|
|
|
|
|
+ mnesia:write(TrieNode#trie_node{topic=Topic});
|
|
|
|
|
+ [#trie_node{topic=Topic}] ->
|
|
|
|
|
+ ignore;
|
|
|
|
|
+ [] ->
|
|
|
|
|
+ %add trie path
|
|
|
|
|
+ [trie_add_path(Triple) || Triple <- emqtt_topic:triples(Topic)],
|
|
|
|
|
+ %add last node
|
|
|
|
|
+ mnesia:write(#trie_node{node_id=Topic, topic=Topic})
|
|
|
|
|
+ end.
|
|
|
|
|
+
|
|
|
|
|
+trie_delete(Topic) ->
|
|
|
|
|
+ case mnesia:read(trie_node, Topic) of
|
|
|
|
|
+ [#trie_node{edge_count=0}] ->
|
|
|
|
|
+ mnesia:delete({trie_node, Topic}),
|
|
|
|
|
+ trie_delete_path(lists:reverse(emqtt_topic:triples(Topic)));
|
|
|
|
|
+ [TrieNode] ->
|
|
|
|
|
+ mnesia:write(TrieNode#trie_node{topic=Topic});
|
|
|
|
|
+ [] ->
|
|
|
|
|
+ ignore
|
|
|
|
|
+ end.
|
|
|
|
|
+
|
|
|
|
|
+trie_match(Words) ->
|
|
|
|
|
+ trie_match(root, Words, []).
|
|
|
|
|
+
|
|
|
|
|
+trie_match(NodeId, [], ResAcc) ->
|
|
|
|
|
+ mnesia:read(trie_node, NodeId) ++ 'trie_match_#'(NodeId, ResAcc);
|
|
|
|
|
+
|
|
|
|
|
+trie_match(NodeId, [W|Words], ResAcc) ->
|
|
|
|
|
+ lists:foldl(fun(WArg, Acc) ->
|
|
|
|
|
+ case mnesia:read(trie, #trie_edge{node_id=NodeId, word=WArg}) of
|
|
|
|
|
+ [#trie{node_id=ChildId}] -> trie_match(ChildId, Words, Acc);
|
|
|
|
|
+ [] -> Acc
|
|
|
|
|
+ end
|
|
|
|
|
+ end, 'trie_match_#'(NodeId, ResAcc), [W, "+"]).
|
|
|
|
|
+
|
|
|
|
|
+'trie_match_#'(NodeId, ResAcc) ->
|
|
|
|
|
+ case mnesia:read(trie, #trie_edge{node_id=NodeId, word="#"}) of
|
|
|
|
|
+ [#trie{node_id=ChildId}] ->
|
|
|
|
|
+ mnesia:read(trie_node, ChildId) ++ ResAcc;
|
|
|
|
|
+ [] ->
|
|
|
|
|
+ ResAcc
|
|
|
|
|
+ end.
|
|
|
|
|
+
|
|
|
|
|
+trie_add_path({Node, Word, Child}) ->
|
|
|
|
|
+ Edge = #trie_edge{node_id=Node, word=Word},
|
|
|
|
|
+ case mnesia:read(trie_node, Node) of
|
|
|
|
|
+ [TrieNode = #trie_node{edge_count=Count}] ->
|
|
|
|
|
+ case mnesia:read(trie, Edge) of
|
|
|
|
|
+ [] ->
|
|
|
|
|
+ mnesia:write(TrieNode#trie_node{edge_count=Count+1}),
|
|
|
|
|
+ mnesia:write(#trie{edge=Edge, node_id=Child});
|
|
|
|
|
+ [_] ->
|
|
|
|
|
+ ok
|
|
|
end;
|
|
end;
|
|
|
- true -> ok
|
|
|
|
|
|
|
+ [] ->
|
|
|
|
|
+ mnesia:write(#trie_node{node_id=Node, edge_count=1}),
|
|
|
|
|
+ mnesia:write(#trie{edge=Edge, node_id=Child})
|
|
|
|
|
+ end.
|
|
|
|
|
+
|
|
|
|
|
+trie_delete_path([{NodeId, Word, _} | RestPath]) ->
|
|
|
|
|
+ Edge = #trie_edge{node_id=NodeId, word=Word},
|
|
|
|
|
+ mnesia:delete({trie, Edge}),
|
|
|
|
|
+ case mnesia:read(trie_node, NodeId) of
|
|
|
|
|
+ [#trie_node{edge_count=1, topic=undefined}] ->
|
|
|
|
|
+ mnesia:delete({trie_node, NodeId}),
|
|
|
|
|
+ trie_delete_path(RestPath);
|
|
|
|
|
+ [TrieNode=#trie_node{edge_count=1, topic=_}] ->
|
|
|
|
|
+ mnesia:write(TrieNode#trie_node{edge_count=0});
|
|
|
|
|
+ [TrieNode=#trie_node{edge_count=C}] ->
|
|
|
|
|
+ mnesia:write(TrieNode#trie_node{edge_count=C-1});
|
|
|
|
|
+ [] ->
|
|
|
|
|
+ throw({notfound, NodeId})
|
|
|
end.
|
|
end.
|
|
|
|
|
|