diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..61767a9fb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/org.eclipse.paho.mqtt.testing"] + path = tests/org.eclipse.paho.mqtt.testing + url = git://git.eclipse.org/gitroot/paho/org.eclipse.paho.mqtt.testing.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 9011e9dc0..d582ef4f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,34 @@ eMQTT ChangeLog ================== +v0.3.0-alpha (2015-01-18) +------------------------ + +NOTICE: Full MQTT 3.1.1 support now! + +Feature: Passed org.eclipse.paho.mqtt.testing/interoperability tests + +Feature: Qos0, Qos1 and Qos2 publish and suscribe + +Feature: session(clean_sess=false) management and offline messages + +Feature: redeliver awaiting puback/pubrec messages(doc: Chapter 4.4) + +Feature: retain messages, add emqtt_server module + +Feature: MQTT 3.1.1 null client_id support + +Bugfix: keepalive timeout to send will message + +Improve: overlapping subscription support + +Improve: add emqtt_packet:dump to dump packets + +Test: passed org.eclipse.paho.mqtt.testing/interoperability + +Test: simple cluster test + + v0.2.1-beta (2015-01-08) ------------------------ diff --git a/LICENSE b/LICENSE index fe58b384a..287c4750f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014, Feng Lee +Copyright (c) 2012-2015, Feng Lee Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d29d20dde..c4e51fa93 100644 --- a/README.md +++ b/README.md @@ -45,19 +45,19 @@ cd $INSTALL_DIR/emqtt ### etc/app.config ``` -{emqtt, [ - {auth, {anonymous, []}}, %internal, anonymous - {listen, [ - {mqtt, 1883, [ - {max_conns, 1024}, - {acceptor_pool, 4} - ]}, - {http, 8883, [ - {max_conns, 512}, - {acceptor_pool, 1} - ]} - ]} -]} + {emqtt, [ + {auth, {anonymous, []}}, %internal, anonymous + {listen, [ + {mqtt, 1883, [ + {max_conns, 1024}, + {acceptor_pool, 4} + ]}, + {http, 8883, [ + {max_conns, 512}, + {acceptor_pool, 1} + ]} + ]} + ]} ``` @@ -97,6 +97,26 @@ on 'host2': Run './bin/emqtt_ctl cluster' on 'host1' or 'host2' to check cluster nodes. +## Cluster + +Suppose we cluster two nodes on 'host1', 'host2', steps: + +on 'host1': + +``` +./bin/emqtt start +``` + +on 'host2': + +``` +./bin/emqtt start + +./bin/emqtt cluster emqtt@host1 +``` + +Run './bin/emqtt cluster' on 'host1' or 'host2' to check cluster nodes. + ## HTTP API eMQTT support http to publish message. @@ -134,5 +154,6 @@ feng at emqtt.io ## Thanks +@hejin1026 (260495915 at qq.com) @desoulter (assoulter123 at gmail.com) diff --git a/TODO b/TODO index 836737038..d3ebc89cd 100644 --- a/TODO +++ b/TODO @@ -23,3 +23,32 @@ merge pull request#26 temporary, 5000, worker, [emqtt_client]}]}}. fucking stupid..... esockd locked + +0.2.1 +===== + +full MQTT 3.1.1 support... + +node cluster.... + +one million connections test... + +topic match benchmark tests... + +Dialyzer ... + +full test cases... + +spawn_link to replace 'spawn' and 'link' + +keepalive + +retained + +QOS + +dural sub + +packet dump... + + diff --git a/apps/emqtt/include/emqtt.hrl b/apps/emqtt/include/emqtt.hrl index dbffbe0e4..ff67fafde 100644 --- a/apps/emqtt/include/emqtt.hrl +++ b/apps/emqtt/include/emqtt.hrl @@ -1,5 +1,5 @@ %%------------------------------------------------------------------------------ -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -20,49 +20,76 @@ %% SOFTWARE. %%------------------------------------------------------------------------------ -%% --------------------------------- -%% banner -%% --------------------------------- --define(COPYRIGHT, "Copyright (C) 2014, Feng Lee"). +%%------------------------------------------------------------------------------ +%% Banner +%%------------------------------------------------------------------------------ +-define(COPYRIGHT, "Copyright (C) 2012-2015, Feng Lee "). -define(LICENSE_MESSAGE, "Licensed under MIT"). --define(PROTOCOL_VERSION, "MQTT/3.1"). +-define(PROTOCOL_VERSION, "MQTT/3.1.1"). -define(ERTS_MINIMUM, "6.0"). %%------------------------------------------------------------------------------ -%% MQTT Qos +%% MQTT QoS %%------------------------------------------------------------------------------ -define(QOS_0, 0). -define(QOS_1, 1). -define(QOS_2, 2). --type qos() :: ?QOS_2 | ?QOS_1 | ?QOS_0. +-type mqtt_qos() :: ?QOS_2 | ?QOS_1 | ?QOS_0. + +%%------------------------------------------------------------------------------ +%% MQTT Client +%%------------------------------------------------------------------------------ +-record(mqtt_client, { + client_id, + username +}). + +-type mqtt_client() :: #mqtt_client{}. + +%%------------------------------------------------------------------------------ +%% MQTT Session +%%------------------------------------------------------------------------------ +-record(mqtt_session, { + client_id, + session_pid, + subscriptions = [], + awaiting_ack, + awaiting_rel +}). + +-type mqtt_session() :: #mqtt_session{}. %%------------------------------------------------------------------------------ %% MQTT Message %%------------------------------------------------------------------------------ --record(mqtt_msg, { - retain, - qos, - topic, - dup, - msgid, - payload, - encoder +-record(mqtt_message, { + msgid :: integer() | undefined, + qos = ?QOS_0 :: mqtt_qos(), + retain = false :: boolean(), + dup = false :: boolean(), + topic :: binary(), + payload :: binary() }). --type mqtt_msg() :: #mqtt_msg{}. +-type mqtt_message() :: #mqtt_message{}. %%------------------------------------------------------------------------------ %% MQTT User Management %%------------------------------------------------------------------------------ --record(emqtt_user, { - username :: binary(), - passwdhash :: binary() +-record(mqtt_user, { + username :: binary(), + passwdhash :: binary() }). +%%------------------------------------------------------------------------------ +%% MQTT Authorization +%%------------------------------------------------------------------------------ + +%%TODO: ClientId | Username --> Pub | Sub --> Topics diff --git a/apps/emqtt/include/emqtt_frame.hrl b/apps/emqtt/include/emqtt_frame.hrl deleted file mode 100644 index d51f3663b..000000000 --- a/apps/emqtt/include/emqtt_frame.hrl +++ /dev/null @@ -1,92 +0,0 @@ -% -% NOTICE: copy from rabbitmq mqtt-adaper -% - -%% The contents of this file are subject to the Mozilla Public License -%% Version 1.1 (the "License"); you may not use this file except in -%% compliance with the License. You may obtain a copy of the License -%% at http://www.mozilla.org/MPL/ -%% -%% Software distributed under the License is distributed on an "AS IS" -%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%% the License for the specific language governing rights and -%% limitations under the License. -%% -%% The Original Code is RabbitMQ. -%% -%% The Initial Developer of the Original Code is VMware, Inc. -%% Copyright (c) 2007-2012 VMware, Inc. All rights reserved. -%% - --define(CLIENT_ID_MAXLEN, 1024). - --define(PROTOCOL_NAMES, [{3, <<"MQIsdp">>}, {4, <<"MQTT">>}]). - --define(MQTT_PROTO_MAJOR, 3). --define(MQTT_PROTO_MINOR, 1). - -%% frame types - --define(CONNECT, 1). --define(CONNACK, 2). --define(PUBLISH, 3). --define(PUBACK, 4). --define(PUBREC, 5). --define(PUBREL, 6). --define(PUBCOMP, 7). --define(SUBSCRIBE, 8). --define(SUBACK, 9). --define(UNSUBSCRIBE, 10). --define(UNSUBACK, 11). --define(PINGREQ, 12). --define(PINGRESP, 13). --define(DISCONNECT, 14). - -%% connect return codes - --define(CONNACK_ACCEPT, 0). --define(CONNACK_PROTO_VER, 1). %% unacceptable protocol version --define(CONNACK_INVALID_ID, 2). %% identifier rejected --define(CONNACK_SERVER, 3). %% server unavailable --define(CONNACK_CREDENTIALS, 4). %% bad user name or password --define(CONNACK_AUTH, 5). %% not authorized - - --record(mqtt_frame, {fixed, - variable, - payload}). - --record(mqtt_frame_fixed, {type = 0, - dup = 0, - qos = 0, - retain = 0}). - --record(mqtt_frame_connect, {proto_ver, - will_retain, - will_qos, - will_flag, - clean_sess, - keep_alive, - client_id, - will_topic, - will_msg, - username, - password}). - --record(mqtt_frame_connack, {return_code}). - --record(mqtt_frame_publish, {topic_name, - message_id}). - --record(mqtt_frame_subscribe,{message_id, - topic_table}). - --record(mqtt_frame_suback, {message_id, - qos_table = []}). - --record(mqtt_topic, {name, - qos}). - --record(mqtt_frame_other, {other}). - - diff --git a/apps/emqtt/include/emqtt_log.hrl b/apps/emqtt/include/emqtt_log.hrl deleted file mode 100644 index cb9215ebc..000000000 --- a/apps/emqtt/include/emqtt_log.hrl +++ /dev/null @@ -1,85 +0,0 @@ -%%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee -%% -%% Permission is hereby granted, free of charge, to any person obtaining a copy -%% of this software and associated documentation files (the "Software"), to deal -%% in the Software without restriction, including without limitation the rights -%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the Software is -%% furnished to do so, subject to the following conditions: -%% -%% The above copyright notice and this permission notice shall be included in all -%% copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%% SOFTWARE. -%%------------------------------------------------------------------------------ - -%%------------------------------------------------------------------------------ -%% Logging mechanism -%%------------------------------------------------------------------------------ --define(PRINT(Format, Args), - io:format(Format, Args)). - --define(PRINT_MSG(Msg), - io:format(Msg)). - --define(DEBUG(Format, Args), - lager:debug(Format, Args)). - --define(DEBUG_TRACE(Dest, Format, Args), - lager:debug(Dest, Format, Args)). - --define(DEBUG_MSG(Msg), - lager:debug(Msg)). - --define(INFO(Format, Args), - lager:info(Format, Args)). - --define(INFO_TRACE(Dest, Format, Args), - lager:info(Dest, Format, Args)). - --define(INFO_MSG(Msg), - lager:info(Msg)). - --define(WARN(Format, Args), - lager:warning(Format, Args)). - --define(WARN_TRACE(Dest, Format, Args), - lager:warning(Dest, Format, Args)). - --define(WARN_MSG(Msg), - lager:warning(Msg)). - --define(WARNING(Format, Args), - lager:warning(Format, Args)). - --define(WARNING_TRACE(Dest, Format, Args), - lager:warning(Dest, Format, Args)). - --define(WARNING_MSG(Msg), - lager:warning(Msg)). - --define(ERROR(Format, Args), - lager:error(Format, Args)). - --define(ERROR_TRACE(Dest, Format, Args), - lager:error(Dest, Format, Args)). - --define(ERROR_MSG(Msg), - lager:error(Msg)). - --define(CRITICAL(Format, Args), - lager:critical(Format, Args)). - --define(CRITICAL_TRACE(Dest, Format, Args), - lager:critical(Dest, Format, Args)). - --define(CRITICAL_MSG(Msg), - lager:critical(Msg)). - diff --git a/apps/emqtt/include/emqtt_packet.hrl b/apps/emqtt/include/emqtt_packet.hrl new file mode 100644 index 000000000..4dbc2c007 --- /dev/null +++ b/apps/emqtt/include/emqtt_packet.hrl @@ -0,0 +1,135 @@ +%%------------------------------------------------------------------------------ +%% Copyright (c) 2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ +%% +%% The Original Code is from RabbitMQ. +%% + +%%------------------------------------------------------------------------------ +%% MQTT Protocol Version and Levels +%%------------------------------------------------------------------------------ +-define(MQTT_PROTO_V31, 3). +-define(MQTT_PROTO_V311, 4). + +-define(PROTOCOL_NAMES, [{?MQTT_PROTO_V31, <<"MQIsdp">>}, {?MQTT_PROTO_V311, <<"MQTT">>}]). + +-define(MAX_CLIENTID_LEN, 1024). + +%%------------------------------------------------------------------------------ +%% MQTT Control Packet Types +%%------------------------------------------------------------------------------ +-define(RESERVED, 0). %% Reserved +-define(CONNECT, 1). %% Client request to connect to Server +-define(CONNACK, 2). %% Server to Client: Connect acknowledgment +-define(PUBLISH, 3). %% Publish message +-define(PUBACK, 4). %% Publish acknowledgment +-define(PUBREC, 5). %% Publish received (assured delivery part 1) +-define(PUBREL, 6). %% Publish release (assured delivery part 2) +-define(PUBCOMP, 7). %% Publish complete (assured delivery part 3) +-define(SUBSCRIBE, 8). %% Client subscribe request +-define(SUBACK, 9). %% Server Subscribe acknowledgment +-define(UNSUBSCRIBE, 10). %% Unsubscribe request +-define(UNSUBACK, 11). %% Unsubscribe acknowledgment +-define(PINGREQ, 12). %% PING request +-define(PINGRESP, 13). %% PING response +-define(DISCONNECT, 14). %% Client is disconnecting + +%%------------------------------------------------------------------------------ +%% MQTT Connect Return Codes +%%------------------------------------------------------------------------------ +-define(CONNACK_ACCEPT, 0). %% Connection accepted +-define(CONNACK_PROTO_VER, 1). %% Unacceptable protocol version +-define(CONNACK_INVALID_ID, 2). %% Client Identifier is correct UTF-8 but not allowed by the Server +-define(CONNACK_SERVER, 3). %% Server unavailable +-define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed +-define(CONNACK_AUTH, 5). %% Client is not authorized to connect + +%%------------------------------------------------------------------------------ +%% MQTT Erlang Types +%%------------------------------------------------------------------------------ +-type mqtt_packet_type() :: ?RESERVED..?DISCONNECT. + +-type mqtt_packet_id() :: 1..16#ffff | undefined. + + +%%------------------------------------------------------------------------------ +%% MQTT Packet Fixed Header +%%------------------------------------------------------------------------------ +-record(mqtt_packet_header, { + type = ?RESERVED :: mqtt_packet_type(), + dup = false :: boolean(), + qos = 0 :: 0 | 1 | 2, + retain = false :: boolean() }). + +%%------------------------------------------------------------------------------ +%% MQTT Packets +%%------------------------------------------------------------------------------ +-record(mqtt_packet_connect, { + proto_ver, + proto_name, + will_retain, + will_qos, + will_flag, + clean_sess, + keep_alive, + client_id, + will_topic, + will_msg, + username, + password }). + +-record(mqtt_packet_connack, { + ack_flags = ?RESERVED, + return_code }). + +-record(mqtt_packet_publish, { + topic_name :: binary(), + packet_id :: mqtt_packet_id() }). + +-record(mqtt_packet_puback, { + packet_id :: mqtt_packet_id() }). + +-record(mqtt_topic, { + name :: binary(), + qos :: 0 | 1 | 2 }). + +-record(mqtt_packet_subscribe, { + packet_id :: mqtt_packet_id(), + topic_table :: list(#mqtt_topic{}) }). + +-record(mqtt_packet_suback, { + packet_id :: mqtt_packet_id(), + qos_table = [] }). + +%%------------------------------------------------------------------------------ +%% MQTT Control Packet +%%------------------------------------------------------------------------------ +-record(mqtt_packet, { + header :: #mqtt_packet_header{}, + variable :: #mqtt_packet_connect{} | #mqtt_packet_connack{} + | #mqtt_packet_publish{} | #mqtt_packet_puback{} + | #mqtt_packet_subscribe{} | #mqtt_packet_suback{} + | mqtt_packet_id(), + payload :: binary() }). + +-type mqtt_packet() :: #mqtt_packet{}. + + diff --git a/apps/emqtt/include/emqtt_topic.hrl b/apps/emqtt/include/emqtt_topic.hrl index c31ea8556..4cb9e4dda 100644 --- a/apps/emqtt/include/emqtt_topic.hrl +++ b/apps/emqtt/include/emqtt_topic.hrl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -24,15 +24,15 @@ %% Core PubSub Topic %%------------------------------------------------------------------------------ -record(topic, { - name :: binary(), - node :: node() + name :: binary(), + node :: node() }). -type topic() :: #topic{}. -record(topic_subscriber, { topic :: binary(), - qos = 0 :: integer(), + qos = 0 :: non_neg_integer(), subpid :: pid() }). @@ -44,7 +44,7 @@ -record(topic_trie_edge, { node_id :: binary(), - word :: binary() + word :: binary() | char() }). -record(topic_trie, { @@ -52,3 +52,8 @@ node_id :: binary() }). +%%------------------------------------------------------------------------------ +%% System Topic +%%------------------------------------------------------------------------------ +-define(SYSTOP, <<"$SYS">>). + diff --git a/apps/emqtt/src/emqtt.erl b/apps/emqtt/src/emqtt.erl index d881106ad..06d4b7d96 100644 --- a/apps/emqtt/src/emqtt.erl +++ b/apps/emqtt/src/emqtt.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -29,7 +29,7 @@ {packet, raw}, {reuseaddr, true}, {backlog, 512}, - {nodelay, false} + {nodelay, true} ]). listen(Listeners) when is_list(Listeners) -> diff --git a/apps/emqtt/src/emqtt_app.erl b/apps/emqtt/src/emqtt_app.erl index b8fac497f..82f4bd366 100644 --- a/apps/emqtt/src/emqtt_app.erl +++ b/apps/emqtt/src/emqtt_app.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,15 +22,17 @@ -module(emqtt_app). --author('feng@slimchat.io'). - --include("emqtt_log.hrl"). +-author('feng@emqtt.io'). -behaviour(application). %% Application callbacks -export([start/2, stop/1]). +-define(PRINT_MSG(Msg), io:format(Msg)). + +-define(PRINT(Format, Args), io:format(Format, Args)). + %% =================================================================== %% Application callbacks %% =================================================================== @@ -57,38 +59,62 @@ print_vsn() -> ?PRINT("~s ~s is running now~n", [Desc, Vsn]). start_servers(Sup) -> + {ok, SessOpts} = application:get_env(session), + {ok, RetainOpts} = application:get_env(retain), lists:foreach( fun({Name, F}) when is_function(F) -> ?PRINT("~s is starting...", [Name]), F(), ?PRINT_MSG("[done]~n"); - ({Name, Server}) when is_atom(Server) -> + ({Name, Server}) -> ?PRINT("~s is starting...", [Name]), start_child(Sup, Server), ?PRINT_MSG("[done]~n"); - ({Name, Server, Opts}) when is_atom(Server) -> + ({Name, Server, Opts}) -> ?PRINT("~s is starting...", [ Name]), start_child(Sup, Server, Opts), ?PRINT_MSG("[done]~n") end, - [{"emqtt cm", emqtt_cm}, + [{"emqtt config", emqtt_config}, + {"emqtt server", emqtt_server, RetainOpts}, + {"emqtt client manager", emqtt_cm}, + {"emqtt session manager", emqtt_sm}, + {"emqtt session supervisor", {supervisor, emqtt_session_sup}, SessOpts}, {"emqtt auth", emqtt_auth}, - {"emqtt retained", emqtt_retained}, {"emqtt pubsub", emqtt_pubsub}, + {"emqtt router", emqtt_router}, {"emqtt monitor", emqtt_monitor} ]). -start_child(Sup, Name) -> +start_child(Sup, {supervisor, Name}) -> + supervisor:start_child(Sup, supervisor_spec(Name)); +start_child(Sup, Name) when is_atom(Name) -> {ok, _ChiId} = supervisor:start_child(Sup, worker_spec(Name)). -start_child(Sup, Name, Opts) -> + +start_child(Sup, {supervisor, Name}, Opts) -> + supervisor:start_child(Sup, supervisor_spec(Name, Opts)); +start_child(Sup, Name, Opts) when is_atom(Name) -> {ok, _ChiId} = supervisor:start_child(Sup, worker_spec(Name, Opts)). +%%TODO: refactor... +supervisor_spec(Name) -> + {Name, + {Name, start_link, []}, + permanent, infinity, supervisor, [Name]}. + +supervisor_spec(Name, Opts) -> + {Name, + {Name, start_link, [Opts]}, + permanent, infinity, supervisor, [Name]}. + worker_spec(Name) -> - {Name, {Name, start_link, []}, - permanent, 5000, worker, [Name]}. -worker_spec(Name, Opts) -> - {Name, {Name, start_link, [Opts]}, - permanent, 5000, worker, [Name]}. + {Name, + {Name, start_link, []}, + permanent, 5000, worker, [Name]}. +worker_spec(Name, Opts) -> + {Name, + {Name, start_link, [Opts]}, + permanent, 5000, worker, [Name]}. %% %% @spec stop(atom) -> 'ok' diff --git a/apps/emqtt/src/emqtt_auth.erl b/apps/emqtt/src/emqtt_auth.erl index 0905ef8ee..4045c5c1a 100644 --- a/apps/emqtt/src/emqtt_auth.erl +++ b/apps/emqtt/src/emqtt_auth.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,12 +22,10 @@ -module(emqtt_auth). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -include("emqtt.hrl"). --include("emqtt_log.hrl"). - -export([start_link/0, add/2, check/1, check/2, @@ -73,7 +71,6 @@ init([]) -> ok = AuthMod:init(Opts), ets:new(?TAB, [named_table, protected]), ets:insert(?TAB, {mod, AuthMod}), - ?PRINT("emqtt authmod is ~p", [AuthMod]), {ok, undefined}. authmod(Name) when is_atom(Name) -> diff --git a/apps/emqtt/src/emqtt_auth_anonymous.erl b/apps/emqtt/src/emqtt_auth_anonymous.erl index a823eeef3..8bd5e4ad6 100644 --- a/apps/emqtt/src/emqtt_auth_anonymous.erl +++ b/apps/emqtt/src/emqtt_auth_anonymous.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,7 @@ -module(emqtt_auth_anonymous). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -export([init/1, add/2, diff --git a/apps/emqtt/src/emqtt_auth_internal.erl b/apps/emqtt/src/emqtt_auth_internal.erl index f2dcef1dd..c0d5a6d10 100644 --- a/apps/emqtt/src/emqtt_auth_internal.erl +++ b/apps/emqtt/src/emqtt_auth_internal.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,7 @@ -module(emqtt_auth_internal). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -include("emqtt.hrl"). @@ -32,10 +32,10 @@ delete/1]). init(_Opts) -> - mnesia:create_table(emqtt_user, [ + mnesia:create_table(mqtt_user, [ {ram_copies, [node()]}, - {attributes, record_info(fields, emqtt_user)}]), - mnesia:add_table_copy(emqtt_user, node(), ram_copies), + {attributes, record_info(fields, mqtt_user)}]), + mnesia:add_table_copy(mqtt_user, node(), ram_copies), ok. check(undefined, _) -> false; @@ -44,19 +44,19 @@ check(_, undefined) -> false; check(Username, Password) when is_binary(Username), is_binary(Password) -> PasswdHash = crypto:hash(md5, Password), - case mnesia:dirty_read(emqtt_user, Username) of - [#emqtt_user{passwdhash=PasswdHash}] -> true; + case mnesia:dirty_read(mqtt_user, Username) of + [#mqtt_user{passwdhash=PasswdHash}] -> true; _ -> false end. add(Username, Password) when is_binary(Username) and is_binary(Password) -> mnesia:dirty_write( - #emqtt_user{ + #mqtt_user{ username=Username, passwdhash=crypto:hash(md5, Password) } ). delete(Username) when is_binary(Username) -> - mnesia:dirty_delete(emqtt_user, Username). + mnesia:dirty_delete(mqtt_user, Username). diff --git a/apps/emqtt/src/emqtt_bridge.erl b/apps/emqtt/src/emqtt_bridge.erl new file mode 100644 index 000000000..04df2955d --- /dev/null +++ b/apps/emqtt/src/emqtt_bridge.erl @@ -0,0 +1,26 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + +-module(emqtt_bridge). + + + diff --git a/apps/emqtt/src/emqtt_client.erl b/apps/emqtt/src/emqtt_client.erl index da5dabfb0..751f29d17 100644 --- a/apps/emqtt/src/emqtt_client.erl +++ b/apps/emqtt/src/emqtt_client.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,165 +22,141 @@ -module(emqtt_client). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -behaviour(gen_server). --export([start_link/1, - info/1, - go/2]). +-export([start_link/1, info/1, go/2]). -export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, + handle_call/3, + handle_cast/2, + handle_info/2, code_change/3, - terminate/2]). + terminate/2]). -include("emqtt.hrl"). --include("emqtt_log.hrl"). +-include("emqtt_packet.hrl"). --include("emqtt_frame.hrl"). - --record(state, {socket, - conn_name, - await_recv, - connection_state, - conserve, - parse_state, - message_id, - client_id, - clean_sess, - will_msg, - keep_alive, - awaiting_ack, - subtopics, - awaiting_rel}). - - --define(FRAME_TYPE(Frame, Type), - Frame = #mqtt_frame{ fixed = #mqtt_frame_fixed{ type = Type }}). +%%Client State... +-record(state, { + socket, + peer_name, + conn_name, + await_recv, + conn_state, + conserve, + parse_state, + proto_state, + keepalive +}). start_link(Sock) -> gen_server:start_link(?MODULE, [Sock], []). info(Pid) -> - gen_server:call(Pid, info). + gen_server:call(Pid, info). go(Pid, Sock) -> - gen_server:call(Pid, {go, Sock}). + gen_server:call(Pid, {go, Sock}). init([Sock]) -> {ok, #state{socket = Sock}, 1000}. -handle_call({go, Sock}, _From, State=#state{socket = Sock}) -> +handle_call({go, Sock}, _From, #state{socket = Sock}) -> + {ok, Peername} = emqtt_net:peer_string(Sock), {ok, ConnStr} = emqtt_net:connection_string(Sock, inbound), + lager:info("Connect from ~s", [ConnStr]), {reply, ok, - control_throttle( - #state{ socket = Sock, - conn_name = ConnStr, - await_recv = false, - connection_state = running, - conserve = false, - parse_state = emqtt_frame:initial_state(), - message_id = 1, - subtopics = [], - awaiting_ack = gb_trees:empty(), - awaiting_rel = gb_trees:empty()})}; + control_throttle( + #state{ socket = Sock, + peer_name = Peername, + conn_name = ConnStr, + await_recv = false, + conn_state = running, + conserve = false, + parse_state = emqtt_packet:initial_state(), + proto_state = emqtt_protocol:initial_state(Sock, Peername)}), 10000}; +handle_call(info, _From, State = #state{ + conn_name=ConnName, proto_state = ProtoState}) -> + {reply, [{conn_name, ConnName} | emqtt_protocol:info(ProtoState)], State}; -handle_call(info, _From, #state{conn_name=ConnName, - message_id=MsgId, client_id=ClientId} = State) -> - Info = [{conn_name, ConnName}, - {message_id, MsgId}, - {client_id, ClientId}], - {reply, Info, State}; - -handle_call(_Req, _From, State) -> - {reply, ok, State}. +handle_call(Req, _From, State) -> + {stop, {badreq, Req}, State}. handle_cast(Msg, State) -> - {stop, {badmsg, Msg}, State}. + {stop, {badmsg, Msg}, State}. handle_info(timeout, State) -> - stop({shutdown, timeout}, State); + stop({shutdown, timeout}, State); -handle_info({stop, duplicate_id}, State=#state{conn_name=ConnName, client_id=ClientId}) -> - ?ERROR("Shutdown for duplicate clientid:~s, conn:~s", [ClientId, ConnName]), - stop({shutdown, duplicate_id}, State); +handle_info({stop, duplicate_id, _NewPid}, State=#state{ proto_state = ProtoState, conn_name=ConnName}) -> + %% TODO: to... + %% need transfer data??? + %% emqtt_client:transfer(NewPid, Data), + lager:error("Shutdown for duplicate clientid: ~s, conn:~s", + [emqtt_protocol:client_id(ProtoState), ConnName]), + stop({shutdown, duplicate_id}, State); -handle_info({dispatch, Msg}, #state{socket = Sock, message_id=MsgId} = State) -> +%%TODO: ok?? +handle_info({dispatch, {From, Message}}, #state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqtt_protocol:send_message({From, Message}, ProtoState), + {noreply, State#state{proto_state = ProtoState1}}; - #mqtt_msg{retain = Retain, - qos = Qos, - topic = Topic, - dup = Dup, - payload = Payload, - encoder = Encoder} = Msg, - - Payload1 = - if - Encoder == undefined -> Payload; - true -> Encoder(Payload) - end, - - Frame = #mqtt_frame{ - fixed = #mqtt_frame_fixed{type = ?PUBLISH, - qos = Qos, - retain = Retain, - dup = Dup}, - variable = #mqtt_frame_publish{topic_name = Topic, - message_id = if - Qos == ?QOS_0 -> undefined; - true -> MsgId - end}, - payload = Payload1}, - - send_frame(Sock, Frame), - - if - Qos == ?QOS_0 -> - {noreply, State}; - true -> - {noreply, next_msg_id(State)} - end; +handle_info({redeliver, {?PUBREL, PacketId}}, #state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqtt_protocol:redeliver({?PUBREL, PacketId}, ProtoState), + {noreply, State#state{proto_state = ProtoState1}}; handle_info({inet_reply, _Ref, ok}, State) -> {noreply, State, hibernate}; -handle_info({inet_async, Sock, _Ref, {ok, Data}}, #state{ socket = Sock}=State) -> +handle_info({inet_async, Sock, _Ref, {ok, Data}}, State = #state{ peer_name = PeerName, socket = Sock }) -> + lager:debug("RECV from ~s: ~p", [PeerName, Data]), process_received_bytes( - Data, control_throttle(State #state{ await_recv = false })); + Data, control_throttle(State #state{ await_recv = false })); handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> network_error(Reason, State); -handle_info({inet_reply, _Sock, {error, Reason}}, State) -> - {noreply, State}; +handle_info({inet_reply, _Sock, {error, Reason}}, State = #state{peer_name = PeerName}) -> + lager:critical("Client ~s: unexpected inet_reply '~p'", [PeerName, Reason]), + {noreply, State}; -handle_info(keep_alive_timeout, #state{keep_alive=KeepAlive}=State) -> - case emqtt_keep_alive:state(KeepAlive) of - idle -> - ?INFO("keep_alive timeout: ~p", [State#state.conn_name]), - {stop, normal, State}; - active -> - KeepAlive1 = emqtt_keep_alive:reset(KeepAlive), - {noreply, State#state{keep_alive=KeepAlive1}} - end; +handle_info({keepalive, start, TimeoutSec}, State = #state{socket = Socket}) -> + lager:info("Client ~s: Start KeepAlive with ~p seconds", [State#state.peer_name, TimeoutSec]), + KeepAlive = emqtt_keepalive:new(Socket, TimeoutSec, {keepalive, timeout}), + {noreply, State#state{ keepalive = KeepAlive }}; -handle_info(Info, State) -> - ?ERROR("badinfo :~p",[Info]), - {stop, {badinfo, Info}, State}. +handle_info({keepalive, timeout}, State = #state { keepalive = KeepAlive }) -> + case emqtt_keepalive:resume(KeepAlive) of + timeout -> + lager:info("Client ~s: Keepalive Timeout!", [State#state.peer_name]), + stop({shutdown, keepalive_timeout}, State#state{keepalive = undefined}); + {resumed, KeepAlive1} -> + lager:info("Client ~s: Keepalive Resumed", [State#state.peer_name]), + {noreply, State#state{ keepalive = KeepAlive1 }} + end; -terminate(_Reason, #state{keep_alive=KeepAlive}) -> - emqtt_keep_alive:cancel(KeepAlive), - emqtt_cm:destroy(self()), - ok. +handle_info(Info, State = #state{peer_name = PeerName}) -> + lager:critical("Client ~s: unexpected info ~p",[PeerName, Info]), + {stop, {badinfo, Info}, State}. + +terminate(Reason, #state{ peer_name = PeerName, keepalive = KeepAlive, proto_state = ProtoState }) -> + lager:info("Client ~s: ~p terminated, reason: ~p~n", [PeerName, self(), Reason]), + emqtt_keepalive:cancel(KeepAlive), + case {ProtoState, Reason} of + {undefined, _} -> ok; + {_, {shutdown, Error}} -> + emqtt_protocol:shutdown(Error, ProtoState); + {_, _} -> ok %TODO: + end, + ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. - + async_recv(Sock, Length, infinity) when is_port(Sock) -> prim_inet:async_recv(Sock, Length, -1); @@ -191,221 +167,43 @@ async_recv(Sock, Length, Timeout) when is_port(Sock) -> % receive and parse tcp data %------------------------------------------------------- process_received_bytes(<<>>, State) -> - {noreply, State}; + {noreply, State, hibernate}; process_received_bytes(Bytes, State = #state{ parse_state = ParseState, + proto_state = ProtoState, conn_name = ConnStr }) -> - case emqtt_frame:parse(Bytes, ParseState) of - {more, ParseState1} -> - {noreply, - control_throttle( State #state{ parse_state = ParseState1 }), - hibernate}; - {ok, Frame, Rest} -> - case process_frame(Frame, State) of - {ok, State1} -> - PS = emqtt_frame:initial_state(), - process_received_bytes( - Rest, - State1 #state{ parse_state = PS}); - {err, Reason, State1} -> - ?ERROR("MQTT protocol error ~p for connection ~p~n", [Reason, ConnStr]), - stop({shutdown, Reason}, State1); - {stop, State1} -> - stop(normal, State1) - end; - {error, Error} -> - ?ERROR("MQTT detected framing error ~p for connection ~p~n", [ConnStr, Error]), - stop({shutdown, Error}, State) + case emqtt_packet:parse(Bytes, ParseState) of + {more, ParseState1} -> + {noreply, + control_throttle( State #state{ parse_state = ParseState1 }), + hibernate}; + {ok, Packet, Rest} -> + case emqtt_protocol:handle_packet(Packet, ProtoState) of + {ok, ProtoState1} -> + process_received_bytes( + Rest, + State#state{ parse_state = emqtt_packet:initial_state(), + proto_state = ProtoState1 }); + {error, Error} -> + lager:error("MQTT protocol error ~p for connection ~p~n", [Error, ConnStr]), + stop({shutdown, Error}, State); + {error, Error, ProtoState1} -> + stop({shutdown, Error}, State#state{proto_state = ProtoState1}); + {stop, Reason, ProtoState1} -> + stop(Reason, State#state{proto_state = ProtoState1}) + end; + {error, Error} -> + lager:error("MQTT detected framing error ~p for connection ~p~n", [ConnStr, Error]), + stop({shutdown, Error}, State) end. -process_frame(Frame = #mqtt_frame{fixed = #mqtt_frame_fixed{type = Type}}, - State=#state{client_id=ClientId, keep_alive=KeepAlive}) -> - KeepAlive1 = emqtt_keep_alive:activate(KeepAlive), - case validate_frame(Type, Frame) of - ok -> - ?INFO("frame from ~s: ~p", [ClientId, Frame]), - handle_retained(Type, Frame), - process_request(Type, Frame, State#state{keep_alive=KeepAlive1}); - {error, Reason} -> - {err, Reason, State} - end. - -process_request(?CONNECT, - #mqtt_frame{ variable = #mqtt_frame_connect{ - username = Username, - password = Password, - proto_ver = ProtoVersion, - clean_sess = CleanSess, - keep_alive = AlivePeriod, - client_id = ClientId } = Var}, #state{socket = Sock} = State) -> - {ReturnCode, State1} = - case {lists:member(ProtoVersion, proplists:get_keys(?PROTOCOL_NAMES)), - valid_client_id(ClientId)} of - {false, _} -> - {?CONNACK_PROTO_VER, State}; - {_, false} -> - {?CONNACK_INVALID_ID, State}; - _ -> - case emqtt_auth:check(Username, Password) of - false -> - ?ERROR_MSG("MQTT login failed - no credentials"), - {?CONNACK_CREDENTIALS, State}; - true -> - ?INFO("connect from clientid: ~p, ~p", [ClientId, AlivePeriod]), - emqtt_cm:create(ClientId, self()), - KeepAlive = emqtt_keep_alive:new(AlivePeriod*1500, keep_alive_timeout), - {?CONNACK_ACCEPT, - State #state{ will_msg = make_will_msg(Var), - client_id = ClientId, - keep_alive = KeepAlive}} - end - end, - ?INFO("recv conn...:~p", [ReturnCode]), - send_frame(Sock, #mqtt_frame{ fixed = #mqtt_frame_fixed{ type = ?CONNACK}, - variable = #mqtt_frame_connack{ - return_code = ReturnCode }}), - {ok, State1}; - -process_request(?PUBLISH, Frame=#mqtt_frame{ - fixed = #mqtt_frame_fixed{qos = ?QOS_0}}, State) -> - emqtt_pubsub:publish(make_msg(Frame)), - {ok, State}; - -process_request(?PUBLISH, - Frame=#mqtt_frame{ - fixed = #mqtt_frame_fixed{qos = ?QOS_1}, - variable = #mqtt_frame_publish{message_id = MsgId}}, - State=#state{socket=Sock}) -> - emqtt_pubsub:publish(make_msg(Frame)), - send_frame(Sock, #mqtt_frame{fixed = #mqtt_frame_fixed{ type = ?PUBACK }, - variable = #mqtt_frame_publish{ message_id = MsgId}}), - {ok, State}; - -process_request(?PUBLISH, - Frame=#mqtt_frame{ - fixed = #mqtt_frame_fixed{qos = ?QOS_2}, - variable = #mqtt_frame_publish{message_id = MsgId}}, - State=#state{socket=Sock}) -> - emqtt_pubsub:publish(make_msg(Frame)), - put({msg, MsgId}, pubrec), - send_frame(Sock, #mqtt_frame{fixed = #mqtt_frame_fixed{type = ?PUBREC}, - variable = #mqtt_frame_publish{ message_id = MsgId}}), - - {ok, State}; - -process_request(?PUBACK, #mqtt_frame{}, State) -> - %TODO: fixme later - {ok, State}; - -process_request(?PUBREC, #mqtt_frame{ - variable = #mqtt_frame_publish{message_id = MsgId}}, - State=#state{socket=Sock}) -> - %TODO: fixme later - send_frame(Sock, - #mqtt_frame{fixed = #mqtt_frame_fixed{ type = ?PUBREL}, - variable = #mqtt_frame_publish{ message_id = MsgId}}), - {ok, State}; - -process_request(?PUBREL, - #mqtt_frame{ - variable = #mqtt_frame_publish{message_id = MsgId}}, - State=#state{socket=Sock}) -> - erase({msg, MsgId}), - send_frame(Sock, - #mqtt_frame{fixed = #mqtt_frame_fixed{ type = ?PUBCOMP}, - variable = #mqtt_frame_publish{ message_id = MsgId}}), - {ok, State}; - -process_request(?PUBCOMP, #mqtt_frame{ - variable = #mqtt_frame_publish{message_id = _MsgId}}, State) -> - %TODO: fixme later - {ok, State}; - -process_request(?SUBSCRIBE, - #mqtt_frame{ - variable = #mqtt_frame_subscribe{message_id = MessageId, - topic_table = Topics}, - payload = undefined}, - #state{socket=Sock} = State) -> - - [emqtt_pubsub:subscribe({Name, Qos}, self()) || - #mqtt_topic{name=Name, qos=Qos} <- Topics], - - GrantedQos = [Qos || #mqtt_topic{qos=Qos} <- Topics], - - send_frame(Sock, #mqtt_frame{fixed = #mqtt_frame_fixed{type = ?SUBACK}, - variable = #mqtt_frame_suback{ - message_id = MessageId, - qos_table = GrantedQos}}), - - {ok, State}; - -process_request(?UNSUBSCRIBE, - #mqtt_frame{ - variable = #mqtt_frame_subscribe{message_id = MessageId, - topic_table = Topics }, - payload = undefined}, #state{socket = Sock, client_id = ClientId} = State) -> - - - [emqtt_pubsub:unsubscribe(Name, self()) || #mqtt_topic{name=Name} <- Topics], - - send_frame(Sock, #mqtt_frame{fixed = #mqtt_frame_fixed{type = ?UNSUBACK }, - variable = #mqtt_frame_suback{message_id = MessageId }}), - - {ok, State}; - -process_request(?PINGREQ, #mqtt_frame{}, #state{socket=Sock, keep_alive=KeepAlive}=State) -> - %Keep alive timer - KeepAlive1 = emqtt_keep_alive:reset(KeepAlive), - send_frame(Sock, #mqtt_frame{fixed = #mqtt_frame_fixed{ type = ?PINGRESP }}), - {ok, State#state{keep_alive=KeepAlive1}}; - -process_request(?DISCONNECT, #mqtt_frame{}, State=#state{client_id=ClientId}) -> - ?INFO("~s disconnected", [ClientId]), - {stop, State}. - -next_msg_id(State = #state{ message_id = 16#ffff }) -> - State #state{ message_id = 1 }; -next_msg_id(State = #state{ message_id = MsgId }) -> - State #state{ message_id = MsgId + 1 }. - -maybe_clean_sess(false, _Conn, _ClientId) -> - % todo: establish subscription to deliver old unacknowledged messages - ok. - %%---------------------------------------------------------------------------- - -make_will_msg(#mqtt_frame_connect{ will_flag = false }) -> - undefined; -make_will_msg(#mqtt_frame_connect{ will_retain = Retain, - will_qos = Qos, - will_topic = Topic, - will_msg = Msg }) -> - #mqtt_msg{retain = Retain, - qos = Qos, - topic = Topic, - dup = false, - payload = Msg }. - -send_will_msg(#state{will_msg = undefined}) -> - ignore; -send_will_msg(#state{will_msg = WillMsg }) -> - emqtt_pubsub:publish(WillMsg). - -send_frame(Sock, Frame) -> - ?INFO("send frame:~p", [Frame]), - erlang:port_command(Sock, emqtt_frame:serialise(Frame)). - -%%---------------------------------------------------------------------------- -network_error(Reason, - State = #state{ conn_name = ConnStr}) -> - ?ERROR("MQTT detected network error '~p' for ~p", [Reason, ConnStr]), - send_will_msg(State), - % todo: flush channel after publish +network_error(Reason, State = #state{ peer_name = PeerName }) -> + lager:error("Client ~s: MQTT detected network error '~p'", [PeerName, Reason]), stop({shutdown, conn_closed}, State). -run_socket(State = #state{ connection_state = blocked }) -> +run_socket(State = #state{ conn_state = blocked }) -> State; run_socket(State = #state{ await_recv = true }) -> State; @@ -413,75 +211,16 @@ run_socket(State = #state{ socket = Sock }) -> async_recv(Sock, 0, infinity), State#state{ await_recv = true }. -control_throttle(State = #state{ connection_state = Flow, +control_throttle(State = #state{ conn_state = Flow, conserve = Conserve }) -> case {Flow, Conserve} of - {running, true} -> State #state{ connection_state = blocked }; + {running, true} -> State #state{ conn_state = blocked }; {blocked, false} -> run_socket(State #state{ - connection_state = running }); + conn_state = running }); {_, _} -> run_socket(State) end. stop(Reason, State ) -> {stop, Reason, State}. -valid_client_id(ClientId) -> - ClientIdLen = size(ClientId), - 1 =< ClientIdLen andalso ClientIdLen =< ?CLIENT_ID_MAXLEN. - -handle_retained(?PUBLISH, #mqtt_frame{fixed = #mqtt_frame_fixed{retain = false}}) -> - ignore; - -handle_retained(?PUBLISH, #mqtt_frame{ - fixed = #mqtt_frame_fixed{retain = true}, - variable = #mqtt_frame_publish{topic_name = Topic}, - payload= <<>> }) -> - emqtt_retained:delete(Topic); - -handle_retained(?PUBLISH, Frame=#mqtt_frame{ - fixed = #mqtt_frame_fixed{retain = true}, - variable = #mqtt_frame_publish{topic_name = Topic}}) -> - emqtt_retained:insert(Topic, make_msg(Frame)); - -handle_retained(_, _) -> - ignore. - -validate_frame(?PUBLISH, #mqtt_frame{variable = #mqtt_frame_publish{topic_name = Topic}}) -> - case emqtt_topic:validate({publish, Topic}) of - true -> ok; - false -> {error, badtopic} - end; - -validate_frame(?UNSUBSCRIBE, #mqtt_frame{variable = #mqtt_frame_subscribe{topic_table = Topics}}) -> - ErrTopics = [Topic || #mqtt_topic{name=Topic, qos=Qos} <- Topics, - not emqtt_topic:validate({subscribe, Topic})], - case ErrTopics of - [] -> ok; - _ -> ?ERROR("error topics: ~p", [ErrTopics]), {error, badtopic} - end; - -validate_frame(?SUBSCRIBE, #mqtt_frame{variable = #mqtt_frame_subscribe{topic_table = Topics}}) -> - ErrTopics = [Topic || #mqtt_topic{name=Topic, qos=Qos} <- Topics, - not (emqtt_topic:validate({subscribe, Topic}) and (Qos < 3))], - case ErrTopics of - [] -> ok; - _ -> ?ERROR("error topics: ~p", [ErrTopics]), {error, badtopic} - end; - -validate_frame(_Type, _Frame) -> - ok. - -make_msg(#mqtt_frame{ - fixed = #mqtt_frame_fixed{qos = Qos, - retain = Retain, - dup = Dup}, - variable = #mqtt_frame_publish{topic_name = Topic, - message_id = MessageId}, - payload = Payload}) -> - #mqtt_msg{retain = Retain, - qos = Qos, - topic = Topic, - dup = Dup, - msgid = MessageId, - payload = Payload}. diff --git a/apps/emqtt/src/emqtt_cm.erl b/apps/emqtt/src/emqtt_cm.erl index 9061a6800..64cd451f4 100644 --- a/apps/emqtt/src/emqtt_cm.erl +++ b/apps/emqtt/src/emqtt_cm.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -23,21 +23,21 @@ %client manager -module(emqtt_cm). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -behaviour(gen_server). -define(SERVER, ?MODULE). +-define(TAB, emqtt_client). + %% ------------------------------------------------------------------ %% API Function Exports %% ------------------------------------------------------------------ -export([start_link/0]). --export([create/2, - destroy/1, - lookup/1]). +-export([lookup/1, register/2, unregister/2]). %% ------------------------------------------------------------------ %% gen_server Function Exports @@ -56,47 +56,75 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). +%% +%% @doc lookup client pid with clientId. +%% -spec lookup(ClientId :: binary()) -> pid() | undefined. -lookup(ClientId) -> +lookup(ClientId) when is_binary(ClientId) -> case ets:lookup(emqtt_client, ClientId) of - [{_, Pid}] -> Pid; + [{_, Pid, _}] -> Pid; [] -> undefined end. --spec create(ClientId :: binary(), Pid :: pid()) -> ok. -create(ClientId, Pid) -> - case lookup(ClientId) of - Pid -> - ignore; - OldPid when is_pid(OldPid) -> - OldPid ! {stop, duplicate_id}, - ets:insert(emqtt_client, {ClientId, Pid}); - undefined -> - ets:insert(emqtt_client, {ClientId, Pid}) - end. +%% +%% @doc register clientId with pid. +%% +-spec register(ClientId :: binary(), Pid :: pid()) -> ok. +register(ClientId, Pid) when is_binary(ClientId), is_pid(Pid) -> + gen_server:call(?SERVER, {register, ClientId, Pid}). --spec destroy(binary() | pid()) -> ok. -destroy(ClientId) when is_binary(ClientId) -> - ets:delete(emqtt_client, ClientId); - -destroy(Pid) when is_pid(Pid) -> - ets:match_delete(emqtt_client, {{'_', Pid}}). +%% +%% @doc unregister clientId with pid. +%% +-spec unregister(ClientId :: binary(), Pid :: pid()) -> ok. +unregister(ClientId, Pid) when is_binary(ClientId), is_pid(Pid) -> + gen_server:cast(?SERVER, {unregister, ClientId, Pid}). %% ------------------------------------------------------------------ %% gen_server Function Definitions %% ------------------------------------------------------------------ -init(Args) -> +init([]) -> %on one node - ets:new(emqtt_client, [named_table, public]), - {ok, Args}. + ets:new(?TAB, [set, named_table, protected]), + {ok, none}. + +handle_call({register, ClientId, Pid}, _From, State) -> + case ets:lookup(?TAB, ClientId) of + [{_, Pid, _}] -> + lager:error("clientId '~s' has been registered with ~p", [ClientId, Pid]), + ignore; + [{_, OldPid, MRef}] -> + OldPid ! {stop, duplicate_id, Pid}, + erlang:demonitor(MRef), + insert(ClientId, Pid); + [] -> + insert(ClientId, Pid) + end, + {reply, ok, State}; handle_call(_Request, _From, State) -> {reply, ok, State}. +handle_cast({unregister, ClientId, Pid}, State) -> + case ets:lookup(?TAB, ClientId) of + [{_, Pid, MRef}] -> + erlang:demonitor(MRef), + ets:delete(?TAB, ClientId); + [_] -> + ignore; + [] -> + lager:error("cannot find clientId '~s' with ~p", [ClientId, Pid]) + end, + {noreply, State}; + handle_cast(_Msg, State) -> {noreply, State}. +handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> + ets:match_delete(emqtt_client, {{'_', DownPid, MRef}}), + {noreply, State}; + handle_info(_Info, State) -> {noreply, State}. @@ -106,4 +134,6 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +insert(ClientId, Pid) -> + ets:insert(emqtt_client, {ClientId, Pid, erlang:monitor(process, Pid)}). diff --git a/apps/emqtt/src/emqtt_config.erl b/apps/emqtt/src/emqtt_config.erl new file mode 100644 index 000000000..0a18e922f --- /dev/null +++ b/apps/emqtt/src/emqtt_config.erl @@ -0,0 +1,82 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + +-module(emqtt_config). + +-export([lookup/1]). + +-behaviour(gen_server). + +-define(SERVER, ?MODULE). + +%% ------------------------------------------------------------------ +%% API Function Exports +%% ------------------------------------------------------------------ + +-export([start_link/0]). + + +%% ------------------------------------------------------------------ +%% gen_server Function Exports +%% ------------------------------------------------------------------ + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +%% ------------------------------------------------------------------ +%% API Function Definitions +%% ------------------------------------------------------------------ + +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +%%TODO: fix later... +lookup(Key) -> {ok, Key}. + +%% ------------------------------------------------------------------ +%% gen_server Function Definitions +%% ------------------------------------------------------------------ + +init(_Args) -> + ets:new(?MODULE, [set, protected, named_table]), + %%TODO: Load application config. + {ok, none}. + +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% ------------------------------------------------------------------ +%% Internal Function Definitions +%% ------------------------------------------------------------------ + diff --git a/apps/emqtt/src/emqtt_ctl.erl b/apps/emqtt/src/emqtt_ctl.erl index 9962ed73d..bee4caf5c 100644 --- a/apps/emqtt/src/emqtt_ctl.erl +++ b/apps/emqtt/src/emqtt_ctl.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,17 +22,20 @@ -module(emqtt_ctl). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -include("emqtt.hrl"). --include("emqtt_log.hrl"). +-define(PRINT_MSG(Msg), + io:format(Msg)). + +-define(PRINT(Format, Args), + io:format(Format, Args)). -export([status/1, - cluster_info/1, cluster/1, - add_user/1, - delete_user/1]). + useradd/1, + userdel/1]). status([]) -> {InternalStatus, _ProvidedStatus} = init:get_status(), @@ -44,26 +47,47 @@ status([]) -> ?PRINT_MSG("emqtt is running~n") end. -cluster_info([]) -> +cluster([]) -> Nodes = [node()|nodes()], - ?PRINT("cluster nodes: ~p~n", [Nodes]). + ?PRINT("cluster nodes: ~p~n", [Nodes]); cluster([SNode]) -> - Node = list_to_atom(SNode), + Node = node_name(SNode), case net_adm:ping(Node) of pong -> application:stop(emqtt), + application:stop(esockd), mnesia:stop(), mnesia:start(), mnesia:change_config(extra_db_nodes, [Node]), + application:start(esockd), application:start(emqtt), ?PRINT("cluster with ~p successfully.~n", [Node]); pang -> ?PRINT("failed to connect to ~p~n", [Node]) end. -add_user([Username, Password]) -> + +useradd([Username, Password]) -> ?PRINT("~p", [emqtt_auth:add(list_to_binary(Username), list_to_binary(Password))]). -delete_user([Username]) -> +userdel([Username]) -> ?PRINT("~p", [emqtt_auth:delete(list_to_binary(Username))]). + +node_name(SNode) -> + SNode1 = + case string:tokens(SNode, "@") of + [_Node, _Server] -> + SNode; + _ -> + case net_kernel:longnames() of + true -> + SNode ++ "@" ++ inet_db:gethostname() ++ + "." ++ inet_db:res_option(domain); + false -> + SNode ++ "@" ++ inet_db:gethostname(); + _ -> + SNode + end + end, + list_to_atom(SNode1). diff --git a/apps/emqtt/src/emqtt_db.erl b/apps/emqtt/src/emqtt_db.erl index 0b5f508ad..961556666 100644 --- a/apps/emqtt/src/emqtt_db.erl +++ b/apps/emqtt/src/emqtt_db.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,7 @@ -module(emqtt_db). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -export([init/0, stop/0]). diff --git a/apps/emqtt/src/emqtt_event.erl b/apps/emqtt/src/emqtt_event.erl new file mode 100644 index 000000000..ec0ff0f62 --- /dev/null +++ b/apps/emqtt/src/emqtt_event.erl @@ -0,0 +1,29 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + +-module(emqtt_event). + +-export([start_link/0]). + +start_link() -> + gen_event:start_link({local, ?MODULE}). + diff --git a/apps/emqtt/src/emqtt_frame.erl b/apps/emqtt/src/emqtt_frame.erl deleted file mode 100644 index 660e2c6f8..000000000 --- a/apps/emqtt/src/emqtt_frame.erl +++ /dev/null @@ -1,266 +0,0 @@ -%%------------------------------------------------------------------------------ -%% Copyright (c) 2014, Feng Lee -%% -%% Permission is hereby granted, free of charge, to any person obtaining a copy -%% of this software and associated documentation files (the "Software"), to deal -%% in the Software without restriction, including without limitation the rights -%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the Software is -%% furnished to do so, subject to the following conditions: -%% -%% The above copyright notice and this permission notice shall be included in all -%% copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%% SOFTWARE. -%%------------------------------------------------------------------------------ -%% -%% The Original Code is from RabbitMQ. -%% - --module(emqtt_frame). - --include("emqtt_frame.hrl"). - --export([parse/2, initial_state/0]). --export([serialise/1]). - --define(RESERVED, 0). --define(MAX_LEN, 16#fffffff). --define(HIGHBIT, 2#10000000). --define(LOWBITS, 2#01111111). - -initial_state() -> none. - -parse(<<>>, none) -> - {more, fun(Bin) -> parse(Bin, none) end}; -parse(<>, none) -> - parse_remaining_len(Rest, #mqtt_frame_fixed{ type = MessageType, - dup = bool(Dup), - qos = QoS, - retain = bool(Retain) }); -parse(Bin, Cont) -> Cont(Bin). - -parse_remaining_len(<<>>, Fixed) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Fixed) end}; -parse_remaining_len(Rest, Fixed) -> - parse_remaining_len(Rest, Fixed, 1, 0). - -parse_remaining_len(_Bin, _Fixed, _Multiplier, Length) - when Length > ?MAX_LEN -> - {error, invalid_mqtt_frame_len}; -parse_remaining_len(<<>>, Fixed, Multiplier, Length) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Fixed, Multiplier, Length) end}; -parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Fixed, Multiplier, Value) -> - parse_remaining_len(Rest, Fixed, Multiplier * ?HIGHBIT, Value + Len * Multiplier); -parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Fixed, Multiplier, Value) -> - parse_frame(Rest, Fixed, Value + Len * Multiplier). - -parse_frame(Bin, #mqtt_frame_fixed{ type = Type, - qos = Qos } = Fixed, Length) -> - case {Type, Bin} of - {?CONNECT, <>} -> - {ProtoName, Rest1} = parse_utf(FrameBin), - <> = Rest1, - <> = Rest2, - {ClientId, Rest4} = parse_utf(Rest3), - {WillTopic, Rest5} = parse_utf(Rest4, WillFlag), - {WillMsg, Rest6} = parse_msg(Rest5, WillFlag), - {UserName, Rest7} = parse_utf(Rest6, UsernameFlag), - {PasssWord, <<>>} = parse_utf(Rest7, PasswordFlag), - case protocol_name_approved(ProtoVersion, ProtoName) of - true -> - wrap(Fixed, - #mqtt_frame_connect{ - proto_ver = ProtoVersion, - will_retain = bool(WillRetain), - will_qos = WillQos, - will_flag = bool(WillFlag), - clean_sess = bool(CleanSession), - keep_alive = KeepAlive, - client_id = ClientId, - will_topic = WillTopic, - will_msg = WillMsg, - username = UserName, - password = PasssWord}, Rest); - false -> - {error, protocol_header_corrupt} - end; - {?PUBLISH, <>} -> - {TopicName, Rest1} = parse_utf(FrameBin), - {MessageId, Payload} = case Qos of - 0 -> {undefined, Rest1}; - _ -> <> = Rest1, - {M, R} - end, - wrap(Fixed, #mqtt_frame_publish {topic_name = TopicName, - message_id = MessageId }, - Payload, Rest); - {?PUBACK, <>} -> - <> = FrameBin, - wrap(Fixed, #mqtt_frame_publish{message_id = MessageId}, Rest); - {?PUBREC, <>} -> - <> = FrameBin, - wrap(Fixed, #mqtt_frame_publish{message_id = MessageId}, Rest); - {?PUBREL, <>} -> - <> = FrameBin, - wrap(Fixed, #mqtt_frame_publish { message_id = MessageId }, Rest); - {?PUBCOMP, <>} -> - <> = FrameBin, - wrap(Fixed, #mqtt_frame_publish { message_id = MessageId }, Rest); - {Subs, <>} - when Subs =:= ?SUBSCRIBE orelse Subs =:= ?UNSUBSCRIBE -> - 1 = Qos, - <> = FrameBin, - Topics = parse_topics(Subs, Rest1, []), - wrap(Fixed, #mqtt_frame_subscribe { message_id = MessageId, - topic_table = Topics }, Rest); - {Minimal, Rest} - when Minimal =:= ?DISCONNECT orelse Minimal =:= ?PINGREQ -> - Length = 0, - wrap(Fixed, Rest); - {_, TooShortBin} -> - {more, fun(BinMore) -> - parse_frame(<>, - Fixed, Length) - end} - end. - -parse_topics(_, <<>>, Topics) -> - Topics; -parse_topics(?SUBSCRIBE = Sub, Bin, Topics) -> - {Name, <<_:6, QoS:2, Rest/binary>>} = parse_utf(Bin), - parse_topics(Sub, Rest, [#mqtt_topic { name = Name, qos = QoS } | Topics]); -parse_topics(?UNSUBSCRIBE = Sub, Bin, Topics) -> - {Name, <>} = parse_utf(Bin), - parse_topics(Sub, Rest, [#mqtt_topic { name = Name } | Topics]). - -wrap(Fixed, Variable, Payload, Rest) -> - {ok, #mqtt_frame { variable = Variable, fixed = Fixed, payload = Payload }, Rest}. -wrap(Fixed, Variable, Rest) -> - {ok, #mqtt_frame { variable = Variable, fixed = Fixed }, Rest}. -wrap(Fixed, Rest) -> - {ok, #mqtt_frame { fixed = Fixed }, Rest}. - -parse_utf(Bin, 0) -> - {undefined, Bin}; -parse_utf(Bin, _) -> - parse_utf(Bin). - -parse_utf(<>) -> - {Str, Rest}. - -parse_msg(Bin, 0) -> - {undefined, Bin}; -parse_msg(<>, _) -> - {Msg, Rest}. - -bool(0) -> false; -bool(1) -> true. - -%% serialisation - -serialise(#mqtt_frame{ fixed = Fixed, - variable = Variable, - payload = Payload }) -> - serialise_variable(Fixed, Variable, serialise_payload(Payload)). - -serialise_payload(undefined) -> <<>>; -serialise_payload(B) when is_binary(B) -> B. - -serialise_variable(#mqtt_frame_fixed { type = ?CONNACK } = Fixed, - #mqtt_frame_connack { return_code = ReturnCode }, - <<>> = PayloadBin) -> - VariableBin = <>, - serialise_fixed(Fixed, VariableBin, PayloadBin); - -serialise_variable(#mqtt_frame_fixed { type = SubAck } = Fixed, - #mqtt_frame_suback { message_id = MessageId, - qos_table = Qos }, - <<>> = _PayloadBin) - when SubAck =:= ?SUBACK orelse SubAck =:= ?UNSUBACK -> - VariableBin = <>, - QosBin = << <> || Q <- Qos >>, - serialise_fixed(Fixed, VariableBin, QosBin); - -serialise_variable(#mqtt_frame_fixed { type = ?PUBLISH, - qos = Qos } = Fixed, - #mqtt_frame_publish { topic_name = TopicName, - message_id = MessageId }, - PayloadBin) -> - TopicBin = serialise_utf(TopicName), - MessageIdBin = case Qos of - 0 -> <<>>; - 1 -> <>; - 2 -> <> - end, - serialise_fixed(Fixed, <>, PayloadBin); - -serialise_variable(#mqtt_frame_fixed { type = ?PUBACK } = Fixed, - #mqtt_frame_publish { message_id = MessageId }, - PayloadBin) -> - MessageIdBin = <>, - serialise_fixed(Fixed, MessageIdBin, PayloadBin); - -serialise_variable(#mqtt_frame_fixed { type = ?PUBREC } = Fixed, - #mqtt_frame_publish{ message_id = MsgId}, - PayloadBin) -> - serialise_fixed(Fixed, <>, PayloadBin); - -serialise_variable(#mqtt_frame_fixed { type = ?PUBREL } = Fixed, - #mqtt_frame_publish{ message_id = MsgId}, - PayloadBin) -> - serialise_fixed(Fixed, <>, PayloadBin); - -serialise_variable(#mqtt_frame_fixed { type = ?PUBCOMP } = Fixed, - #mqtt_frame_publish{ message_id = MsgId}, - PayloadBin) -> - serialise_fixed(Fixed, <>, PayloadBin); - -serialise_variable(#mqtt_frame_fixed {} = Fixed, - undefined, - <<>> = _PayloadBin) -> - serialise_fixed(Fixed, <<>>, <<>>). - -serialise_fixed(#mqtt_frame_fixed{ type = Type, - dup = Dup, - qos = Qos, - retain = Retain }, VariableBin, PayloadBin) - when is_integer(Type) andalso ?CONNECT =< Type andalso Type =< ?DISCONNECT -> - Len = size(VariableBin) + size(PayloadBin), - true = (Len =< ?MAX_LEN), - LenBin = serialise_len(Len), - <>. - -serialise_utf(String) -> - StringBin = unicode:characters_to_binary(String), - Len = size(StringBin), - true = (Len =< 16#ffff), - <>. - -serialise_len(N) when N =< ?LOWBITS -> - <<0:1, N:7>>; -serialise_len(N) -> - <<1:1, (N rem ?HIGHBIT):7, (serialise_len(N div ?HIGHBIT))/binary>>. - -opt(undefined) -> ?RESERVED; -opt(false) -> 0; -opt(true) -> 1; -opt(X) when is_integer(X) -> X. - -protocol_name_approved(Ver, Name) -> - lists:member({Ver, Name}, ?PROTOCOL_NAMES). diff --git a/apps/emqtt/src/emqtt_http.erl b/apps/emqtt/src/emqtt_http.erl index 2458c4241..e6aa3ccec 100644 --- a/apps/emqtt/src/emqtt_http.erl +++ b/apps/emqtt/src/emqtt_http.erl @@ -1,5 +1,5 @@ %%------------------------------------------------------------------------------ -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,12 +22,10 @@ -module(emqtt_http). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -include("emqtt.hrl"). --include("emqtt_log.hrl"). - -import(proplists, [get_value/2, get_value/3]). -export([handle/1]). @@ -45,14 +43,11 @@ handle(Req) -> handle('POST', "/mqtt/publish", Req) -> Params = mochiweb_request:parse_post(Req), - ?INFO("~p~n", [Params]), + lager:info("~p~n", [Params]), Topic = list_to_binary(get_value("topic", Params)), Message = list_to_binary(get_value("message", Params)), - emqtt_pubsub:publish(#mqtt_msg { - retain = 0, - qos = ?QOS_0, + emqtt_pubsub:publish(#mqtt_message { topic = Topic, - dup = 0, payload = Message }), Req:ok({"text/plan", "ok"}); diff --git a/apps/emqtt/src/emqtt_keepalive.erl b/apps/emqtt/src/emqtt_keepalive.erl new file mode 100644 index 000000000..7dd7be0f8 --- /dev/null +++ b/apps/emqtt/src/emqtt_keepalive.erl @@ -0,0 +1,71 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + +-module(emqtt_keepalive). + +-author('feng@emqtt.io'). + +-export([new/3, resume/1, cancel/1]). + +-record(keepalive, {socket, recv_oct, timeout_sec, timeout_msg, timer_ref}). + +%% +%% @doc create a keepalive. +%% +new(Socket, TimeoutSec, TimeoutMsg) when TimeoutSec > 0 -> + {ok, [{recv_oct, RecvOct}]} = inet:getstat(Socket, [recv_oct]), + Ref = erlang:send_after(TimeoutSec*1000, self(), TimeoutMsg), + #keepalive { socket = Socket, + recv_oct = RecvOct, + timeout_sec = TimeoutSec, + timeout_msg = TimeoutMsg, + timer_ref = Ref }. + +%% +%% @doc try to resume keepalive, called when timeout. +%% +resume(KeepAlive = #keepalive { socket = Socket, + recv_oct = RecvOct, + timeout_sec = TimeoutSec, + timeout_msg = TimeoutMsg, + timer_ref = Ref }) -> + {ok, [{recv_oct, NewRecvOct}]} = inet:getstat(Socket, [recv_oct]), + if + NewRecvOct =:= RecvOct -> + timeout; + true -> + %need? + cancel(Ref), + NewRef = erlang:send_after(TimeoutSec*1000, self(), TimeoutMsg), + {resumed, KeepAlive#keepalive { recv_oct = NewRecvOct, timer_ref = NewRef }} + end. + +%% +%% @doc cancel keepalive +%% +cancel(#keepalive { timer_ref = Ref }) -> + cancel(Ref); +cancel(undefined) -> + undefined; +cancel(Ref) -> + catch erlang:cancel_timer(Ref). + diff --git a/apps/emqtt/src/emqtt_message.erl b/apps/emqtt/src/emqtt_message.erl new file mode 100644 index 000000000..d8dde9814 --- /dev/null +++ b/apps/emqtt/src/emqtt_message.erl @@ -0,0 +1,133 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + +-module(emqtt_message). + +-author('feng@emqtt.io'). + +-include("emqtt.hrl"). + +-include("emqtt_packet.hrl"). + +-export([from_packet/1, to_packet/1]). + +-export([set_flag/1, set_flag/2, unset_flag/1, unset_flag/2]). + +-export([dump/1]). + +%%---------------------------------------------------------------------------- + +-ifdef(use_specs). + +-spec( from_packet( mqtt_packet() ) -> mqtt_message() | undefined ). + +-spec( to_packet( mqtt_message() ) -> mqtt_packet() ). + +-sepc( set_flag(atom(), mqtt_message() ) -> mqtt_message(). + +-sepc( unset_flag(atom(), mqtt_message() ) -> mqtt_message(). + +-endif. + +%%---------------------------------------------------------------------------- + +%% +%% @doc message from packet +%% +from_packet(#mqtt_packet{ header = #mqtt_packet_header{ type = ?PUBLISH, + retain = Retain, + qos = Qos, + dup = Dup }, + variable = #mqtt_packet_publish{ topic_name = Topic, + packet_id = PacketId }, + payload = Payload }) -> + #mqtt_message{ msgid = PacketId, + qos = Qos, + retain = Retain, + dup = Dup, + topic = Topic, + payload = Payload }; + +from_packet(#mqtt_packet_connect{ will_flag = false }) -> + undefined; + +from_packet(#mqtt_packet_connect{ will_retain = Retain, + will_qos = Qos, + will_topic = Topic, + will_msg = Msg }) -> + #mqtt_message{ retain = Retain, + qos = Qos, + topic = Topic, + dup = false, + payload = Msg }. + +%% +%% @doc message to packet +%% +to_packet(#mqtt_message{ msgid = MsgId, + qos = Qos, + retain = Retain, + dup = Dup, + topic = Topic, + payload = Payload }) -> + + PacketId = if + Qos =:= ?QOS_0 -> undefined; + true -> MsgId + end, + + #mqtt_packet{ header = #mqtt_packet_header { type = ?PUBLISH, + qos = Qos, + retain = Retain, + dup = Dup }, + variable = #mqtt_packet_publish { topic_name = Topic, + packet_id = PacketId }, + payload = Payload }. + +%% +%% @doc set dup, retain flag +%% +set_flag(Msg) -> + Msg#mqtt_message{dup = true, retain = true}. +set_flag(dup, Msg = #mqtt_message{dup = false}) -> + Msg#mqtt_message{dup = true}; +set_flag(retain, Msg = #mqtt_message{retain = false}) -> + Msg#mqtt_message{retain = true}; +set_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. + +unset_flag(Msg) -> + Msg#mqtt_message{dup = false, retain = false}. +unset_flag(dup, Msg = #mqtt_message{dup = true}) -> + Msg#mqtt_message{dup = false}; +unset_flag(retain, Msg = #mqtt_message{retain = true}) -> + Msg#mqtt_message{retain = false}; +unset_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. + + +%% +%% @doc dump message +%% +dump(#mqtt_message{msgid= MsgId, qos = Qos, retain = Retain, dup = Dup, topic = Topic}) -> + io_lib:format("Message(MsgId=~p, Qos=~p, Retain=~s, Dup=~s, Topic=~s)", + [ MsgId, Qos, Retain, Dup, Topic ]). + + diff --git a/apps/emqtt/src/emqtt_monitor.erl b/apps/emqtt/src/emqtt_monitor.erl index 608dc2701..934aff4fb 100644 --- a/apps/emqtt/src/emqtt_monitor.erl +++ b/apps/emqtt/src/emqtt_monitor.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,9 +22,7 @@ -module(emqtt_monitor). --author('feng@slimchat.io'). - --include("emqtt_log.hrl"). +-author('feng@emqtt.io'). -behavior(gen_server). @@ -50,7 +48,6 @@ start_link() -> %%-------------------------------------------------------------------- init([]) -> erlang:system_monitor(self(), [{long_gc, 5000}, {large_heap, 1000000}, busy_port]), - ?INFO("monitor is started...[ok]", []), {ok, #state{}}. %%-------------------------------------------------------------------- %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | @@ -62,7 +59,7 @@ init([]) -> %% Description: Handling call messages %%-------------------------------------------------------------------- handle_call(Request, _From, State) -> - ?ERROR("unexpected request: ~p", [Request]), + lager:error("unexpected request: ~p", [Request]), {reply, {error, unexpected_request}, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | @@ -71,7 +68,7 @@ handle_call(Request, _From, State) -> %% Description: Handling cast messages %%-------------------------------------------------------------------- handle_cast(Msg, State) -> - ?ERROR("unexpected msg: ~p", [Msg]), + lager:error("unexpected msg: ~p", [Msg]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | @@ -80,22 +77,22 @@ handle_cast(Msg, State) -> %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- handle_info({monitor, GcPid, long_gc, Info}, State) -> - ?ERROR("long_gc: gcpid = ~p, ~p ~n ~p", [GcPid, process_info(GcPid, + lager:error("long_gc: gcpid = ~p, ~p ~n ~p", [GcPid, process_info(GcPid, [registered_name, memory, message_queue_len,heap_size,total_heap_size]), Info]), {noreply, State}; handle_info({monitor, GcPid, large_heap, Info}, State) -> - ?ERROR("large_heap: gcpid = ~p,~p ~n ~p", [GcPid, process_info(GcPid, + lager:error("large_heap: gcpid = ~p,~p ~n ~p", [GcPid, process_info(GcPid, [registered_name, memory, message_queue_len,heap_size,total_heap_size]), Info]), {noreply, State}; handle_info({monitor, SusPid, busy_port, Port}, State) -> - ?ERROR("busy_port: suspid = ~p, port = ~p", [process_info(SusPid, + lager:error("busy_port: suspid = ~p, port = ~p", [process_info(SusPid, [registered_name, memory, message_queue_len,heap_size,total_heap_size]), Port]), {noreply, State}; handle_info(Info, State) -> - ?ERROR("unexpected info: ~p", [Info]), + lager:error("unexpected info: ~p", [Info]), {noreply, State}. %%-------------------------------------------------------------------- diff --git a/apps/emqtt/src/emqtt_net.erl b/apps/emqtt/src/emqtt_net.erl index e6620c3b7..21d98d704 100644 --- a/apps/emqtt/src/emqtt_net.erl +++ b/apps/emqtt/src/emqtt_net.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,11 +22,11 @@ -module(emqtt_net). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -export([tcp_name/3, tcp_host/1, getopts/2, setopts/2, getaddr/2, port_to_listeners/1]). --export([connection_string/2]). +-export([peername/1, sockname/1, peer_string/1, connection_string/2]). -include_lib("kernel/include/inet.hrl"). @@ -196,6 +196,14 @@ setopts(Sock, Options) when is_port(Sock) -> sockname(Sock) when is_port(Sock) -> inet:sockname(Sock). +peer_string(Sock) -> + case peername(Sock) of + {ok, {Addr, Port}} -> + {ok, lists:flatten(io_lib:format("~s:~p", [maybe_ntoab(Addr), Port]))}; + Error -> + Error + end. + peername(Sock) when is_port(Sock) -> inet:peername(Sock). ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> diff --git a/apps/emqtt/src/emqtt_packet.erl b/apps/emqtt/src/emqtt_packet.erl new file mode 100644 index 000000000..13bc32d61 --- /dev/null +++ b/apps/emqtt/src/emqtt_packet.erl @@ -0,0 +1,353 @@ +%%------------------------------------------------------------------------------ +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ +%% +%% The Original Code is from RabbitMQ. +%% + +-module(emqtt_packet). + +-include("emqtt_packet.hrl"). + +-export([initial_state/0]). + +-export([parse/2, serialise/1]). + +-export([dump/1]). + +-define(MAX_LEN, 16#fffffff). +-define(HIGHBIT, 2#10000000). +-define(LOWBITS, 2#01111111). + +initial_state() -> none. + +parse(<<>>, none) -> + {more, fun(Bin) -> parse(Bin, none) end}; +parse(<>, none) -> + parse_remaining_len(Rest, #mqtt_packet_header{type = PacketType, + dup = bool(Dup), + qos = QoS, + retain = bool(Retain) }); +parse(Bin, Cont) -> Cont(Bin). + +parse_remaining_len(<<>>, Header) -> + {more, fun(Bin) -> parse_remaining_len(Bin, Header) end}; +parse_remaining_len(Rest, Header) -> + parse_remaining_len(Rest, Header, 1, 0). + +parse_remaining_len(_Bin, _Header, _Multiplier, Length) + when Length > ?MAX_LEN -> + {error, invalid_mqtt_frame_len}; +parse_remaining_len(<<>>, Header, Multiplier, Length) -> + {more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length) end}; +parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value) -> + parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier); +parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value) -> + parse_frame(Rest, Header, Value + Len * Multiplier). + +parse_frame(Bin, #mqtt_packet_header{ type = Type, + qos = Qos } = Header, Length) -> + case {Type, Bin} of + {?CONNECT, <>} -> + {ProtoName, Rest1} = parse_utf(FrameBin), + <> = Rest1, + <> = Rest2, + {ClientId, Rest4} = parse_utf(Rest3), + {WillTopic, Rest5} = parse_utf(Rest4, WillFlag), + {WillMsg, Rest6} = parse_msg(Rest5, WillFlag), + {UserName, Rest7} = parse_utf(Rest6, UsernameFlag), + {PasssWord, <<>>} = parse_utf(Rest7, PasswordFlag), + case protocol_name_approved(ProtoVersion, ProtoName) of + true -> + wrap(Header, + #mqtt_packet_connect{ + proto_ver = ProtoVersion, + proto_name = ProtoName, + will_retain = bool(WillRetain), + will_qos = WillQos, + will_flag = bool(WillFlag), + clean_sess = bool(CleanSession), + keep_alive = KeepAlive, + client_id = ClientId, + will_topic = WillTopic, + will_msg = WillMsg, + username = UserName, + password = PasssWord}, Rest); + false -> + {error, protocol_header_corrupt} + end; + {?PUBLISH, <>} -> + {TopicName, Rest1} = parse_utf(FrameBin), + {PacketId, Payload} = case Qos of + 0 -> {undefined, Rest1}; + _ -> <> = Rest1, + {Id, R} + end, + wrap(Header, #mqtt_packet_publish {topic_name = TopicName, + packet_id = PacketId }, + Payload, Rest); + {?PUBACK, <>} -> + <> = FrameBin, + wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); + {?PUBREC, <>} -> + <> = FrameBin, + wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); + {?PUBREL, <>} -> + 1 = Qos, + <> = FrameBin, + wrap(Header, #mqtt_packet_puback{ packet_id = PacketId }, Rest); + {?PUBCOMP, <>} -> + <> = FrameBin, + wrap(Header, #mqtt_packet_puback{ packet_id = PacketId }, Rest); + {Subs, <>} + when Subs =:= ?SUBSCRIBE orelse Subs =:= ?UNSUBSCRIBE -> + 1 = Qos, + <> = FrameBin, + Topics = parse_topics(Subs, Rest1, []), + wrap(Header, #mqtt_packet_subscribe { packet_id = PacketId, + topic_table = Topics }, Rest); + {Minimal, Rest} + when Minimal =:= ?DISCONNECT orelse Minimal =:= ?PINGREQ -> + Length = 0, + wrap(Header, Rest); + {_, TooShortBin} -> + {more, fun(BinMore) -> + parse_frame(<>, + Header, Length) + end} + end. + +parse_topics(_, <<>>, Topics) -> + Topics; +parse_topics(?SUBSCRIBE = Sub, Bin, Topics) -> + {Name, <<_:6, QoS:2, Rest/binary>>} = parse_utf(Bin), + parse_topics(Sub, Rest, [#mqtt_topic { name = Name, qos = QoS } | Topics]); +parse_topics(?UNSUBSCRIBE = Sub, Bin, Topics) -> + {Name, <>} = parse_utf(Bin), + parse_topics(Sub, Rest, [#mqtt_topic { name = Name } | Topics]). + +wrap(Header, Variable, Payload, Rest) -> + {ok, #mqtt_packet{ header = Header, variable = Variable, payload = Payload }, Rest}. +wrap(Header, Variable, Rest) -> + {ok, #mqtt_packet { header = Header, variable = Variable }, Rest}. +wrap(Header, Rest) -> + {ok, #mqtt_packet { header = Header }, Rest}. + +parse_utf(Bin, 0) -> + {undefined, Bin}; +parse_utf(Bin, _) -> + parse_utf(Bin). + +parse_utf(<>) -> + {Str, Rest}. + +parse_msg(Bin, 0) -> + {undefined, Bin}; +parse_msg(<>, _) -> + {Msg, Rest}. + +bool(0) -> false; +bool(1) -> true. + +%% serialisation + +serialise(#mqtt_packet{ header = Header, + variable = Variable, + payload = Payload }) -> + serialise_header(Header, + serialise_variable(Header, Variable, + serialise_payload(Payload))). + +serialise_payload(undefined) -> <<>>; +serialise_payload(B) when is_binary(B) -> B. + +serialise_variable(#mqtt_packet_header { type = ?CONNACK }, + #mqtt_packet_connack { ack_flags = AckFlags, + return_code = ReturnCode }, + <<>> = PayloadBin) -> + VariableBin = <>, + {VariableBin, PayloadBin}; + +serialise_variable(#mqtt_packet_header { type = SubAck }, + #mqtt_packet_suback { packet_id = PacketId, + qos_table = Qos }, + <<>> = _PayloadBin) + when SubAck =:= ?SUBACK orelse SubAck =:= ?UNSUBACK -> + VariableBin = <>, + QosBin = << <> || Q <- Qos >>, + {VariableBin, QosBin}; + +serialise_variable(#mqtt_packet_header { type = ?PUBLISH, + qos = Qos }, + #mqtt_packet_publish { topic_name = TopicName, + packet_id = PacketId }, + PayloadBin) -> + TopicBin = serialise_utf(TopicName), + PacketIdBin = case Qos of + 0 -> <<>>; + 1 -> <>; + 2 -> <> + end, + {<>, PayloadBin}; + +serialise_variable(#mqtt_packet_header { type = PubAck }, + #mqtt_packet_puback { packet_id = PacketId }, + <<>> = _Payload) + when PubAck =:= ?PUBACK; PubAck =:= ?PUBREC; + PubAck =:= ?PUBREL; PubAck =:= ?PUBCOMP -> + {<>, <<>>}; + +serialise_variable(#mqtt_packet_header { }, + undefined, + <<>> = _PayloadBin) -> + {<<>>, <<>>}. + +serialise_header(#mqtt_packet_header{ type = Type, + dup = Dup, + qos = Qos, + retain = Retain }, + {VariableBin, PayloadBin}) + when is_integer(Type) andalso ?CONNECT =< Type andalso Type =< ?DISCONNECT -> + Len = size(VariableBin) + size(PayloadBin), + true = (Len =< ?MAX_LEN), + LenBin = serialise_len(Len), + <>. + +serialise_utf(String) -> + StringBin = unicode:characters_to_binary(String), + Len = size(StringBin), + true = (Len =< 16#ffff), + <>. + +serialise_len(N) when N =< ?LOWBITS -> + <<0:1, N:7>>; +serialise_len(N) -> + <<1:1, (N rem ?HIGHBIT):7, (serialise_len(N div ?HIGHBIT))/binary>>. + +opt(undefined) -> ?RESERVED; +opt(false) -> 0; +opt(true) -> 1; +opt(X) when is_integer(X) -> X. + +protocol_name_approved(Ver, Name) -> + lists:member({Ver, Name}, ?PROTOCOL_NAMES). + +dump(#mqtt_packet{header = Header, variable = Variable, payload = Payload}) when + Payload =:= undefined orelse Payload =:= <<>> -> + dump_header(Header, dump_variable(Variable)); + +dump(#mqtt_packet{header = Header, variable = Variable, payload = Payload}) -> + dump_header(Header, dump_variable(Variable, Payload)). + +dump_header(#mqtt_packet_header{type = Type, dup = Dup, qos = QoS, retain = Retain}, S) -> + S1 = + if + S == undefined -> <<>>; + true -> [", ", S] + end, + io_lib:format("~s(Qos=~p, Retain=~s, Dup=~s~s)", [dump_type(Type), QoS, Retain, Dup, S1]). + +dump_variable( #mqtt_packet_connect { + proto_ver = ProtoVer, + proto_name = ProtoName, + will_retain = WillRetain, + will_qos = WillQoS, + will_flag = WillFlag, + clean_sess = CleanSess, + keep_alive = KeepAlive, + client_id = ClientId, + will_topic = WillTopic, + will_msg = WillMsg, + username = Username, + password = Password} ) -> + Format = "ClientId=~s, ProtoName=~s, ProtoVsn=~p, CleanSess=~s, KeepAlive=~p, Username=~s, Password=~s", + Args = [ClientId, ProtoName, ProtoVer, CleanSess, KeepAlive, Username, dump_password(Password)], + {Format1, Args1} = if + WillFlag -> { Format ++ ", Will(Qos=~p, Retain=~s, Topic=~s, Msg=~s)", + Args ++ [ WillQoS, WillRetain, WillTopic, WillMsg ] }; + true -> {Format, Args} + end, + io_lib:format(Format1, Args1); + +dump_variable( #mqtt_packet_connack { + ack_flags = AckFlags, + return_code = ReturnCode } ) -> + io_lib:format("AckFlags=~p, RetainCode=~p", [AckFlags, ReturnCode]); + +dump_variable( #mqtt_packet_publish { + topic_name = TopicName, + packet_id = PacketId} ) -> + io_lib:format("TopicName=~s, PacketId=~p", [TopicName, PacketId]); + +dump_variable( #mqtt_packet_puback { + packet_id = PacketId } ) -> + io_lib:format("PacketId=~p", [PacketId]); + +dump_variable( #mqtt_packet_subscribe { + packet_id = PacketId, + topic_table = TopicTable }) -> + L = [{Name, QoS} || #mqtt_topic{name = Name, qos = QoS} <- TopicTable], + io_lib:format("PacketId=~p, TopicTable=~p", [PacketId, L]); + +dump_variable( #mqtt_packet_suback { + packet_id = PacketId, + qos_table = QosTable} ) -> + io_lib:format("PacketId=~p, QosTable=~p", [PacketId, QosTable]); + +dump_variable(PacketId) when is_integer(PacketId) -> + io_lib:format("PacketId=~p", [PacketId]); + +dump_variable(undefined) -> undefined. + +dump_variable(undefined, undefined) -> + undefined; +dump_variable(undefined, <<>>) -> + undefined; +dump_variable(Variable, Payload) -> + io_lib:format("~s, Payload=~p", [dump_variable(Variable), Payload]). + +dump_password(undefined) -> undefined; +dump_password(_) -> <<"******">>. + +dump_type(?CONNECT) -> "CONNECT"; +dump_type(?CONNACK) -> "CONNACK"; +dump_type(?PUBLISH) -> "PUBLISH"; +dump_type(?PUBACK) -> "PUBACK"; +dump_type(?PUBREC) -> "PUBREC"; +dump_type(?PUBREL) -> "PUBREL"; +dump_type(?PUBCOMP) -> "PUBCOMP"; +dump_type(?SUBSCRIBE) -> "SUBSCRIBE"; +dump_type(?SUBACK) -> "SUBACK"; +dump_type(?UNSUBSCRIBE) -> "UNSUBSCRIBE"; +dump_type(?UNSUBACK) -> "UNSUBACK"; +dump_type(?PINGREQ) -> "PINGREQ"; +dump_type(?PINGRESP) -> "PINGRESP"; +dump_type(?DISCONNECT) -> "DISCONNECT". + diff --git a/apps/emqtt/src/emqtt_protocol.erl b/apps/emqtt/src/emqtt_protocol.erl index 468b70a22..d0857c554 100644 --- a/apps/emqtt/src/emqtt_protocol.erl +++ b/apps/emqtt/src/emqtt_protocol.erl @@ -1,5 +1,356 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ -module(emqtt_protocol). --include("emqtt_frame.hrl"). +-include("emqtt.hrl"). + +-include("emqtt_packet.hrl"). + +%% ------------------------------------------------------------------ +%% API Function Exports +%% ------------------------------------------------------------------ + +-export([initial_state/2, client_id/1]). + +-export([handle_packet/2, send_message/2, send_packet/2, redeliver/2, shutdown/2]). + +-export([info/1]). + + +%% ------------------------------------------------------------------ +%% Protocol State +%% ------------------------------------------------------------------ +-record(proto_state, { + socket, + peer_name, + connected = false, %received CONNECT action? + proto_vsn, + proto_name, + %packet_id, + client_id, + clean_sess, + session, %% session state or session pid + will_msg +}). + +%%---------------------------------------------------------------------------- + +-ifdef(use_specs). + +-type(proto_state() :: #proto_state{}). + +-spec(send_message({pid() | tuple(), mqtt_message()}, proto_state()) -> {ok, proto_state()}). + +-spec(handle_packet(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, any()}). + +-endif. + +%%---------------------------------------------------------------------------- + +-define(PACKET_TYPE(Packet, Type), + Packet = #mqtt_packet { header = #mqtt_packet_header { type = Type }}). + +-define(PUBACK_PACKET(PacketId), #mqtt_packet_puback { packet_id = PacketId }). + +initial_state(Socket, Peername) -> + #proto_state{ + socket = Socket, + peer_name = Peername + }. + +client_id(#proto_state { client_id = ClientId }) -> ClientId. + +%%SHOULD be registered in emqtt_cm +info(#proto_state{ proto_vsn = ProtoVsn, + proto_name = ProtoName, + client_id = ClientId, + clean_sess = CleanSess, + will_msg = WillMsg }) -> + [ {proto_vsn, ProtoVsn}, + {proto_name, ProtoName}, + {client_id, ClientId}, + {clean_sess, CleanSess}, + {will_msg, WillMsg} ]. + + +%%CONNECT – Client requests a connection to a Server + +%%A Client can only send the CONNECT Packet once over a Network Connection. +handle_packet(?PACKET_TYPE(Packet, ?CONNECT), State = #proto_state{connected = false}) -> + handle_packet(?CONNECT, Packet, State#proto_state{connected = true}); + +handle_packet(?PACKET_TYPE(_Packet, ?CONNECT), State = #proto_state{connected = true}) -> + {error, protocol_bad_connect, State}; + +%%Received other packets when CONNECT not arrived. +handle_packet(_Packet, State = #proto_state{connected = false}) -> + {error, protocol_not_connected, State}; + +handle_packet(?PACKET_TYPE(Packet, Type), + State = #proto_state { peer_name = PeerName, client_id = ClientId }) -> + lager:info("RECV from ~s@~s: ~s", [ClientId, PeerName, emqtt_packet:dump(Packet)]), + case validate_packet(Packet) of + ok -> + handle_packet(Type, Packet, State); + {error, Reason} -> + {error, Reason, State} + end. + +handle_packet(?CONNECT, Packet = #mqtt_packet { + variable = #mqtt_packet_connect { + username = Username, + password = Password, + clean_sess = CleanSess, + keep_alive = KeepAlive, + client_id = ClientId } = Var }, + State = #proto_state{ peer_name = PeerName } ) -> + lager:info("RECV from ~s@~s: ~s", [ClientId, PeerName, emqtt_packet:dump(Packet)]), + {ReturnCode1, State1} = + case validate_connect(Var) of + ?CONNACK_ACCEPT -> + case emqtt_auth:check(Username, Password) of + true -> + ClientId1 = clientid(ClientId, State), + start_keepalive(KeepAlive), + emqtt_cm:register(ClientId1, self()), + {?CONNACK_ACCEPT, State#proto_state{ will_msg = willmsg(Var), + clean_sess = CleanSess, + client_id = ClientId1 }}; + false -> + lager:error("~s@~s: username '~s' login failed - no credentials", [ClientId, PeerName, Username]), + {?CONNACK_CREDENTIALS, State#proto_state{client_id = ClientId}} + end; + ReturnCode -> + {ReturnCode, State#proto_state{client_id = ClientId}} + end, + send_packet( #mqtt_packet { + header = #mqtt_packet_header { type = ?CONNACK }, + variable = #mqtt_packet_connack{ return_code = ReturnCode1 }}, State1 ), + %%Starting session + {ok, Session} = emqtt_session:start({CleanSess, ClientId, self()}), + {ok, State1#proto_state { session = Session }}; + +handle_packet(?PUBLISH, Packet = #mqtt_packet { + header = #mqtt_packet_header {qos = ?QOS_0}}, + State = #proto_state{session = Session}) -> + emqtt_session:publish(Session, {?QOS_0, emqtt_message:from_packet(Packet)}), + {ok, State}; + +handle_packet(?PUBLISH, Packet = #mqtt_packet { + header = #mqtt_packet_header { qos = ?QOS_1 }, + variable = #mqtt_packet_publish{packet_id = PacketId }}, + State = #proto_state { session = Session }) -> + emqtt_session:publish(Session, {?QOS_1, emqtt_message:from_packet(Packet)}), + send_packet( make_packet(?PUBACK, PacketId), State); + +handle_packet(?PUBLISH, Packet = #mqtt_packet { + header = #mqtt_packet_header { qos = ?QOS_2 }, + variable = #mqtt_packet_publish { packet_id = PacketId } }, + State = #proto_state { session = Session }) -> + NewSession = emqtt_session:publish(Session, {?QOS_2, emqtt_message:from_packet(Packet)}), + send_packet( make_packet(?PUBREC, PacketId), State#proto_state {session = NewSession} ); + +handle_packet(Puback, #mqtt_packet{variable = ?PUBACK_PACKET(PacketId) }, + State = #proto_state { session = Session }) + when Puback >= ?PUBACK andalso Puback =< ?PUBCOMP -> + + NewSession = emqtt_session:puback(Session, {Puback, PacketId}), + NewState = State#proto_state {session = NewSession}, + if + Puback =:= ?PUBREC -> + send_packet( make_packet(?PUBREL, PacketId), NewState); + Puback =:= ?PUBREL -> + send_packet( make_packet(?PUBCOMP, PacketId), NewState); + true -> + ok + end, + {ok, NewState}; + +handle_packet(?SUBSCRIBE, #mqtt_packet { + variable = #mqtt_packet_subscribe{ + packet_id = PacketId, + topic_table = TopicTable}, + payload = undefined}, + State = #proto_state { session = Session } ) -> + + Topics = [{Name, Qos} || #mqtt_topic{name=Name, qos=Qos} <- TopicTable], + {ok, NewSession, GrantedQos} = emqtt_session:subscribe(Session, Topics), + send_packet(#mqtt_packet { header = #mqtt_packet_header { type = ?SUBACK }, + variable = #mqtt_packet_suback{ packet_id = PacketId, + qos_table = GrantedQos }}, + State#proto_state{ session = NewSession }); + +handle_packet(?UNSUBSCRIBE, #mqtt_packet { + variable = #mqtt_packet_subscribe{ + packet_id = PacketId, + topic_table = Topics }, + payload = undefined}, + State = #proto_state{session = Session}) -> + {ok, NewSession} = emqtt_session:unsubscribe(Session, [Name || #mqtt_topic{ name = Name } <- Topics]), + send_packet(#mqtt_packet { header = #mqtt_packet_header {type = ?UNSUBACK }, + variable = #mqtt_packet_suback{packet_id = PacketId }}, + State#proto_state { session = NewSession } ); + +handle_packet(?PINGREQ, #mqtt_packet{}, State) -> + send_packet(make_packet(?PINGRESP), State); + +handle_packet(?DISCONNECT, #mqtt_packet{}, State) -> + %%TODO: how to handle session? + % clean willmsg + {stop, normal, State#proto_state{will_msg = undefined}}. + +make_packet(Type) when Type >= ?CONNECT andalso Type =< ?DISCONNECT -> + #mqtt_packet{ header = #mqtt_packet_header { type = Type } }. + +make_packet(PubAck, PacketId) when PubAck >= ?PUBACK andalso PubAck =< ?PUBCOMP -> + #mqtt_packet { header = #mqtt_packet_header { type = PubAck, qos = puback_qos(PubAck) }, + variable = #mqtt_packet_puback { packet_id = PacketId}}. + +puback_qos(?PUBACK) -> ?QOS_0; +puback_qos(?PUBREC) -> ?QOS_0; +puback_qos(?PUBREL) -> ?QOS_1; +puback_qos(?PUBCOMP) -> ?QOS_0. + +%% qos0 message +send_message({_From, Message = #mqtt_message{ qos = ?QOS_0 }}, State) -> + send_packet(emqtt_message:to_packet(Message), State); + +%% message from session +send_message({_From = SessPid, Message}, State = #proto_state{session = SessPid}) when is_pid(SessPid) -> + send_packet(emqtt_message:to_packet(Message), State); + +%% message(qos1, qos2) not from session +send_message({_From, Message = #mqtt_message{ qos = Qos }}, State = #proto_state{ session = Session }) + when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> + {Message1, NewSession} = emqtt_session:store(Session, Message), + send_packet(emqtt_message:to_packet(Message1), State#proto_state{session = NewSession}). + +send_packet(Packet, State = #proto_state{socket = Sock, peer_name = PeerName, client_id = ClientId}) -> + lager:info("SENT to ~s@~s: ~s", [ClientId, PeerName, emqtt_packet:dump(Packet)]), + Data = emqtt_packet:serialise(Packet), + lager:debug("SENT to ~s: ~p", [PeerName, Data]), + %%FIXME Later... + erlang:port_command(Sock, Data), + {ok, State}. + +%% +%% @doc redeliver PUBREL PacketId +%% +redeliver({?PUBREL, PacketId}, State) -> + send_packet( make_packet(?PUBREL, PacketId), State). + +shutdown(Error, #proto_state{peer_name = PeerName, client_id = ClientId, will_msg = WillMsg}) -> + send_willmsg(WillMsg), + try_unregister(ClientId, self()), + lager:info("Protocol ~s@~s Shutdown: ~p", [ClientId, PeerName, Error]), + ok. + +willmsg(Packet) when is_record(Packet, mqtt_packet_connect) -> + emqtt_message:from_packet(Packet). + +clientid(<<>>, #proto_state{peer_name = PeerName}) -> + <<"eMQTT/", (base64:encode(PeerName))/binary>>; + +clientid(ClientId, _State) -> ClientId. + +%%---------------------------------------------------------------------------- + +send_willmsg(undefined) -> ignore; +%%TODO:should call session... +send_willmsg(WillMsg) -> emqtt_router:route(WillMsg). + +start_keepalive(0) -> ignore; +start_keepalive(Sec) when Sec > 0 -> + self() ! {keepalive, start, round(Sec * 1.5)}. + +%%---------------------------------------------------------------------------- +%% Validate Packets +%%---------------------------------------------------------------------------- +validate_connect( Connect = #mqtt_packet_connect{} ) -> + case validate_protocol(Connect) of + true -> + case validate_clientid(Connect) of + true -> + ?CONNACK_ACCEPT; + false -> + ?CONNACK_INVALID_ID + end; + false -> + ?CONNACK_PROTO_VER + end. + +validate_protocol(#mqtt_packet_connect { proto_ver = Ver, proto_name = Name }) -> + lists:member({Ver, Name}, ?PROTOCOL_NAMES). + +validate_clientid(#mqtt_packet_connect { client_id = ClientId }) + when ( size(ClientId) >= 1 ) andalso ( size(ClientId) =< ?MAX_CLIENTID_LEN ) -> + true; + +%% MQTT3.1.1 allow null clientId. +validate_clientid(#mqtt_packet_connect { proto_ver =?MQTT_PROTO_V311, client_id = ClientId }) + when size(ClientId) =:= 0 -> + true; + +validate_clientid(#mqtt_packet_connect { proto_ver = Ver, clean_sess = CleanSess, client_id = ClientId }) -> + lager:warning("Invalid ClientId: ~s, ProtoVer: ~p, CleanSess: ~s", [ClientId, Ver, CleanSess]), + false. + +validate_packet(#mqtt_packet { header = #mqtt_packet_header { type = ?PUBLISH }, + variable = #mqtt_packet_publish{ topic_name = Topic }}) -> + case emqtt_topic:validate({publish, Topic}) of + true -> ok; + false -> lager:warning("Error publish topic: ~p", [Topic]), {error, badtopic} + end; + +validate_packet(#mqtt_packet { header = #mqtt_packet_header { type = ?SUBSCRIBE }, + variable = #mqtt_packet_subscribe{topic_table = Topics }}) -> + + validate_topics(subscribe, Topics); + +validate_packet(#mqtt_packet{ header = #mqtt_packet_header { type = ?UNSUBSCRIBE }, + variable = #mqtt_packet_subscribe{ topic_table = Topics }}) -> + + validate_topics(unsubscribe, Topics); + +validate_packet(_Packet) -> + ok. + +validate_topics(Type, []) when Type =:= subscribe orelse Type =:= unsubscribe -> + lager:error("Empty Topics!"), + {error, empty_topics}; + +validate_topics(Type, Topics) when Type =:= subscribe orelse Type =:= unsubscribe -> + ErrTopics = [Topic || #mqtt_topic{name=Topic, qos=Qos} <- Topics, + not (emqtt_topic:validate({Type, Topic}) and validate_qos(Qos))], + case ErrTopics of + [] -> ok; + _ -> lager:error("Error Topics: ~p", [ErrTopics]), {error, badtopic} + end. + +validate_qos(undefined) -> true; +validate_qos(Qos) when Qos =< ?QOS_2 -> true; +validate_qos(_) -> false. + +try_unregister(undefined, _) -> ok; +try_unregister(ClientId, _) -> emqtt_cm:unregister(ClientId, self()). diff --git a/apps/emqtt/src/emqtt_pubsub.erl b/apps/emqtt/src/emqtt_pubsub.erl index f4cf7c451..c05e344f1 100644 --- a/apps/emqtt/src/emqtt_pubsub.erl +++ b/apps/emqtt/src/emqtt_pubsub.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,12 +22,10 @@ -module(emqtt_pubsub). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -include("emqtt.hrl"). --include("emqtt_log.hrl"). - -include("emqtt_topic.hrl"). -include_lib("stdlib/include/qlc.hrl"). @@ -62,6 +60,20 @@ terminate/2, code_change/3]). +%%---------------------------------------------------------------------------- + +-ifdef(use_specs). + +-spec topics() -> list(topic()). + +-spec subscribe({binary(), mqtt_qos()} | list(), pid()) -> {ok, list(mqtt_qos())}. + +-spec unsubscribe(binary() | list(binary()), pid()) -> ok. + +-endif. + +%%---------------------------------------------------------------------------- + -record(state, {}). %% ------------------------------------------------------------------ @@ -77,32 +89,35 @@ start_link() -> %% %% @doc All topics %% --spec topics() -> list(topic()). topics() -> mnesia:dirty_all_keys(topic). %% -%% @doc Subscribe Topic +%% @doc Subscribe Topic or Topics %% --spec subscribe({Topic :: binary(), Qos :: qos()}, SubPid :: pid()) -> any(). subscribe({Topic, Qos}, SubPid) when is_binary(Topic) and is_pid(SubPid) -> - gen_server:call(?SERVER, {subscribe, {Topic, Qos}, SubPid}). + subscribe([{Topic, Qos}], SubPid); + +subscribe(Topics, SubPid) when is_list(Topics) and is_pid(SubPid) -> + gen_server:call(?SERVER, {subscribe, Topics, SubPid}). %% -%% @doc Unsubscribe Topic +%% @doc Unsubscribe Topic or Topics %% --spec unsubscribe(Topic :: binary(), SubPid :: pid()) -> any(). unsubscribe(Topic, SubPid) when is_binary(Topic) and is_pid(SubPid) -> - gen_server:cast(?SERVER, {unsubscribe, Topic, SubPid}). + unsubscribe([Topic], SubPid); + +unsubscribe(Topics, SubPid) when is_list(Topics) and is_pid(SubPid) -> + gen_server:cast(?SERVER, {unsubscribe, Topics, SubPid}). %% %% @doc Publish to cluster node. %% --spec publish(Msg :: mqtt_msg()) -> ok. -publish(Msg=#mqtt_msg{topic=Topic}) -> +-spec publish(Msg :: mqtt_message()) -> ok. +publish(Msg=#mqtt_message{topic=Topic}) -> publish(Topic, Msg). --spec publish(Topic :: binary(), Msg :: mqtt_msg()) -> any(). +-spec publish(Topic :: binary(), Msg :: mqtt_message()) -> any(). publish(Topic, Msg) when is_binary(Topic) -> lists:foreach(fun(#topic{name=Name, node=Node}) -> case Node =:= node() of @@ -112,8 +127,14 @@ publish(Topic, Msg) when is_binary(Topic) -> end, match(Topic)). %dispatch locally, should only be called by publish -dispatch(Topic, Msg) when is_binary(Topic) -> - [SubPid ! {dispatch, Msg} || #topic_subscriber{subpid=SubPid} <- ets:lookup(topic_subscriber, Topic)]. +dispatch(Topic, Msg = #mqtt_message{qos = Qos}) when is_binary(Topic) -> + lists:foreach(fun(#topic_subscriber{qos = SubQos, subpid=SubPid}) -> + Msg1 = if + Qos > SubQos -> Msg#mqtt_message{qos = SubQos}; + true -> Msg + end, + SubPid ! {dispatch, {self(), Msg1}} + end, ets:lookup(topic_subscriber, Topic)). -spec match(Topic :: binary()) -> [topic()]. match(Topic) when is_binary(Topic) -> @@ -143,29 +164,23 @@ init([]) -> ets:new(topic_subscriber, [bag, named_table, {keypos, 2}]), {ok, #state{}}. -handle_call({subscribe, {Topic, Qos}, SubPid}, _From, State) -> - case mnesia:transaction(fun trie_add/1, [Topic]) of - {atomic, _} -> - case get({subscriber, SubPid}) of - undefined -> - MonRef = erlang:monitor(process, SubPid), - put({subcriber, SubPid}, MonRef), - put({submon, MonRef}, SubPid); - _ -> - already_monitored - end, - ets:insert(topic_subscriber, #topic_subscriber{topic=Topic, qos = Qos, subpid=SubPid}), - {reply, ok, State}; - {aborted, Reason} -> - {reply, {error, Reason}, State} - end; +handle_call({subscribe, Topics, SubPid}, _From, State) -> + Result = [subscribe_topic({Topic, Qos}, SubPid) || {Topic, Qos} <- Topics], + Reply = + case [Err || Err = {error, _} <- Result] of + [] -> {ok, [Qos || {ok, Qos} <- Result]}; + Errors -> hd(Errors) + end, + {reply, Reply, State}; handle_call(Req, _From, State) -> {stop, {badreq, Req}, State}. -handle_cast({unsubscribe, Topic, SubPid}, State) -> - ets:match_delete(topic_subscriber, #topic_subscriber{topic=Topic, qos ='_', subpid=SubPid}), - try_remove_topic(Topic), +handle_cast({unsubscribe, Topics, SubPid}, State) -> + lists:foreach(fun(Topic) -> + ets:match_delete(topic_subscriber, #topic_subscriber{topic=Topic, qos ='_', subpid=SubPid}), + try_remove_topic(Topic) + end, Topics), {noreply, State}; handle_cast(Msg, State) -> @@ -174,7 +189,7 @@ handle_cast(Msg, State) -> handle_info({'DOWN', Mon, _Type, _Object, _Info}, State) -> case get({submon, Mon}) of undefined -> - ?ERROR("unexpected 'DOWN': ~p", [Mon]); + lager:error("unexpected 'DOWN': ~p", [Mon]); SubPid -> erase({submon, Mon}), erase({subscriber, SubPid}), @@ -196,6 +211,42 @@ code_change(_OldVsn, State, _Extra) -> %% ------------------------------------------------------------------ %% Internal Function Definitions %% ------------------------------------------------------------------ +subscribe_topic({Topic, Qos}, SubPid) -> + case mnesia:transaction(fun trie_add/1, [Topic]) of + {atomic, _} -> + case get({subscriber, SubPid}) of + undefined -> + %%TODO: refactor later... + MonRef = erlang:monitor(process, SubPid), + put({subcriber, SubPid}, MonRef), + put({submon, MonRef}, SubPid); + _ -> + already_monitored + end, + %% remove duplicated subscribers + try_remove_subscriber({Topic, Qos}, SubPid), + ets:insert(topic_subscriber, #topic_subscriber{topic=Topic, qos = Qos, subpid=SubPid}), + %TODO: GrantedQos?? + {ok, Qos}; + {aborted, Reason} -> + {error, Reason} + end. + +try_remove_subscriber({Topic, Qos}, SubPid) -> + case ets:lookup(topic_subscriber, Topic) of + [] -> + not_found; + Subs -> + DupSubs = [Sub || Sub = #topic_subscriber{qos = OldQos, subpid = OldPid} + <- Subs, Qos =/= OldQos, OldPid =:= SubPid], + case DupSubs of + [] -> ok; + [DupSub] -> + lager:warning("PubSub: remove duplicated subscriber ~p", [DupSub]), + ets:delete(topic_subscriber, DupSub) + end + end. + try_remove_topic(Name) when is_binary(Name) -> case ets:member(topic_subscriber, Name) of false -> @@ -218,7 +269,7 @@ trie_add(Topic) when is_binary(Topic) -> [TrieNode=#topic_trie_node{topic=undefined}] -> mnesia:write(TrieNode#topic_trie_node{topic=Topic}); [#topic_trie_node{topic=Topic}] -> - ignore; + {atomic, already_exist}; [] -> %add trie path [trie_add_path(Triple) || Triple <- emqtt_topic:triples(Topic)], diff --git a/apps/emqtt/src/emqtt_queue.erl b/apps/emqtt/src/emqtt_queue.erl new file mode 100644 index 000000000..c025be04f --- /dev/null +++ b/apps/emqtt/src/emqtt_queue.erl @@ -0,0 +1,74 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ +-module(emqtt_queue). + +-include("emqtt.hrl"). + +-export([new/1, new/2, in/3, all/1, clear/1]). + +%%---------------------------------------------------------------------------- + +-ifdef(use_specs). + +-type(mqtt_queue() :: #mqtt_queue_wrapper{}). + +-spec(new(non_neg_intger()) -> mqtt_queue()). + +-spec(in(binary(), mqtt_message(), mqtt_queue()) -> mqtt_queue()). + +-spec(all(mqtt_queue()) -> list()). + +-spec(clear(mqtt_queue()) -> mqtt_queue()). + +-endif. + +%%---------------------------------------------------------------------------- + +-define(DEFAULT_MAX_LEN, 1000). + +-record(mqtt_queue_wrapper, { queue = queue:new(), max_len = ?DEFAULT_MAX_LEN, store_qos0 = false }). + +new(MaxLen) -> #mqtt_queue_wrapper{ max_len = MaxLen }. + +new(MaxLen, StoreQos0) -> #mqtt_queue_wrapper{ max_len = MaxLen, store_qos0 = StoreQos0 }. + +in(ClientId, Message = #mqtt_message{qos = Qos}, + Wrapper = #mqtt_queue_wrapper{ queue = Queue, max_len = MaxLen}) -> + case queue:len(Queue) < MaxLen of + true -> + Wrapper#mqtt_queue_wrapper{ queue = queue:in(Message, Queue) }; + false -> % full + if + Qos =:= ?QOS_0 -> + lager:warning("Queue ~s drop qos0 message: ~p", [ClientId, Message]), + Wrapper; + true -> + {{value, Msg}, Queue1} = queue:drop(Queue), + lager:warning("Queue ~s drop message: ~p", [ClientId, Msg]), + Wrapper#mqtt_queue_wrapper{ queue = Queue1 } + end + end. + +all(#mqtt_queue_wrapper { queue = Queue }) -> queue:to_list(Queue). + +clear(Queue) -> Queue#mqtt_queue_wrapper{ queue = queue:new() }. + diff --git a/apps/emqtt/src/emqtt_queue_sup.erl b/apps/emqtt/src/emqtt_queue_sup.erl new file mode 100644 index 000000000..c49e2e1a8 --- /dev/null +++ b/apps/emqtt/src/emqtt_queue_sup.erl @@ -0,0 +1,41 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ +-module(emqtt_queue_sup). + +-author('feng@emqtt.io'). + +-behavior(supervisor). + +-export([start_link/0, start_queue/0, init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +start_queue() -> + supervisor:start_child(?MODULE, []). + +init([]) -> + {ok, {{simple_one_for_one, 0, 1}, + [{queue, {emqtt_queue, start_link, []}, + transient, 10000, worker, [emqtt_queue]}]}}. + + diff --git a/apps/emqtt/src/emqtt_retained.erl b/apps/emqtt/src/emqtt_retained.erl deleted file mode 100644 index fb57d223a..000000000 --- a/apps/emqtt/src/emqtt_retained.erl +++ /dev/null @@ -1,110 +0,0 @@ -%%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee -%% -%% Permission is hereby granted, free of charge, to any person obtaining a copy -%% of this software and associated documentation files (the "Software"), to deal -%% in the Software without restriction, including without limitation the rights -%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the Software is -%% furnished to do so, subject to the following conditions: -%% -%% The above copyright notice and this permission notice shall be included in all -%% copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%% SOFTWARE. -%%------------------------------------------------------------------------------ - --module(emqtt_retained). - --author('feng@slimchat.io'). - -%%TODO: FIXME Later... - -%% -%% <> - -%% RETAIN -%% Position: byte 1, bit 0. - -%% This flag is only used on PUBLISH messages. When a client sends a PUBLISH to a server, if the Retain flag is set (1), the server should hold on to the message after it has been delivered to the current subscribers. - -%% When a new subscription is established on a topic, the last retained message on that topic should be sent to the subscriber with the Retain flag set. If there is no retained message, nothing is sent - -%% This is useful where publishers send messages on a "report by exception" basis, where it might be some time between messages. This allows new subscribers to instantly receive data with the retained, or Last Known Good, value. - -%% When a server sends a PUBLISH to a client as a result of a subscription that already existed when the original PUBLISH arrived, the Retain flag should not be set, regardless of the Retain flag of the original PUBLISH. This allows a client to distinguish messages that are being received because they were retained and those that are being received "live". - -%% Retained messages should be kept over restarts of the server. - -%% A server may delete a retained message if it receives a message with a zero-length payload and the Retain flag set on the same topic. - --include("emqtt.hrl"). - --include("emqtt_log.hrl"). - --export([start_link/0, - lookup/1, - insert/2, - delete/1, - send/2]). - --behaviour(gen_server). - --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). - --record(state, {}). - -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -lookup(Topic) -> - ets:lookup(retained_msg, Topic). - -insert(Topic, Msg) -> - gen_server:cast(?MODULE, {insert, Topic, Msg}). - -delete(Topic) -> - gen_server:cast(?MODULE, {delete, Topic}). - -send(Topic, Client) -> - [Client ! {dispatch, Msg} ||{_, Msg} <- lookup(Topic)]. - -init([]) -> - ets:new(retained_msg, [set, protected, named_table]), - {ok, #state{}}. - -handle_call(Req, _From, State) -> - {stop, {badreq,Req}, State}. - -handle_cast({insert, Topic, Msg}, State) -> - ets:insert(retained_msg, {Topic, Msg}), - {noreply, State}; - -handle_cast({delete, Topic}, State) -> - ets:delete(retained_msg, Topic), - {noreply, State}; - -handle_cast(Msg, State) -> - {stop, {badmsg, Msg}, State}. - -handle_info(Info, State) -> - {stop, {badinfo, Info}, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - - diff --git a/apps/emqtt/src/emqtt_router.erl b/apps/emqtt/src/emqtt_router.erl new file mode 100644 index 000000000..43e6bd419 --- /dev/null +++ b/apps/emqtt/src/emqtt_router.erl @@ -0,0 +1,99 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + +%%route chain... statistics +-module(emqtt_router). + +-include("emqtt.hrl"). + +-behaviour(gen_server). + +-define(SERVER, ?MODULE). + +%% ------------------------------------------------------------------ +%% API Function Exports +%% ------------------------------------------------------------------ + +-export([start_link/0]). + +%%Router Chain--> +%%--->In +%%Out<--- +-export([route/1]). + +%% ------------------------------------------------------------------ +%% gen_server Function Exports +%% ------------------------------------------------------------------ + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +%%---------------------------------------------------------------------------- + +-ifdef(use_specs). + +-spec(start_link/1 :: () -> {ok, pid()}). + +-spec route(mqtt_message()) -> ok. + +-endif. + +%% ------------------------------------------------------------------ +%% API Function Definitions +%% ------------------------------------------------------------------ + +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +route(Msg) -> + lager:info("Route message: ~s", [emqtt_message:dump(Msg)]), + % need to retain? + emqtt_server:retain(Msg), + % unset flag and pubsub + emqtt_pubsub:publish( emqtt_message:unset_flag(Msg) ). + +%% ------------------------------------------------------------------ +%% gen_server Function Definitions +%% ------------------------------------------------------------------ + +init(Args) -> + {ok, Args, hibernate}. + +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% ------------------------------------------------------------------ +%% Internal Function Definitions +%% ------------------------------------------------------------------ + diff --git a/apps/emqtt/src/emqtt_server.erl b/apps/emqtt/src/emqtt_server.erl new file mode 100644 index 000000000..9360eaa8f --- /dev/null +++ b/apps/emqtt/src/emqtt_server.erl @@ -0,0 +1,141 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + +-module(emqtt_server). + +-author('feng@slimpp.io'). + +-include("emqtt.hrl"). + +-include("emqtt_topic.hrl"). + +-behaviour(gen_server). + +-define(SERVER, ?MODULE). + +-define(RETAINED_TAB, mqtt_retained). + +-define(STORE_LIMIT, 100000). + +-record(mqtt_retained, {topic, qos, payload}). + +-record(state, {store_limit}). + +%% ------------------------------------------------------------------ +%% API Function Exports +%% ------------------------------------------------------------------ + +%%TODO: subscribe +-export([start_link/1, retain/1, subscribe/2]). + +%% ------------------------------------------------------------------ +%% gen_server Function Exports +%% ------------------------------------------------------------------ + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +%% ------------------------------------------------------------------ +%% API Function Definitions +%% ------------------------------------------------------------------ + +start_link(RetainOpts) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [RetainOpts], []). + +retain(#mqtt_message{ retain = false }) -> ignore; + +%% RETAIN flag set to 1 and payload containing zero bytes +retain(#mqtt_message{ retain = true, topic = Topic, payload = <<>> }) -> + mnesia:dirty_delete(?RETAINED_TAB, Topic); + +retain(Msg = #mqtt_message{retain = true}) -> + gen_server:cast(?SERVER, {retain, Msg}). + +%% +subscribe(Topics, CPid) when is_pid(CPid) -> + lager:info("Retained Topics: ~p", [match(Topics)]), + RetainedMsgs = lists:flatten([mnesia:dirty_read(?RETAINED_TAB, Topic) || Topic <- match(Topics)]), + lager:info("Retained Messages: ~p", [RetainedMsgs]), + lists:foreach(fun(Msg) -> + CPid ! {dispatch, {self(), retained_msg(Msg)}} + end, RetainedMsgs). + +%% ------------------------------------------------------------------ +%% gen_server Function Definitions +%% ------------------------------------------------------------------ + +init([RetainOpts]) -> + mnesia:create_table(mqtt_retained, [ + {type, ordered_set}, + {ram_copies, [node()]}, + {attributes, record_info(fields, mqtt_retained)}]), + mnesia:add_table_copy(mqtt_retained, node(), ram_copies), + Limit = proplists:get_value(store_limit, RetainOpts, ?STORE_LIMIT), + {ok, #state{store_limit = Limit}}. + +handle_call(Req, _From, State) -> + {stop, {badreq, Req}, State}. + +handle_cast({retain, Msg = #mqtt_message{ qos = Qos, + topic = Topic, + payload = Payload }}, State = #state{store_limit = Limit}) -> + case mnesia:table_info(?RETAINED_TAB, size) of + Size when Size >= Limit -> + lager:error("Server dropped message(retain) for table is full: ~p", [Msg]); + _ -> + lager:info("Server retained message: ~p", [Msg]), + mnesia:dirty_write(#mqtt_retained{ topic = Topic, + qos = Qos, + payload = Payload }) + end, + {noreply, State}; + +handle_cast(Msg, State) -> + {stop, {badmsg, Msg}, State}. + +handle_info(Info, State) -> + {stop, {badinfo, Info}, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% ------------------------------------------------------------------ +%% Internal Function Definitions +%% ------------------------------------------------------------------ +match(Topics) -> + RetainedTopics = mnesia:dirty_all_keys(?RETAINED_TAB), + lists:flatten([match(Topic, RetainedTopics) || Topic <- Topics]). + +match(Topic, RetainedTopics) -> + case emqtt_topic:type(#topic{name=Topic}) of + direct -> %% FIXME + [Topic]; + wildcard -> + [ T || T <- RetainedTopics, emqtt_topic:match(T, Topic) ] + end. + +retained_msg(#mqtt_retained{topic = Topic, qos = Qos, payload = Payload}) -> + #mqtt_message { qos = Qos, retain = true, topic = Topic, payload = Payload }. + diff --git a/apps/emqtt/src/emqtt_session.erl b/apps/emqtt/src/emqtt_session.erl new file mode 100644 index 000000000..e52890a1a --- /dev/null +++ b/apps/emqtt/src/emqtt_session.erl @@ -0,0 +1,341 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + +-module(emqtt_session). + +-include("emqtt.hrl"). + +-include("emqtt_packet.hrl"). + +%% ------------------------------------------------------------------ +%% API Function Exports +%% ------------------------------------------------------------------ +-export([start/1, resume/3, publish/2, puback/2, subscribe/2, unsubscribe/2, destroy/2]). + +-export([store/2]). + +%%start gen_server +-export([start_link/3]). + +%% ------------------------------------------------------------------ +%% gen_server Function Exports +%% ------------------------------------------------------------------ + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(session_state, { + client_id :: binary(), + client_pid :: pid(), + message_id = 1, + submap :: map(), + msg_queue, %% do not receive rel + awaiting_ack :: map(), + awaiting_rel :: map(), + awaiting_comp :: map(), + expires, + expire_timer }). + +%% ------------------------------------------------------------------ +%% Start Session +%% ------------------------------------------------------------------ +start({true = _CleanSess, ClientId, _ClientPid}) -> + %%Destroy old session if CleanSess is true before. + ok = emqtt_sm:destroy_session(ClientId), + {ok, initial_state(ClientId)}; + +start({false = _CleanSess, ClientId, ClientPid}) -> + {ok, SessPid} = emqtt_sm:start_session(ClientId, ClientPid), + {ok, SessPid}. + +%% ------------------------------------------------------------------ +%% Session API +%% ------------------------------------------------------------------ +resume(SessState = #session_state{}, _ClientId, _ClientPid) -> + SessState; +resume(SessPid, ClientId, ClientPid) when is_pid(SessPid) -> + gen_server:cast(SessPid, {resume, ClientId, ClientPid}), + SessPid. + +publish(Session, {?QOS_0, Message}) -> + emqtt_router:route(Message), Session; + +publish(Session, {?QOS_1, Message}) -> + emqtt_router:route(Message), Session; + +publish(SessState = #session_state{awaiting_rel = AwaitingRel}, + {?QOS_2, Message = #mqtt_message{ msgid = MsgId }}) -> + %% store in awaiting_rel + SessState#session_state{awaiting_rel = maps:put(MsgId, Message, AwaitingRel)}; + +publish(SessPid, {?QOS_2, Message}) when is_pid(SessPid) -> + gen_server:cast(SessPid, {publish, ?QOS_2, Message}), + SessPid. + +%% PUBACK +puback(SessState = #session_state{client_id = ClientId, awaiting_ack = Awaiting}, {?PUBACK, PacketId}) -> + case maps:is_key(PacketId, Awaiting) of + true -> ok; + false -> lager:warning("Session ~s: PUBACK PacketId '~p' not found!", [ClientId, PacketId]) + end, + SessState#session_state{awaiting_ack = maps:remove(PacketId, Awaiting)}; +puback(SessPid, {?PUBACK, PacketId}) when is_pid(SessPid) -> + gen_server:cast(SessPid, {puback, PacketId}), SessPid; + +%% PUBREC +puback(SessState = #session_state{ client_id = ClientId, + awaiting_ack = AwaitingAck, + awaiting_comp = AwaitingComp }, {?PUBREC, PacketId}) -> + case maps:is_key(PacketId, AwaitingAck) of + true -> ok; + false -> lager:warning("Session ~s: PUBREC PacketId '~p' not found!", [ClientId, PacketId]) + end, + SessState#session_state{ awaiting_ack = maps:remove(PacketId, AwaitingAck), + awaiting_comp = maps:put(PacketId, true, AwaitingComp) }; + +puback(SessPid, {?PUBREC, PacketId}) when is_pid(SessPid) -> + gen_server:cast(SessPid, {pubrec, PacketId}), SessPid; + +%% PUBREL +puback(SessState = #session_state{client_id = ClientId, awaiting_rel = Awaiting}, {?PUBREL, PacketId}) -> + case maps:find(PacketId, Awaiting) of + {ok, Msg} -> emqtt_router:route(Msg); + error -> lager:warning("Session ~s: PUBREL PacketId '~p' not found!", [ClientId, PacketId]) + end, + SessState#session_state{awaiting_rel = maps:remove(PacketId, Awaiting)}; + +puback(SessPid, {?PUBREL, PacketId}) when is_pid(SessPid) -> + gen_server:cast(SessPid, {pubrel, PacketId}), SessPid; + +%% PUBCOMP +puback(SessState = #session_state{ client_id = ClientId, + awaiting_comp = AwaitingComp}, {?PUBCOMP, PacketId}) -> + case maps:is_key(PacketId, AwaitingComp) of + true -> ok; + false -> lager:warning("Session ~s: PUBREC PacketId '~p' not exist", [ClientId, PacketId]) + end, + SessState#session_state{ awaiting_comp = maps:remove(PacketId, AwaitingComp) }; + +puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> + gen_server:cast(SessPid, {pubcomp, PacketId}), SessPid. + +%% SUBSCRIBE +subscribe(SessState = #session_state{client_id = ClientId, submap = SubMap}, Topics) -> + Resubs = [Topic || {Name, _Qos} = Topic <- Topics, maps:is_key(Name, SubMap)], + case Resubs of + [] -> ok; + _ -> lager:warning("~s resubscribe ~p", [ClientId, Resubs]) + end, + SubMap1 = lists:foldl(fun({Name, Qos}, Acc) -> maps:put(Name, Qos, Acc) end, SubMap, Topics), + {ok, GrantedQos} = emqtt_pubsub:subscribe(Topics, self()), + %%TODO: should be gen_event and notification... + emqtt_server:subscribe([ Name || {Name, _} <- Topics ], self()), + {ok, SessState#session_state{submap = SubMap1}, GrantedQos}; + +subscribe(SessPid, Topics) when is_pid(SessPid) -> + {ok, GrantedQos} = gen_server:call(SessPid, {subscribe, Topics}), + {ok, SessPid, GrantedQos}. + +%% +%% @doc UNSUBSCRIBE +%% +unsubscribe(SessState = #session_state{client_id = ClientId, submap = SubMap}, Topics) -> + %%TODO: refactor later. + case Topics -- maps:keys(SubMap) of + [] -> ok; + BadUnsubs -> lager:warning("~s should not unsubscribe ~p", [ClientId, BadUnsubs]) + end, + %%unsubscribe from topic tree + ok = emqtt_pubsub:unsubscribe(Topics, self()), + SubMap1 = lists:foldl(fun(Topic, Acc) -> maps:remove(Topic, Acc) end, SubMap, Topics), + {ok, SessState#session_state{submap = SubMap1}}; + +unsubscribe(SessPid, Topics) when is_pid(SessPid) -> + gen_server:call(SessPid, {unsubscribe, Topics}), + {ok, SessPid}. + +destroy(SessPid, ClientId) when is_pid(SessPid) -> + gen_server:cast(SessPid, {destroy, ClientId}). + +%store message(qos1) that sent to client +store(SessState = #session_state{ message_id = MsgId, awaiting_ack = Awaiting}, + Message = #mqtt_message{ qos = Qos }) when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> + %%assign msgid before send + Message1 = Message#mqtt_message{ msgid = MsgId }, + Message2 = + if + Qos =:= ?QOS_2 -> Message1#mqtt_message{dup = false}; + true -> Message1 + end, + Awaiting1 = maps:put(MsgId, Message2, Awaiting), + {Message1, next_msg_id(SessState#session_state{ awaiting_ack = Awaiting1 })}. + +initial_state(ClientId) -> + #session_state { client_id = ClientId, + submap = #{}, + awaiting_ack = #{}, + awaiting_rel = #{}, + awaiting_comp = #{} }. + +initial_state(ClientId, ClientPid) -> + State = initial_state(ClientId), + State#session_state{client_pid = ClientPid}. + +%% ------------------------------------------------------------------ +%% gen_server Function Definitions +%% ------------------------------------------------------------------ + +start_link(SessOpts, ClientId, ClientPid) -> + gen_server:start_link(?MODULE, [SessOpts, ClientId, ClientPid], []). + +init([SessOpts, ClientId, ClientPid]) -> + process_flag(trap_exit, true), + %%TODO: Is this OK? + true = link(ClientPid), + State = initial_state(ClientId, ClientPid), + Expires = proplists:get_value(expires, SessOpts, 1) * 3600, + MsgQueue = emqtt_queue:new( proplists:get_value(max_queue, SessOpts, 1000), + proplists:get_value(store_qos0, SessOpts, false) ), + {ok, State#session_state{ expires = Expires, + msg_queue = MsgQueue }, hibernate}. + +handle_call({subscribe, Topics}, _From, State) -> + {ok, NewState, GrantedQos} = subscribe(State, Topics), + {reply, {ok, GrantedQos}, NewState}; + +handle_call({unsubscribe, Topics}, _From, State) -> + {ok, NewState} = unsubscribe(State, Topics), + {reply, ok, NewState}; + +handle_call(Req, _From, State) -> + {stop, {badreq, Req}, State}. + +handle_cast({resume, ClientId, ClientPid}, State = #session_state { + client_id = ClientId, + client_pid = undefined, + msg_queue = Queue, + awaiting_ack = AwaitingAck, + awaiting_comp = AwaitingComp, + expire_timer = ETimer}) -> + lager:info("Session ~s resumed by ~p", [ClientId, ClientPid]), + %cancel timeout timer + erlang:cancel_timer(ETimer), + + %% redelivery PUBREL + lists:foreach(fun(PacketId) -> + ClientPid ! {redeliver, {?PUBREL, PacketId}} + end, maps:keys(AwaitingComp)), + + %% redelivery messages that awaiting PUBACK or PUBREC + Dup = fun(Msg) -> Msg#mqtt_message{ dup = true } end, + lists:foreach(fun(Msg) -> + ClientPid ! {dispatch, {self(), Dup(Msg)}} + end, maps:values(AwaitingAck)), + + %% send offline messages + lists:foreach(fun(Msg) -> + ClientPid ! {dispatch, {self(), Msg}} + end, emqtt_queue:all(Queue)), + + NewState = State#session_state{ client_pid = ClientPid, + msg_queue = emqtt_queue:clear(Queue), + expire_timer = undefined}, + {noreply, NewState, hibernate}; + +handle_cast({publish, ?QOS_2, Message}, State) -> + NewState = publish(State, {?QOS_2, Message}), + {noreply, NewState}; + +handle_cast({puback, PacketId}, State) -> + NewState = puback(State, {?PUBACK, PacketId}), + {noreply, NewState}; + +handle_cast({pubrec, PacketId}, State) -> + NewState = puback(State, {?PUBREC, PacketId}), + {noreply, NewState}; + +handle_cast({pubrel, PacketId}, State) -> + NewState = puback(State, {?PUBREL, PacketId}), + {noreply, NewState}; + +handle_cast({pubcomp, PacketId}, State) -> + NewState = puback(State, {?PUBCOMP, PacketId}), + {noreply, NewState}; + +handle_cast({destroy, ClientId}, State = #session_state{client_id = ClientId}) -> + lager:warning("Session ~s destroyed", [ClientId]), + {stop, normal, State}; + +handle_cast(Msg, State) -> + {stop, {badmsg, Msg}, State}. + +handle_info({dispatch, {_From, Message}}, State) -> + {noreply, dispatch(Message, State)}; + +handle_info({'EXIT', ClientPid, Reason}, State = #session_state{ + client_id = ClientId, client_pid = ClientPid, expires = Expires}) -> + lager:warning("Session: client ~s@~p exited, caused by ~p", [ClientId, ClientPid, Reason]), + Timer = erlang:send_after(Expires * 1000, self(), session_expired), + {noreply, State#session_state{ client_pid = undefined, expire_timer = Timer}}; + +handle_info(session_expired, State = #session_state{client_id = ClientId}) -> + lager:warning("Session ~s expired!", [ClientId]), + {stop, {shutdown, expired}, State}; + +handle_info(Info, State) -> + {stop, {badinfo, Info}, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% ------------------------------------------------------------------ +%% Internal Function Definitions +%% ------------------------------------------------------------------ + +dispatch(Message, State = #session_state{ client_id = ClientId, + client_pid = undefined }) -> + queue(ClientId, Message, State); + +dispatch(Message = #mqtt_message{ qos = ?QOS_0 }, State = #session_state{ + client_pid = ClientPid }) -> + ClientPid ! {dispatch, {self(), Message}}, + State; + +dispatch(Message = #mqtt_message{ qos = Qos }, State = #session_state{ client_pid = ClientPid }) + when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> + {Message1, NewState} = store(State, Message), + ClientPid ! {dispatch, {self(), Message1}}, + NewState. + +queue(ClientId, Message, State = #session_state{msg_queue = Queue}) -> + State#session_state{msg_queue = emqtt_queue:in(ClientId, Message, Queue)}. + +next_msg_id(State = #session_state{ message_id = 16#ffff }) -> + State#session_state{ message_id = 1 }; + +next_msg_id(State = #session_state{ message_id = MsgId }) -> + State#session_state{ message_id = MsgId + 1 }. + + diff --git a/apps/emqtt/src/emqtt_keep_alive.erl b/apps/emqtt/src/emqtt_session_sup.erl similarity index 54% rename from apps/emqtt/src/emqtt_keep_alive.erl rename to apps/emqtt/src/emqtt_session_sup.erl index 873608fb9..01ea4db7e 100644 --- a/apps/emqtt/src/emqtt_keep_alive.erl +++ b/apps/emqtt/src/emqtt_session_sup.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -20,47 +20,38 @@ %% SOFTWARE. %%------------------------------------------------------------------------------ --module(emqtt_keep_alive). +-module(emqtt_session_sup). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). --export([new/2, - state/1, - activate/1, - reset/1, - cancel/1]). +-behavior(supervisor). --record(keep_alive, {state, period, timer, msg}). +-export([start_link/1, start_session/2]). -new(undefined, _) -> - undefined; -new(0, _) -> - undefined; -new(Period, TimeoutMsg) when is_integer(Period) -> - Ref = erlang:send_after(Period, self(), TimeoutMsg), - #keep_alive{state=idle, period=Period, timer=Ref, msg=TimeoutMsg}. +-export([init/1]). -state(undefined) -> - undefined; -state(#keep_alive{state=State}) -> - State. +%%---------------------------------------------------------------------------- -activate(undefined) -> - undefined; -activate(KeepAlive) when is_record(KeepAlive, keep_alive) -> - KeepAlive#keep_alive{state=active}. +-ifdef(use_specs). -reset(undefined) -> - undefined; -reset(KeepAlive=#keep_alive{period=Period, timer=Timer, msg=Msg}) -> - catch erlang:cancel_timer(Timer), - Ref = erlang:send_after(Period, self(), Msg), - KeepAlive#keep_alive{state=idle, timer = Ref}. +-spec(start_link/1 :: (list(tuple())) -> {ok, pid()}). + +-spec(start_session/2 :: (binary(), pid()) -> {ok, pid()}). + +-endif. + +%%---------------------------------------------------------------------------- + +start_link(SessOpts) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, [SessOpts]). + +start_session(ClientId, ClientPid) -> + supervisor:start_child(?MODULE, [ClientId, ClientPid]). + +init([SessOpts]) -> + {ok, {{simple_one_for_one, 0, 1}, + [{session, {emqtt_session, start_link, [SessOpts]}, + transient, 10000, worker, [emqtt_session]}]}}. -cancel(undefined) -> - undefined; -cancel(KeepAlive=#keep_alive{timer=Timer}) -> - catch erlang:cancel_timer(Timer), - KeepAlive#keep_alive{timer=undefined}. diff --git a/apps/emqtt/src/emqtt_sm.erl b/apps/emqtt/src/emqtt_sm.erl new file mode 100644 index 000000000..c9db9c710 --- /dev/null +++ b/apps/emqtt/src/emqtt_sm.erl @@ -0,0 +1,162 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + + +%%------------------------------------------------------------------------------ +%% +%% The Session state in the Server consists of: +%% The existence of a Session, even if the rest of the Session state is empty. +%% The Client’s subscriptions. +%% QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely +%% acknowledged. +%% QoS 1 and QoS 2 messages pending transmission to the Client. +%% QoS 2 messages which have been received from the Client, but have not been completely +%% acknowledged. +%% Optionally, QoS 0 messages pending transmission to the Client. +%% +%%------------------------------------------------------------------------------ + +-module(emqtt_sm). + +%%emqtt session manager... + +%%cleanSess: true | false + +-include("emqtt.hrl"). + +-behaviour(gen_server). + +-define(SERVER, ?MODULE). + +-define(TABLE, emqtt_session). + +%% ------------------------------------------------------------------ +%% API Function Exports +%% ------------------------------------------------------------------ + +-export([start_link/0]). + +-export([lookup_session/1, start_session/2, destroy_session/1]). + +%% ------------------------------------------------------------------ +%% gen_server Function Exports +%% ------------------------------------------------------------------ + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + + +%%---------------------------------------------------------------------------- + +-ifdef(use_specs). + +-spec(start_link/0 :: () -> {ok, pid()}). + +-spec(lookup_session/1 :: (binary()) -> pid() | undefined). + +-spec(start_session/2 :: (binary(), pid()) -> {ok, pid()} | {error, any()}). + +-spec(destroy_session/1 :: (binary()) -> ok). + +-endif. + +%%---------------------------------------------------------------------------- + +-record(state, {}). + +%% ------------------------------------------------------------------ +%% API Function Definitions +%% ------------------------------------------------------------------ + +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +lookup_session(ClientId) -> + case ets:lookup(?TABLE, ClientId) of + [{_, SessPid, _}] -> SessPid; + [] -> undefined + end. + +start_session(ClientId, ClientPid) -> + gen_server:call(?SERVER, {start_session, ClientId, ClientPid}). + +destroy_session(ClientId) -> + gen_server:call(?SERVER, {destroy_session, ClientId}). + +%% ------------------------------------------------------------------ +%% gen_server Function Definitions +%% ------------------------------------------------------------------ + +init([]) -> + process_flag(trap_exit, true), + ets:new(?TABLE, [set, protected, named_table]), + {ok, #state{}}. + +handle_call({start_session, ClientId, ClientPid}, _From, State) -> + Reply = + case ets:lookup(?TABLE, ClientId) of + [{_, SessPid, _MRef}] -> + emqtt_session:resume(SessPid, ClientId, ClientPid), + {ok, SessPid}; + [] -> + case emqtt_session_sup:start_session(ClientId, ClientPid) of + {ok, SessPid} -> + MRef = erlang:monitor(process, SessPid), + ets:insert(?TABLE, {ClientId, SessPid, MRef}), + {ok, SessPid}; + {error, Error} -> + {error, Error} + end + end, + {reply, Reply, State}; + +handle_call({destroy_session, ClientId}, _From, State) -> + case ets:lookup(?TABLE, ClientId) of + [{_, SessPid, MRef}] -> + erlang:demonitor(MRef), + emqtt_session:destroy(SessPid, ClientId), + ets:delete(?TABLE, ClientId); + [] -> + ignore + end, + {reply, ok, State}; + +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> + ets:match_delete(emqtt_client, {{'_', DownPid, MRef}}), + {noreply, State}; + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + diff --git a/apps/emqtt/src/emqtt_sup.erl b/apps/emqtt/src/emqtt_sup.erl index de3d053b3..567f19a58 100644 --- a/apps/emqtt/src/emqtt_sup.erl +++ b/apps/emqtt/src/emqtt_sup.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,7 @@ -module(emqtt_sup). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -include("emqtt.hrl"). diff --git a/apps/emqtt/src/emqtt_throttle.erl b/apps/emqtt/src/emqtt_throttle.erl new file mode 100644 index 000000000..1e56cda52 --- /dev/null +++ b/apps/emqtt/src/emqtt_throttle.erl @@ -0,0 +1,26 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + +-module(emqtt_throttle). + + + diff --git a/apps/emqtt/src/emqtt_topic.erl b/apps/emqtt/src/emqtt_topic.erl index 29d9d865d..d9ba5d41b 100644 --- a/apps/emqtt/src/emqtt_topic.erl +++ b/apps/emqtt/src/emqtt_topic.erl @@ -1,5 +1,5 @@ %%----------------------------------------------------------------------------- -%% Copyright (c) 2014, Feng Lee +%% Copyright (c) 2012-2015, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,7 @@ -module(emqtt_topic). --author('feng@slimchat.io'). +-author('feng@emqtt.io'). -import(lists, [reverse/1]). diff --git a/apps/emqtt/test/emqtt_packet_tests.erl b/apps/emqtt/test/emqtt_packet_tests.erl new file mode 100644 index 000000000..f1571e0a0 --- /dev/null +++ b/apps/emqtt/test/emqtt_packet_tests.erl @@ -0,0 +1,160 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ +-module(emqtt_packet_tests). + +-include("emqtt_packet.hrl"). + +-import(emqtt_packet, [initial_state/0, parse/2, serialise/1]). + +-ifdef(TEST). + +-include_lib("eunit/include/eunit.hrl"). + +parse_connect_test() -> + State = initial_state(), + %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) + V31ConnBin = <<16,37,0,6,77,81,73,115,100,112,3,2,0,60,0,23,109,111,115,113,112,117,98,47,49,48,52,53,49,45,105,77,97,99,46,108,111,99,97>>, + ?assertMatch({ok, #mqtt_packet{ + header = #mqtt_packet_header { type = ?CONNECT, + dup = false, + qos = 0, + retain = false}, + variable = #mqtt_packet_connect { proto_ver = 3, + proto_name = <<"MQIsdp">>, + client_id = <<"mosqpub/10451-iMac.loca">>, + clean_sess = true, + keep_alive = 60 } }, <<>>}, parse(V31ConnBin, State)), + %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) + V311ConnBin = <<16,35,0,4,77,81,84,84,4,2,0,60,0,23,109,111,115,113,112,117,98,47,49,48,52,53,49,45,105,77,97,99,46,108,111,99,97>>, + ?assertMatch({ok, #mqtt_packet{ + header = #mqtt_packet_header { type = ?CONNECT, + dup = false, + qos = 0, + retain = false}, + variable = #mqtt_packet_connect { proto_ver = 4, + proto_name = <<"MQTT">>, + client_id = <<"mosqpub/10451-iMac.loca">>, + clean_sess = true, + keep_alive = 60 } }, <<>>}, parse(V311ConnBin, State)), + + %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId="", ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60) + V311ConnWithoutClientId = <<16,12,0,4,77,81,84,84,4,2,0,60,0,0>>, + ?assertMatch({ok, #mqtt_packet{ + header = #mqtt_packet_header { type = ?CONNECT, + dup = false, + qos = 0, + retain = false}, + variable = #mqtt_packet_connect { proto_ver = 4, + proto_name = <<"MQTT">>, + client_id = <<>>, + clean_sess = true, + keep_alive = 60 } }, <<>>}, parse(V311ConnWithoutClientId, State)), + %%CONNECT(Qos=0, Retain=false, Dup=false, ClientId=mosqpub/10452-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, Username=test, Password=******, Will(Qos=1, Retain=false, Topic=/will, Msg=willmsg)) + ConnBinWithWill = <<16,67,0,6,77,81,73,115,100,112,3,206,0,60,0,23,109,111,115,113,112,117,98,47,49,48,52,53,50,45,105,77,97,99,46,108,111,99,97,0,5,47,119,105,108,108,0,7,119,105,108,108,109,115,103,0,4,116,101,115,116,0,6,112,117,98,108,105,99>>, + ?assertMatch({ok, #mqtt_packet{ + header = #mqtt_packet_header { type = ?CONNECT, + dup = false, + qos = 0, + retain = false}, + variable = #mqtt_packet_connect { proto_ver = 3, + proto_name = <<"MQIsdp">>, + client_id = <<"mosqpub/10452-iMac.loca">>, + clean_sess = true, + keep_alive = 60, + will_retain = false, + will_qos = 1, + will_flag = true, + will_topic = <<"/will">>, + will_msg = <<"willmsg">> , + username = <<"test">>, + password = <<"public">> } }, + <<>> }, parse(ConnBinWithWill, State)), + ok. + +parse_publish_test() -> + State = initial_state(), + %%PUBLISH(Qos=1, Retain=false, Dup=false, TopicName=a/b/c, PacketId=1, Payload=<<"hahah">>) + PubBin = <<50,14,0,5,97,47,98,47,99,0,1,104,97,104,97,104>>, + ?assertMatch({ok, #mqtt_packet { + header = #mqtt_packet_header { type = ?PUBLISH, + dup = false, + qos = 1, + retain = false}, + variable = #mqtt_packet_publish { topic_name = <<"a/b/c">>, + packet_id = 1 }, + payload = <<"hahah">> }, <<>>}, parse(PubBin, State)), + + %PUBLISH(Qos=0, Retain=false, Dup=false, TopicName=xxx/yyy, PacketId=undefined, Payload=<<"hello">>) + %DISCONNECT(Qos=0, Retain=false, Dup=false) + PubBin1 = <<48,14,0,7,120,120,120,47,121,121,121,104,101,108,108,111,224,0>>, + ?assertMatch({ok, #mqtt_packet { + header = #mqtt_packet_header { type = ?PUBLISH, + dup = false, + qos = 0, + retain = false}, + variable = #mqtt_packet_publish { topic_name = <<"xxx/yyy">>, + packet_id = undefined }, + payload = <<"hello">> }, <<224,0>>}, parse(PubBin1, State)), + ?assertMatch({ok, #mqtt_packet{ + header = #mqtt_packet_header { type = ?DISCONNECT, + dup = false, + qos = 0, + retain = false} + }, <<>>}, parse(<<224, 0>>, State)). + +parse_puback_test() -> + %%PUBACK(Qos=0, Retain=false, Dup=false, PacketId=1) + PubAckBin = <<64,2,0,1>>, + ?assertMatch({ok, #mqtt_packet { + header = #mqtt_packet_header { type = ?PUBACK, + dup = false, + qos = 0, + retain = false } + }, <<>>}, parse(PubAckBin, initial_state())), + ok. + +parse_subscribe_test() -> + ok. + +parse_pingreq_test() -> + ok. + +parse_disconnect_test() -> + %DISCONNECT(Qos=0, Retain=false, Dup=false) + Bin = <<224, 0>>, + ?assertMatch({ok, #mqtt_packet { + header = #mqtt_packet_header { type = ?DISCONNECT, + dup = false, + qos = 0, + retain = false } + }, <<>>}, parse(Bin, initial_state())). + +serialise_connack_test() -> + ConnAck = #mqtt_packet{ header = #mqtt_packet_header { type = ?CONNACK }, + variable = #mqtt_packet_connack { ack_flags = 0, return_code = 0 } }, + ?assertEqual(<<32,2,0,0>>, emqtt_packet:serialise(ConnAck)). + +serialise_puback_test() -> + ok. + +-endif. + diff --git a/apps/emqtt/test/emqtt_topic_tests.erl b/apps/emqtt/test/emqtt_topic_tests.erl index 89e2eeb20..6c353c4d4 100644 --- a/apps/emqtt/test/emqtt_topic_tests.erl +++ b/apps/emqtt/test/emqtt_topic_tests.erl @@ -1,12 +1,34 @@ +%%----------------------------------------------------------------------------- +%% Copyright (c) 2012-2015, Feng Lee +%% +%% Permission is hereby granted, free of charge, to any person obtaining a copy +%% of this software and associated documentation files (the "Software"), to deal +%% in the Software without restriction, including without limitation the rights +%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the Software is +%% furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in all +%% copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%% SOFTWARE. +%%------------------------------------------------------------------------------ + -module(emqtt_topic_tests). --include("emqtt_internal.hrl"). +-include("emqtt_topic.hrl"). -import(emqtt_topic, [validate/1, type/1, match/2, triples/1, words/1]). -ifdef(TEST). --include_lib("enunit/include/enunit.hrl"). +-include_lib("eunit/include/eunit.hrl"). validate_test() -> ?assert( validate({subscribe, <<"a/b/c">>}) ), @@ -17,8 +39,8 @@ validate_test() -> type_test() -> ?assertEqual(direct, type(#topic{name = <<"/a/b/cdkd">>})), - ?assertEqual(wildcard, type(#type{name = <<"/a/+/d">>})), - ?assertEqual(wildcard, type(#type{name = <<"/a/b/#">>})). + ?assertEqual(wildcard, type(#topic{name = <<"/a/+/d">>})), + ?assertEqual(wildcard, type(#topic{name = <<"/a/b/#">>})). -endif. diff --git a/data/.placeholder b/data/.placeholder new file mode 100644 index 000000000..0fec8a3ed --- /dev/null +++ b/data/.placeholder @@ -0,0 +1 @@ +durable queue data... diff --git a/doc/.retain.md.swp b/doc/.retain.md.swp new file mode 100644 index 000000000..648c9731d Binary files /dev/null and b/doc/.retain.md.swp differ diff --git a/doc/cluster.md b/doc/cluster.md new file mode 100644 index 000000000..076ba2689 --- /dev/null +++ b/doc/cluster.md @@ -0,0 +1,25 @@ + + zookeeper + | + eMQTT1 eMQTT2 eMQTT3 + + +Bridge + + + eMQTT1 --> eMQTT2 + + +Cluster + + eMQTT1 <--> eMQTT2 + + +Cluster and Bridge + + eMQTT1 eMQTT3 + ----> + eMQTT2 eMQTT4 + +Mnesia Cluster + diff --git a/doc/design.md b/doc/design.md new file mode 100644 index 000000000..9e775a095 --- /dev/null +++ b/doc/design.md @@ -0,0 +1,30 @@ +# eMQTT Desgin Guide + +## KeepAlive + +## Retained + +## QOS1 + +## QOS2 + +## Durable Subscriptions + +Durable Sub: + +Client->Queue->Router->Queue->Client + +Normal Sub: + +Client->Router->Client + +Router to register queues + +## Topic Tree + +## Cluster + +## Bridge + +## Offline Message + diff --git a/doc/protocol.md b/doc/protocol.md new file mode 100644 index 000000000..77ba565b3 --- /dev/null +++ b/doc/protocol.md @@ -0,0 +1,36 @@ +# MQTT Protocol Guide + +## Server or Broker + +A program or device that acts as an intermediary between Clients which publish Application Messages and Clients which have made Subscriptions. + +A Server Accepts Network Connections from Clients. +Accepts Application Messages published by Clients. +Processes Subscribe and Unsubscribe requests from Clients. +Forwards Application Messages that match Client Subscriptions. + + +Client ----> Broker(Server) ----> Client + +Publisher ----> Broker -----> Subscriber + +## Subscription and Session + +### Subscription + +A Subscription comprises a Topic Filter and a maximum QoS. A Subscription is associated with a single Session. A Session can contain more than one Subscription. Each Subscription within a session has a different Topic Filter. + +### Session + +A stateful interaction between a Client and a Server. Some Sessions last only as long as the Network + +Connection, others can span multiple consecutive Network Connections between a Client and a Server. + +## Topic Name and Filter + +An expression contained in a Subscription, to indicate an interest in one or more topics. A Topic Filter can include wildcard characters. + + +## Packet Identifier + + diff --git a/doc/pubsub.md b/doc/pubsub.md new file mode 100644 index 000000000..298043b62 --- /dev/null +++ b/doc/pubsub.md @@ -0,0 +1,19 @@ +# PubSub + +## Qos + +PubQos | SubQos | In Message | Out Message +-------|--------|------------|------------- + 0 | 0 | - | - + 0 | 1 | - | - + 0 | 2 | - | - + 1 | 0 | - | - + 1 | 1 | - | - + 1 | 2 | - | - + 2 | 0 | - | - + 2 | 1 | - | - + 2 | 2 | - | - + +## Publish + + diff --git a/doc/retain.md b/doc/retain.md new file mode 100644 index 000000000..535c2e9a8 --- /dev/null +++ b/doc/retain.md @@ -0,0 +1,6 @@ +# Retained Message + +## API + +store( + diff --git a/doc/session.md b/doc/session.md new file mode 100644 index 000000000..fc9211a9b --- /dev/null +++ b/doc/session.md @@ -0,0 +1,50 @@ +# Session Design + +## session manager + +```erlang + +%% lookup sesssion +emqtt_sm:lookup_session(ClientId) + +%% Start new or resume existing session +emqtt_sm:start_session(ClientId) + +%% destroy session, discard all data +emqtt_sm:destory_session(ClientId) + +%% close session, save all data +emqtt_sm:close_session(ClientId) +``` + +## session supervisor + +usage? + +## session + +``` +%%system process +process_flag(trap_exit, true), + +session:start() +session:subscribe( +session:publish( +session:resume( +session:suspend( +%%destory all data +session:destory( +%%save all data +session:close() + +``` + +## sm and session + +sm manage and monitor session + +## client and session + + client(normal process)<--link to -->session(system process) + + diff --git a/doc/state_design.md b/doc/state_design.md new file mode 100644 index 000000000..74a484e37 --- /dev/null +++ b/doc/state_design.md @@ -0,0 +1,4 @@ + + +client state --> parse_state + --> proto_state --> session_state diff --git a/doc/user-guide.md b/doc/user-guide.md new file mode 100644 index 000000000..914876532 --- /dev/null +++ b/doc/user-guide.md @@ -0,0 +1,14 @@ +# eMQTT User Guide + +## Introduction + +## Installation + +### Install Requirements + +## Configuration + +## Cluster + +## Bridge + diff --git a/go b/go new file mode 100755 index 000000000..cd18c5893 --- /dev/null +++ b/go @@ -0,0 +1,5 @@ +#!/bin/sh +# -*- tab-width:4;indent-tabs-mode:nil -*- +# ex: ts=4 sw=4 et + +make && make dist && cd rel/emqtt && ./bin/emqtt console diff --git a/rel/files/app.config b/rel/files/app.config index 3521665f8..f6ae0f077 100644 --- a/rel/files/app.config +++ b/rel/files/app.config @@ -14,7 +14,7 @@ {error_logger_redirect, false}, {crash_log, "log/emqtt_crash.log"}, {handlers, [ - {lager_console_backend, info}, + {lager_console_backend, debug}, {lager_file_backend, [ {file, "log/emqtt_error.log"}, {level, error}, @@ -33,12 +33,20 @@ ]}, {emqtt, [ {auth, {anonymous, []}}, %internal, anonymous + {session, [ + {expires, 1}, + {max_queue, 1000}, + {store_qos0, false} + ]}, + {retain, [ + {store_limit, 100000} + ]}, {listen, [ {mqtt, 1883, [ {max_conns, 1024}, {acceptor_pool, 4} ]}, - {http, 8883, [ + {http, 8083, [ {max_conns, 512}, {acceptor_pool, 1} ]} diff --git a/rel/files/emqtt.cfg b/rel/files/emqtt.cfg new file mode 100644 index 000000000..8b1d8dc3d --- /dev/null +++ b/rel/files/emqtt.cfg @@ -0,0 +1,14 @@ +mqtt_ipaddr=0.0.0.0 +mqtt_port=1883 + +max_clientid_len=1024 + + +#Max Connections +max_connections=10000 + +#Max MQTT Message Size +max_message_size=64k + +#Network ingoing limit +rate_ingoing_limit=64kb/s diff --git a/rel/files/emqtt_ctl b/rel/files/emqtt_ctl index becdee431..444439801 100755 --- a/rel/files/emqtt_ctl +++ b/rel/files/emqtt_ctl @@ -97,27 +97,9 @@ case "$1" in $NODETOOL rpc emqtt_ctl status $@ ;; - cluster_info) - if [ $# -ne 1 ]; then - echo "Usage: $SCRIPT cluster_info" - exit 1 - fi - - # Make sure the local node IS running - RES=`$NODETOOL ping` - if [ "$RES" != "pong" ]; then - echo "Node is not running!" - exit 1 - fi - shift - - $NODETOOL rpc emqtt_ctl cluster_info $@ - ;; - - cluster) - if [ $# -ne 2 ]; then - echo "Usage: $SCRIPT cluster " + if [ $# -gt 2 ]; then + echo "Usage: $SCRIPT cluster []" exit 1 fi @@ -132,9 +114,9 @@ case "$1" in $NODETOOL rpc emqtt_ctl cluster $@ ;; - add_user) + useradd) if [ $# -ne 3 ]; then - echo "Usage: $SCRIPT add_user " + echo "Usage: $SCRIPT useradd " exit 1 fi @@ -146,12 +128,12 @@ case "$1" in fi shift - $NODETOOL rpc emqtt_ctl add_user $@ + $NODETOOL rpc emqtt_ctl useradd $@ ;; - delete_user) + userdel) if [ $# -ne 2 ]; then - echo "Usage: $SCRIPT delete_user " + echo "Usage: $SCRIPT userdel " exit 1 fi @@ -163,16 +145,15 @@ case "$1" in fi shift - $NODETOOL rpc emqtt_ctl delete_user $@ + $NODETOOL rpc emqtt_ctl userdel $@ ;; *) echo "Usage: $SCRIPT" - echo " status #query emqtt status" - echo " cluster_info #query cluster nodes" - echo " cluster #cluster node" - echo " add_user #add user" - echo " delete_user #delete user" + echo " status #query emqtt status" + echo " cluster [] #query or cluster nodes" + echo " useradd #add user" + echo " userdel #delete user" exit 1 ;; diff --git a/rel/files/vm.args b/rel/files/vm.args index cf869a472..1c790f3a1 100644 --- a/rel/files/vm.args +++ b/rel/files/vm.args @@ -1,8 +1,8 @@ ## Name of the node --sname emqtt +-name emqtt@127.0.0.1 ## Cookie for distributed erlang --setcookie emqtt +-setcookie emqttsecretcookie ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive ## (Disabled by default..use with caution!) @@ -14,7 +14,7 @@ +A 32 ## max process numbers -+P 100000 ++P 1000000 ## Increase number of concurrent ports/sockets -env ERL_MAX_PORTS 4096 @@ -25,3 +25,4 @@ ## Tweak GC to run more often ##-env ERL_FULLSWEEP_AFTER 10 +# diff --git a/scripts/mosquitto_test b/scripts/mosquitto_test new file mode 100755 index 000000000..d5a504134 --- /dev/null +++ b/scripts/mosquitto_test @@ -0,0 +1,19 @@ +#!/bin/sh +# -*- tab-width:4;indent-tabs-mode:nil -*- +# ex: ts=4 sw=4 et + +# slimple publish +mosquitto_pub -t xxx/yyy -m hello +if [ "$?" == 0 ]; then + echo "[Success]: slimple publish" +else + echo "[Failure]: slimple publish" +fi + +# publish will willmsg +mosquitto_pub -q 1 -t a/b/c -m hahah -u test -P public --will-topic /will --will-payload willmsg --will-qos 1 +if [ "$?" == 0 ]; then + echo "[Success]: publish with willmsg" +else + echo "[Failure]: publish with willmsg" +fi diff --git a/tests/benchmarks/.placeholder b/tests/benchmarks/.placeholder new file mode 100644 index 000000000..e69de29bb