From 369c5e86c5487a87eb37a1fb69d8a894b1f2e440 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 17 Feb 2016 21:15:34 +0800 Subject: [PATCH 01/69] fix issue #438 - Bridge emqttd broker to another emqttd broker & emqttd to mosquitto bridge --- src/emqttd.app.src | 2 +- src/emqttd_parser.erl | 3 ++- test/emqttd_protocol_SUITE.erl | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/emqttd.app.src b/src/emqttd.app.src index b95538fb4..19e661993 100644 --- a/src/emqttd.app.src +++ b/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.16.0"}, + {vsn, "0.17.0"}, {id, "emqttd"}, {modules, []}, {registered, []}, diff --git a/src/emqttd_parser.erl b/src/emqttd_parser.erl index 8423308a3..5788499b4 100644 --- a/src/emqttd_parser.erl +++ b/src/emqttd_parser.erl @@ -208,6 +208,7 @@ parse_msg(<>, _) -> bool(0) -> false; bool(1) -> true. +%% Fix mosquitto bridge: 0x83, 0x84 protocol_name_approved(Ver, Name) -> - lists:member({Ver, Name}, ?PROTOCOL_NAMES). + lists:member({Ver band 16#0F, Name}, ?PROTOCOL_NAMES). diff --git a/test/emqttd_protocol_SUITE.erl b/test/emqttd_protocol_SUITE.erl index 9b0311c2b..f5f63a185 100644 --- a/test/emqttd_protocol_SUITE.erl +++ b/test/emqttd_protocol_SUITE.erl @@ -33,6 +33,7 @@ all() -> groups() -> [{parser, [], [parse_connect, + parse_bridge, parse_publish, parse_puback, parse_subscribe, @@ -122,6 +123,27 @@ parse_connect(_) -> password = <<"public">>}}, <<>>} = Parser(ConnBinWithWill), ok. +parse_bridge(_) -> + Parser = emqttd_parser:new([]), + Data = <<16,86,0,6,77,81,73,115,100,112,131,44,0,60,0,19,67,95,48,48,58,48,67,58,50,57,58,50,66,58,55,55,58,53,50, + 0,48,36,83,89,83,47,98,114,111,107,101,114,47,99,111,110,110,101,99,116,105,111,110,47,67,95,48,48,58,48, + 67,58,50,57,58,50,66,58,55,55,58,53,50,47,115,116,97,116,101,0,1,48>>, + + %% CONNECT(Q0, R0, D0, ClientId=C_00:0C:29:2B:77:52, ProtoName=MQIsdp, ProtoVsn=131, CleanSess=false, KeepAlive=60, + %% Username=undefined, Password=undefined, Will(Q1, R1, Topic=$SYS/broker/connection/C_00:0C:29:2B:77:52/state, Msg=0)) + {ok, #mqtt_packet{variable = Variable}, <<>>} = Parser(Data), + ct:print("~p", [Variable]), + #mqtt_packet_connect{client_id = <<"C_00:0C:29:2B:77:52">>, + proto_ver = 16#83, + proto_name = <<"MQIsdp">>, + will_retain = true, + will_qos = 1, + will_flag = true, + clean_sess = false, + keep_alive = 60, + will_topic = <<"$SYS/broker/connection/C_00:0C:29:2B:77:52/state">>, + will_msg = <<"0">>} = Variable. + parse_publish(_) -> Parser = emqttd_parser:new([]), %%PUBLISH(Qos=1, Retain=false, Dup=false, TopicName=a/b/c, PacketId=1, Payload=<<"hahah">>) From 5695ba178c3c053d4cc319a2901d9c1416e4ea39 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 17 Feb 2016 23:52:04 +0800 Subject: [PATCH 02/69] cli common tests --- src/emqttd_ctl.erl | 24 ++++++++++--- test/emqttd_SUITE.erl | 78 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/src/emqttd_ctl.erl b/src/emqttd_ctl.erl index a880a674f..757b8c580 100644 --- a/src/emqttd_ctl.erl +++ b/src/emqttd_ctl.erl @@ -25,7 +25,8 @@ -define(SERVER, ?MODULE). %% API Function Exports --export([start_link/0, register_cmd/3, unregister_cmd/1, run/1]). +-export([start_link/0, register_cmd/2, register_cmd/3, unregister_cmd/1, + lookup/1, run/1]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -43,6 +44,11 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %% @doc Register a command +-spec register_cmd(atom(), {module(), atom()}) -> ok. +register_cmd(Cmd, MF) -> + register_cmd(Cmd, MF, []). + +%% @doc Register a command with opts -spec register_cmd(atom(), {module(), atom()}, list()) -> ok. register_cmd(Cmd, MF, Opts) -> cast({register_cmd, Cmd, MF, Opts}). @@ -55,17 +61,25 @@ unregister_cmd(Cmd) -> cast(Msg) -> gen_server:cast(?SERVER, Msg). %% @doc Run a command +-spec run([string()]) -> any(). run([]) -> usage(); run(["help"]) -> usage(); run([CmdS|Args]) -> - Cmd = list_to_atom(CmdS), - case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of - [[{Mod, Fun}]] -> Mod:Fun(Args); + case lookup(list_to_atom(CmdS)) of + [{Mod, Fun}] -> Mod:Fun(Args); [] -> usage() end. - + +%% @doc Lookup a command +-spec lookup(atom()) -> [{module(), atom()}]. +lookup(Cmd) -> + case ets:match(?CMD_TAB, {{'_', Cmd}, '$1', '_'}) of + [El] -> El; + [] -> [] + end. + %% @doc Usage usage() -> ?PRINT("Usage: ~s~n", [?MODULE]), diff --git a/test/emqttd_SUITE.erl b/test/emqttd_SUITE.erl index eca4b5294..6eafdd2dd 100644 --- a/test/emqttd_SUITE.erl +++ b/test/emqttd_SUITE.erl @@ -27,7 +27,8 @@ all() -> {group, retainer}, {group, broker}, {group, metrics}, - {group, stats}]. + {group, stats}, + {group, cli}]. groups() -> [{pubsub, [sequence], @@ -48,7 +49,18 @@ groups() -> {metrics, [sequence], [inc_dec_metric]}, {stats, [sequence], - [set_get_stat]}]. + [set_get_stat]}, + {cli, [sequence], + [ctl_register_cmd, + cli_status, + cli_broker, + cli_clients, + cli_sessions, + cli_topics, + cli_subscriptions, + cli_bridges, + cli_plugins, + cli_listeners]}]. init_per_suite(Config) -> application:start(lager), @@ -212,3 +224,65 @@ inc_dec_metric(_) -> set_get_stat(_) -> emqttd_stats:setstat('retained/max', 99), 99 = emqttd_stats:getstat('retained/max'). + +%%-------------------------------------------------------------------- +%% CLI Group +%%-------------------------------------------------------------------- + +ctl_register_cmd(_) -> + emqttd_ctl:register_cmd(test_cmd, {?MODULE, test_cmd}), + erlang:yield(), + timer:sleep(5), + [{?MODULE, test_cmd}] = emqttd_ctl:lookup(test_cmd), + emqttd_ctl:run(["test_cmd", "arg1", "arg2"]), + emqttd_ctl:unregister_cmd(test_cmd). + +test_cmd(["arg1", "arg2"]) -> + ct:print("test_cmd is called"); + +test_cmd([]) -> + io:format("test command"). + +cli_status(_) -> + emqttd_cli:status([]). + +cli_broker(_) -> + emqttd_cli:broker([]), + emqttd_cli:broker(["stats"]), + emqttd_cli:broker(["metrics"]), + emqttd_cli:broker(["pubsub"]). + +cli_clients(_) -> + emqttd_cli:clients(["list"]), + emqttd_cli:clients(["show", "clientId"]), + emqttd_cli:clients(["kick", "clientId"]). + +cli_sessions(_) -> + emqttd_cli:sessions(["list"]), + emqttd_cli:sessions(["list", "persistent"]), + emqttd_cli:sessions(["list", "transient"]), + emqttd_cli:sessions(["show", "clientId"]). + +cli_topics(_) -> + emqttd_cli:topics(["list"]), + emqttd_cli:topics(["show", "topic"]). + +cli_subscriptions(_) -> + emqttd_cli:subscriptions(["list"]), + emqttd_cli:subscriptions(["show", "clientId"]), + emqttd_cli:subscriptions(["add", "clientId", "topic", "2"]), + emqttd_cli:subscriptions(["del", "clientId", "topic"]). + +cli_plugins(_) -> + emqttd_cli:plugins(["list"]), + emqttd_cli:plugins(["load", "emqttd_plugin_template"]), + emqttd_cli:plugins(["unload", "emqttd_plugin_template"]). + +cli_bridges(_) -> + emqttd_cli:bridges(["list"]), + emqttd_cli:bridges(["start", "a@127.0.0.1", "topic"]), + emqttd_cli:bridges(["stop", "a@127.0.0.1", "topic"]). + +cli_listeners(_) -> + emqttd_cli:listeners([]). + From e40775f1f1af632a6c08adc031da81a5b4e3966c Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 21 Feb 2016 10:06:14 +0800 Subject: [PATCH 03/69] fix issue #438 --- src/emqttd_parser.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqttd_parser.erl b/src/emqttd_parser.erl index 5788499b4..bca942130 100644 --- a/src/emqttd_parser.erl +++ b/src/emqttd_parser.erl @@ -74,7 +74,8 @@ parse_frame(Bin, #mqtt_packet_header{type = Type, qos = Qos} = Header, Length) case {Type, Bin} of {?CONNECT, <>} -> {ProtoName, Rest1} = parse_utf(FrameBin), - <> = Rest1, + %% Fix mosquitto bridge: 0x83, 0x84 + <<_Bridge:4, ProtoVersion:4, Rest2/binary>> = Rest1, <>, _) -> bool(0) -> false; bool(1) -> true. -%% Fix mosquitto bridge: 0x83, 0x84 protocol_name_approved(Ver, Name) -> - lists:member({Ver band 16#0F, Name}, ?PROTOCOL_NAMES). + lists:member({Ver, Name}, ?PROTOCOL_NAMES). From 0d58d2441e4481520096a0c1495b4793ad7cb0ff Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 22 Feb 2016 01:25:59 +0800 Subject: [PATCH 04/69] readthedocs --- docs/Makefile | 177 +++++++++++++++++++ docs/make.bat | 242 ++++++++++++++++++++++++++ docs/source/bridge.rst | 48 ++++++ docs/source/cluster.rst | 135 +++++++++++++++ docs/source/commands.rst | 68 ++++++++ docs/source/conf.py | 337 +++++++++++++++++++++++++++++++++++++ docs/source/config.rst | 78 +++++++++ docs/source/getstarted.rst | 186 ++++++++++++++++++++ docs/source/index.rst | 35 ++++ docs/source/install.rst | 135 +++++++++++++++ docs/source/plugins.rst | 47 ++++++ 11 files changed, 1488 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/bridge.rst create mode 100644 docs/source/cluster.rst create mode 100644 docs/source/commands.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/config.rst create mode 100644 docs/source/getstarted.rst create mode 100644 docs/source/index.rst create mode 100644 docs/source/install.rst create mode 100644 docs/source/plugins.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..167db2e93 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ErlangMQTTBroker.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ErlangMQTTBroker.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/ErlangMQTTBroker" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ErlangMQTTBroker" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 000000000..f8d18dac5 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +set I18NSPHINXOPTS=%SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ErlangMQTTBroker.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ErlangMQTTBroker.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/source/bridge.rst b/docs/source/bridge.rst new file mode 100644 index 000000000..8cc1af69f --- /dev/null +++ b/docs/source/bridge.rst @@ -0,0 +1,48 @@ + +.. _bridge:: + + +==================== +Bridge Guide +==================== + + +------------------- +emqttd Node Bridge +------------------- + +:: + --------- --------- --------- + Publisher --> | node1 | --Bridge Forward--> | node2 | --Bridge Forward--> | node3 | --> Subscriber + --------- --------- --------- + + +----------------- +mosquitto Bridge +----------------- + +:: + ------------- ----------------- + Sensor ----> | mosquitto | --Bridge--> | | + ------------- | emqttd | + ------------- | Cluster | + Sensor ----> | mosquitto | --Bridge--> | | + ------------- ----------------- + + +mosquitto.conf +-------------- + + +------------- +rsmb Bridge +------------- + +broker.cfg +---------- + +:: + connection emqttd + addresses 127.0.0.1:2883 + topic sensor/# + diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst new file mode 100644 index 000000000..bc3956fa8 --- /dev/null +++ b/docs/source/cluster.rst @@ -0,0 +1,135 @@ + +.. _cluster: + +==================== +Cluster Guide +==================== + +----------------------------------- +Erlang/OPT Distributed Programming +----------------------------------- + +.. code:: + + --------- --------- + | Node1 | --------| Node2 | + --------- --------- + | \ / | + | \ / | + | / \ | + | / \ | + --------- --------- + | Node3 | --------| Node4 | + --------- --------- + +Node +---------- + +.. code:: shell + + erl -name node1@127.0.0.1 + erl -name node2@127.0.0.1 + erl -name node3@127.0.0.1 + erl -name node4@127.0.0.1 + + + (node1@127.0.0.1)1> net_kernel:connect_node('node2@127.0.0.1'). + true + (node1@127.0.0.1)2> net_kernel:connect_node('node3@127.0.0.1'). + true + (node1@127.0.0.1)3> net_kernel:connect_node('node4@127.0.0.1'). + true + (node1@127.0.0.1)4> nodes(). + ['node2@127.0.0.1','node3@127.0.0.1','node4@127.0.0.1'] + +epmd +---------- + + (node1@127.0.0.1)6> net_adm:names(). + {ok,[{"node1",62740}, + {"node2",62746}, + {"node3",62877}, + {"node4",62895}]} + +Cookie +-------- + + 1. $HOME/.erlang.cookie + + 2. erl -setcookie + +From: http://erlang.org/doc/reference_manual/distributed.html + +---------------------- +Cluster Design +---------------------- + + + topic1 -> node1, node2 + topic2 -> node3 + topic3 -> node2, node4 + + +Topic Trie and Route Table +--------------------------- + +:: + -------------------------- + | t | + | / \ | + | + # | + | / \ | + | x y | + -------------------------- + | t/+/x -> node1, node3 | + | t/+/y -> node1 | + | t/# -> node2 | + | t/a -> node3 | + -------------------------- + + +Subscription and Message Dispatch +--------------------------------- + +:: + title: Message Route and Deliver + + client1->node1: Publish[t/a] + node1-->node2: Route[t/#] + node1-->node3: Route[t/a] + node2-->client2: Deliver[t/#] + node3-->client3: Deliver[t/a] + +.. image:: _static/images/route.png + + +----------------------- +Cluster Configuration +----------------------- + + + +----------------------- +Sessions across Nodes +----------------------- + + + node1 + ----------- + |-->| session | + | ----------- + node2 | + -------------- | + client-->| connection |<--| + -------------- + + +------------------ +Notice: NetSplit +------------------ + + +------------------------ +Consistent Hash and DHT +------------------------ + diff --git a/docs/source/commands.rst b/docs/source/commands.rst new file mode 100644 index 000000000..fe443a8b3 --- /dev/null +++ b/docs/source/commands.rst @@ -0,0 +1,68 @@ + +.. _commands:: + +=================== +Admin Command Line +=================== + + +---------- +status +---------- + +---------- +broker +---------- + +----------- +cluster +----------- + +----------- +clients +----------- + + +----------- +sessions +----------- + + +----------- +topics +----------- + + +----------------- +subscriptions +----------------- + + +----------- +plugins +----------- + + +---------- +bridges +---------- + +------ +vm +------ + + +--------- +trace +--------- + + +---------- +listeners +---------- + +---------- +mnesia +---------- + + diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 000000000..f96ed0579 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,337 @@ +# -*- coding: utf-8 -*- +# +# Erlang MQTT Broker documentation build configuration file, created by +# sphinx-quickstart on Mon Feb 22 00:46:47 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Erlang MQTT Broker' +copyright = u'2016, Feng Lee' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.0' +# The full version, including alpha/beta/rc tags. +release = '1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'ErlangMQTTBrokerdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'ErlangMQTTBroker.tex', u'Erlang MQTT Broker Documentation', + u'Feng Lee', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'erlangmqttbroker', u'Erlang MQTT Broker Documentation', + [u'Feng Lee'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'ErlangMQTTBroker', u'Erlang MQTT Broker Documentation', + u'Feng Lee', 'ErlangMQTTBroker', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# -- Options for Epub output ---------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'Erlang MQTT Broker' +epub_author = u'Feng Lee' +epub_publisher = u'Feng Lee' +epub_copyright = u'2016, Feng Lee' + +# The basename for the epub file. It defaults to the project name. +#epub_basename = u'Erlang MQTT Broker' + +# The HTML theme for the epub output. Since the default themes are not optimized +# for small screen space, using the same theme for HTML and epub output is +# usually not wise. This defaults to 'epub', a theme designed to save visual +# space. +#epub_theme = 'epub' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +#epub_cover = () + +# A sequence of (type, uri, title) tuples for the guide element of content.opf. +#epub_guide = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + +# Choose between 'default' and 'includehidden'. +#epub_tocscope = 'default' + +# Fix unsupported image types using the PIL. +#epub_fix_images = False + +# Scale large images. +#epub_max_image_width = 0 + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#epub_show_urls = 'inline' + +# If false, no index is generated. +#epub_use_index = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/docs/source/config.rst b/docs/source/config.rst new file mode 100644 index 000000000..5ac233112 --- /dev/null +++ b/docs/source/config.rst @@ -0,0 +1,78 @@ + +.. _configuration: + +====================== +Configuration +====================== + + +------------ +etc/vm.args +------------ + +.. code:: + + ##------------------------------------------------------------------------- + ## Name of the node + ##------------------------------------------------------------------------- + -name emqttd@127.0.0.1 + + ## Cookie for distributed erlang + -setcookie emqttdsecretcookie + + ##------------------------------------------------------------------------- + ## Flags + ##------------------------------------------------------------------------- + + ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive + ## (Disabled by default..use with caution!) + ##-heart + -smp true + + ## Enable kernel poll and a few async threads + +K true + + ## 12 threads/core. + +A 48 + + ## max process numbers + +P 8192 + + ## Sets the maximum number of simultaneously existing ports for this system + +Q 8192 + + ## max atom number + ## +t + + ## Set the distribution buffer busy limit (dist_buf_busy_limit) in kilobytes. + ## Valid range is 1-2097151. Default is 1024. + ## +zdbbl 8192 + + ## CPU Schedulers + ## +sbt db + + ##------------------------------------------------------------------------- + ## Env + ##------------------------------------------------------------------------- + + ## Increase number of concurrent ports/sockets, deprecated in R17 + -env ERL_MAX_PORTS 8192 + + -env ERTS_MAX_PORTS 8192 + + -env ERL_MAX_ETS_TABLES 1024 + + ## Tweak GC to run more often + -env ERL_FULLSWEEP_AFTER 1000 + +------------------ +etc/emqttd.config +------------------ + + +Log Level +------------- + +Broker Parameters +------------------ + diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst new file mode 100644 index 000000000..4be93787c --- /dev/null +++ b/docs/source/getstarted.rst @@ -0,0 +1,186 @@ + +.. _getstarted: + +============ +Get Started +============ + +-------------------- +Overview +-------------------- + + +-------------------- +MQTT PubSub +-------------------- + +-------------------------- +Quick Start +-------------------------- + +.. code:: console + + unzip emqttd-macosx-0.16.0-beta-20160216.zip && cd emqttd + + # Start emqttd + ./bin/emqttd start + + # Check Status + ./bin/emqttd_ctl status + + # Stop emqttd + ./bin/emqttd stop + +-------------------- +Compile from Source +-------------------- + +.. code:: console + + git clone https://github.com/emqtt/emqttd.git + + cd emqttd && make && make dist + +-------------------- +Web Dashboard +-------------------- + +.. image:: ./_static/images/dashboard.png + + +-------------------- +Features List +-------------------- + +* Full MQTT V3.1/V3.1.1 protocol specification support +* QoS0, QoS1, QoS2 Publish and Subscribe +* Session Management and Offline Messages +* Retained Messages Support +* Last Will Message Support +* TCP/SSL Connection Support +* MQTT Over Websocket(SSL) Support +* HTTP Publish API Support +* [$SYS/brokers/#](https://github.com/emqtt/emqtt/wiki/$SYS-Topics-of-Broker) Support +* Client Authentication with clientId, ipaddress +* Client Authentication with username, password. +* Client ACL control with ipaddress, clientid, username. +* Cluster brokers on several servers. +* [Bridge](https://github.com/emqtt/emqttd/wiki/Bridge) brokers locally or remotely +* 500K+ concurrent clients connections per server +* Extensible architecture with Hooks, Modules and Plugins +* Passed eclipse paho interoperability tests + +-------------------- +Modules and Plugins +-------------------- + +Modules +-------- + +* [emqttd_auth_clientid](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with ClientIds +* [emqttd_auth_username](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with Username and Password +* [emqttd_auth_ldap](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with LDAP +* [emqttd_mod_presence](https://github.com/emqtt/emqttd/wiki/Presence) - Publish presence message to $SYS topics when client connected or disconnected +* emqttd_mod_autosub - Subscribe topics when client connected +* [emqttd_mod_rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) - Topics rewrite like HTTP rewrite module + +Plugins +-------- + +* [emqttd_plugin_template](https://github.com/emqtt/emqttd_plugin_template) - Plugin template and demo +* [emqttd_dashboard](https://github.com/emqtt/emqttd_dashboard) - Web Dashboard +* [emqttd_plugin_mysql](https://github.com/emqtt/emqttd_plugin_mysql) - Authentication with MySQL +* [emqttd_plugin_pgsql](https://github.com/emqtt/emqttd_plugin_pgsql) - Authentication with PostgreSQL +* [emqttd_plugin_kafka](https://github.com/emqtt/emqtt_kafka) - Publish MQTT Messages to Kafka +* [emqttd_plugin_redis](https://github.com/emqtt/emqttd_plugin_redis) - Redis Plugin +* [emqttd_plugin_mongo](https://github.com/emqtt/emqttd_plugin_mongo) - MongoDB Plugin +* [emqttd_stomp](https://github.com/emqtt/emqttd_stomp) - Stomp Protocol Plugin +* [emqttd_sockjs](https://github.com/emqtt/emqttd_sockjs) - SockJS(Stomp) Plugin +* [emqttd_recon](https://github.com/emqtt/emqttd_recon) - Recon Plugin + +---------------------------------- +One million Connections Benchmark +---------------------------------- + +Linux Kernel Parameters +----------------------- + +.. code:: + + sysctl -w fs.file-max=2097152 + sysctl -w fs.nr_open=2097152 + +TCP Stack Parameters +----------------------- + +.. code:: + + sysctl -w net.core.somaxconn=65536 + +Erlang VM +----------------- + +emqttd/etc/vm.args:: + + ## max process numbers + +P 2097152 + + ## Sets the maximum number of simultaneously existing ports for this system + +Q 1048576 + + ## Increase number of concurrent ports/sockets + -env ERL_MAX_PORTS 1048576 + + -env ERTS_MAX_PORTS 1048576 + +emqttd.config +----------------- + +emqttd/etc/emqttd.config:: + + {mqtt, 1883, [ + %% Size of acceptor pool + {acceptors, 64}, + + %% Maximum number of concurrent clients + {max_clients, 1000000}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Connection Options + {connopts, [ + %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec + %% {rate_limit, "100,10"} %% 100K burst, 10K rate + ]}, + ... + +Test Client +----------- + +.. code:: + + sysctl -w net.ipv4.ip_local_port_range="500 65535" + echo 1000000 > /proc/sys/fs/nr_open + +---------------------------- +emqtt MQTT Client Libraries +---------------------------- + +GitHub: https://github.com/emqtt + ++--------------------+----------------------+ +| `emqttc`_ | Erlang MQTT Client | ++--------------------+----------------------+ +| `emqtt_benchmark`_ | MQTT benchmark Tool | ++--------------------+----------------------+ +| `CocoaMQTT`_ | Swift MQTT Client | ++--------------------+----------------------+ +| `QMQTT`_ | QT MQTT Client | ++--------------------+----------------------+ + +.. _emqttc: https://github.com/emqtt/emqttc +.. _emqtt_benchmark: https://github.com/emqtt/emqtt_benchmark +.. _CocoaMQTT: https://github.com/emqtt/CocoaMQTT +.. _QMQTT: https://github.com/emqtt/qmqtt + diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 000000000..72d41c8e7 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,35 @@ +.. Erlang MQTT Broker documentation master file, created by + sphinx-quickstart on Mon Feb 22 00:46:47 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Erlang MQTT Broker's documentation! +============================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + getstarted + install + config + cluster + bridge + commands + plugins + +Support and Contacts: + ++------------+--------------------------------+ +| Site: | http://emqtt.io | ++------------+--------------------------------+ +| GitHub: | https://github.com/emqtt | ++------------+--------------------------------+ +| Twitter: | @emqtt | ++------------+--------------------------------+ +| Author: | Feng Lee | ++------------+--------------------------------+ + + + diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 000000000..4ba42200f --- /dev/null +++ b/docs/source/install.rst @@ -0,0 +1,135 @@ + +======================= +Installation +======================= + + +.. NOTE:: + + Linux, FreeBSD Recommended. + +---------------- +Download +---------------- + +Download binary package from: http://emqtt.io/downloads + ++-----------+-----------------------------------+ +| Ubuntu | http://emqtt.io/downloads/ubuntu | ++-----------+-----------------------------------+ +| CentOS | http://emqtt.io/downloads/centos | ++-----------+-----------------------------------+ +| FreeBSD | http://emqtt.io/downloads/freebsd | ++-----------+-----------------------------------+ +| Mac OS X | http://emqtt.io/downloads/macosx | ++-----------+-----------------------------------+ +| Windows | http://emqtt.io/downloads/windows | ++-----------+-----------------------------------+ + + +-------------------- +Installing on Linux +-------------------- + +CentOS: http://emqtt.io/downloads/centos + +.. code:: console + + unzip emqttd-centos64-0.16.0-beta-20160216.zip + + + +.. code:: console + + cd emqttd && ./bin/emqttd console + + +.. code:: console + + starting emqttd on node 'emqttd@127.0.0.1' + emqttd ctl is starting...[done] + emqttd trace is starting...[done] + emqttd pubsub is starting...[done] + emqttd stats is starting...[done] + emqttd metrics is starting...[done] + emqttd retainer is starting...[done] + emqttd pooler is starting...[done] + emqttd client manager is starting...[done] + emqttd session manager is starting...[done] + emqttd session supervisor is starting...[done] + emqttd broker is starting...[done] + emqttd alarm is starting...[done] + emqttd mod supervisor is starting...[done] + emqttd bridge supervisor is starting...[done] + emqttd access control is starting...[done] + emqttd system monitor is starting...[done] + http listen on 0.0.0.0:18083 with 4 acceptors. + mqtt listen on 0.0.0.0:1883 with 16 acceptors. + mqtts listen on 0.0.0.0:8883 with 4 acceptors. + http listen on 0.0.0.0:8083 with 4 acceptors. + Erlang MQTT Broker 0.16.0 is running now + Eshell V6.4 (abort with ^G) + (emqttd@127.0.0.1)1> + + +.. code:: console + + ./bin/emqttd start + + +.. code:: console + + ./bin/emqttd_ctl status + + +.. code:: console + + $ ./bin/emqttd_ctl status + Node 'emqttd@127.0.0.1' is started + emqttd 0.16.0 is running + + + http://localhost:8083/status + + + ./bin/emqttd stop + +--------------------- +Installing on FreeBSD +--------------------- + +FreeBSD: http://emqtt.io/downloads/freebsd + +----------------------- +Installing on Mac +----------------------- + +.. code:: erlang + +----------------------- +Installing on Windows +----------------------- + +----------------------- +Installing From Source +----------------------- + +.. code:: console + + git clone https://github.com/emqtt/emqttd.git + + cd emqttd + + make && make dist + +Binary Package:: + + rel/emqttd + +------------------- +/etc/init.d/emqttd +------------------- + + + + diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst new file mode 100644 index 000000000..f5bba4374 --- /dev/null +++ b/docs/source/plugins.rst @@ -0,0 +1,47 @@ + +.. _plugins: + +================= +Plugins +================= + +------------------------------------ +emqttd_plugin_template +------------------------------------ + +----------------------------------- +emqttd_dashboard: Dashboard Plugin +----------------------------------- + +Configure Dashboard +-------------------- + +------------------------------------------- +emqttd_plugin_mysql: +------------------------------------------- + +------------------------------------------------ +emqttd_plugin_pgsql: +------------------------------------------------ + +-------------------------------------------- +emqttd_plugin_redis: +-------------------------------------------- + +----------------------------- +emqttd_stomp: +----------------------------- + +-------------------------------- +emqttd_sockjs: +-------------------------------- + +-------------------------------- +emqttd_recon: Recon Plugin +-------------------------------- + +------------------------- +Plugin Development Guide +------------------------- + + From b673a4914cfc695efaf1c62092b2a765c46c333f Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 22 Feb 2016 01:30:58 +0800 Subject: [PATCH 05/69] command line --- docs/source/commands.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/commands.rst b/docs/source/commands.rst index fe443a8b3..9e4f536ca 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -2,7 +2,7 @@ .. _commands:: =================== -Admin Command Line +Command Line =================== From 10b4d769598287397611bb33d2bf21cf5d6b6c5a Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 22 Feb 2016 01:31:57 +0800 Subject: [PATCH 06/69] setup --- docs/source/cluster.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst index bc3956fa8..c9e3049a2 100644 --- a/docs/source/cluster.rst +++ b/docs/source/cluster.rst @@ -104,7 +104,7 @@ Subscription and Message Dispatch ----------------------- -Cluster Configuration +Cluster Setup ----------------------- From 64a1db2bef27acda9d39d3cb7680a22314097626 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 22 Feb 2016 01:36:27 +0800 Subject: [PATCH 07/69] fix catalog --- docs/source/cluster.rst | 2 +- docs/source/getstarted.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst index c9e3049a2..c5fa64266 100644 --- a/docs/source/cluster.rst +++ b/docs/source/cluster.rst @@ -6,7 +6,7 @@ Cluster Guide ==================== ----------------------------------- -Erlang/OPT Distributed Programming +Erlang/OPT Distributed ----------------------------------- .. code:: diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index 4be93787c..d3932547a 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -99,7 +99,7 @@ Plugins * [emqttd_recon](https://github.com/emqtt/emqttd_recon) - Recon Plugin ---------------------------------- -One million Connections Benchmark +One million Connections ---------------------------------- Linux Kernel Parameters @@ -164,7 +164,7 @@ Test Client echo 1000000 > /proc/sys/fs/nr_open ---------------------------- -emqtt MQTT Client Libraries +emqtt Client Libraries ---------------------------- GitHub: https://github.com/emqtt From f14052599e07893ae8539d3608564d7dbe2e0686 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 22 Feb 2016 23:45:03 +0800 Subject: [PATCH 08/69] rm Guide --- docs/source/bridge.rst | 2 +- docs/source/cluster.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/bridge.rst b/docs/source/bridge.rst index 8cc1af69f..150ad16a8 100644 --- a/docs/source/bridge.rst +++ b/docs/source/bridge.rst @@ -3,7 +3,7 @@ ==================== -Bridge Guide +Bridge ==================== diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst index c5fa64266..8415aef3f 100644 --- a/docs/source/cluster.rst +++ b/docs/source/cluster.rst @@ -2,7 +2,7 @@ .. _cluster: ==================== -Cluster Guide +Cluster ==================== ----------------------------------- From de6fcbed7d01cc82fb9d21272d6412c88865311f Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 22 Feb 2016 23:57:09 +0800 Subject: [PATCH 09/69] coap, mqtt --- docs/source/coap.rst | 3 +++ docs/source/design.rst | 3 +++ docs/source/mqtt.rst | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 docs/source/coap.rst create mode 100644 docs/source/design.rst create mode 100644 docs/source/mqtt.rst diff --git a/docs/source/coap.rst b/docs/source/coap.rst new file mode 100644 index 000000000..32d482e17 --- /dev/null +++ b/docs/source/coap.rst @@ -0,0 +1,3 @@ +============== +CoAP Protocol +============== diff --git a/docs/source/design.rst b/docs/source/design.rst new file mode 100644 index 000000000..2b8f7e884 --- /dev/null +++ b/docs/source/design.rst @@ -0,0 +1,3 @@ +============== +Design Guide +============== diff --git a/docs/source/mqtt.rst b/docs/source/mqtt.rst new file mode 100644 index 000000000..f0f1849dd --- /dev/null +++ b/docs/source/mqtt.rst @@ -0,0 +1,3 @@ +============== +MQTT Protocol +============== From 638a9ad797c3a304b6a9c29e23df7fdef9f824b7 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 23 Feb 2016 12:06:18 +0800 Subject: [PATCH 10/69] bridge --- docs/source/bridge.rst | 107 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 13 deletions(-) diff --git a/docs/source/bridge.rst b/docs/source/bridge.rst index 150ad16a8..da9ccbd49 100644 --- a/docs/source/bridge.rst +++ b/docs/source/bridge.rst @@ -1,27 +1,98 @@ .. _bridge:: +============ +Bridge Guide +============ -==================== -Bridge -==================== +------------- +emqttd Bridge +------------- +Two or more emqttd brokers could be bridged together. Bridge forward PUBLISH message from one broker node to another:: -------------------- -emqttd Node Bridge -------------------- - -:: --------- --------- --------- Publisher --> | node1 | --Bridge Forward--> | node2 | --Bridge Forward--> | node3 | --> Subscriber --------- --------- --------- +Configure Bridge +---------------- + +We create two emqttd brokers on localhost, for example: + ++---------+---------------------+----------+ +| Name | Node | MQTT Port | ++---------+---------------------+----------+ +| emqttd1 | emqttd1@127.0.0.1 | 1883 | ++---------+---------------------+----------+ +| emqttd2 | emqttd2@127.0.0.1 | 2883 | ++---------+---------------------+----------+ + +And then create a bridge that forwards all the 'sensor/#' messages from emqttd1 to emqttd2. + +1. Start Brokers +................ + +.. code:: console + + cd emqttd1/ && ./bin/emqttd start + cd emqttd2/ && ./bin/emqttd start + +2. Create bridge: emqttd1--sensor/#-->emqttd2 +............................................. + +.. code:: console + + $ cd emqttd1 && ./bin/emqttd_ctl bridges start emqttd2@127.0.0.1 sensor/# + + bridge is started. + + $ ./bin/emqttd_ctl bridges list + + bridge: emqttd1@127.0.0.1--sensor/#-->emqttd2@127.0.0.1 + +3. Test the bridge +................... + +.. code:: console + + #emqttd2 + mosquitto_sub -t sensor/# -p 2883 -d + + #emqttd1 + mosquitto_pub -t sensor/1/temperature -m "37.5" -d + +4. Delete the bridge +..................... + +.. code:: console + + ./bin/emqttd_ctl bridges stop emqttd2@127.0.0.1 sensor/# + +------------------ +emqttd Bridge CLI +----------------- + +.. code:: console + + #query bridges + ./bin/emqttd_ctl bridges list + + #start bridge + ./bin/emqttd_ctl bridges start + + #start bridge with options + ./bin/emqttd_ctl bridges start + + #stop bridge + ./bin/emqttd_ctl bridges stop ----------------- mosquitto Bridge ----------------- -:: +Bridge mosquitto to emqttd broker:: + ------------- ----------------- Sensor ----> | mosquitto | --Bridge--> | | ------------- | emqttd | @@ -29,19 +100,29 @@ mosquitto Bridge Sensor ----> | mosquitto | --Bridge--> | | ------------- ----------------- - mosquitto.conf -------------- +Suppose that we start an emqttd broker on localost:2883, and mosquitto on localhost:1883. + +A bridge configured in mosquitto.conf:: + + connection emqttd + address 127.0.0.1:2883 + topic sensor/# out 2 + + # Set the version of the MQTT protocol to use with for this bridge. Can be one + # of mqttv31 or mqttv311. Defaults to mqttv31. + bridge_protocol_version mqttv311 ------------- rsmb Bridge ------------- -broker.cfg ----------- +Bridge a RSMB to emqttd broker, same settings as mosquitto. + +broker.cfg:: -:: connection emqttd addresses 127.0.0.1:2883 topic sensor/# From 1f5e4911f386d88e0fd92ead57a40305706bbe38 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 23 Feb 2016 14:38:19 +0800 Subject: [PATCH 11/69] cluster --- docs/source/_static/images/cluster.png | Bin 0 -> 26430 bytes docs/source/_static/images/concept.png | Bin 0 -> 26430 bytes docs/source/_static/images/dashboard.png | Bin 0 -> 114427 bytes docs/source/_static/images/emqtt.png | Bin 0 -> 50559 bytes .../_static/images/erlang_supervisor.png | Bin 0 -> 83681 bytes docs/source/_static/images/pubsub_concept.png | Bin 0 -> 83909 bytes docs/source/_static/images/route.png | Bin 0 -> 21184 bytes docs/source/_static/images/weixin.jpg | Bin 0 -> 41618 bytes docs/source/bridge.rst | 8 +- docs/source/cluster.rst | 179 +++++++++++++++--- 10 files changed, 152 insertions(+), 35 deletions(-) create mode 100644 docs/source/_static/images/cluster.png create mode 100644 docs/source/_static/images/concept.png create mode 100644 docs/source/_static/images/dashboard.png create mode 100644 docs/source/_static/images/emqtt.png create mode 100644 docs/source/_static/images/erlang_supervisor.png create mode 100644 docs/source/_static/images/pubsub_concept.png create mode 100644 docs/source/_static/images/route.png create mode 100644 docs/source/_static/images/weixin.jpg diff --git a/docs/source/_static/images/cluster.png b/docs/source/_static/images/cluster.png new file mode 100644 index 0000000000000000000000000000000000000000..77c7dcc9e2924440da9ba1f9e81e2c082b874dcb GIT binary patch literal 26430 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/concept.png b/docs/source/_static/images/concept.png new file mode 100644 index 0000000000000000000000000000000000000000..77c7dcc9e2924440da9ba1f9e81e2c082b874dcb GIT binary patch literal 26430 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/dashboard.png b/docs/source/_static/images/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..7506ccb392aeceeaf05eb3a7a64a2f533bf789c9 GIT binary patch literal 114427 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/emqtt.png b/docs/source/_static/images/emqtt.png new file mode 100644 index 0000000000000000000000000000000000000000..ab2b5fd249ce6ab2daa74f696f951157f87c4773 GIT binary patch literal 50559 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 E0HRv9UH||9 literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/erlang_supervisor.png b/docs/source/_static/images/erlang_supervisor.png new file mode 100644 index 0000000000000000000000000000000000000000..37ccecf8ea24c4b84a439cda2b9ab05e46abe934 GIT binary patch literal 83681 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 M%i_=Cb60NsA9g-IEC2ui literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/pubsub_concept.png b/docs/source/_static/images/pubsub_concept.png new file mode 100644 index 0000000000000000000000000000000000000000..2d2a8aed63bdb5d7cb546cf356af100530bb0015 GIT binary patch literal 83909 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

BtQ*0!9?wV8K5T_tl|U&9*tp-vmOGtatd4 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

EYjLQ1<)QR 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 Q1Mo*uR92)yNH5_308Q9@=Kufz literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/route.png b/docs/source/_static/images/route.png new file mode 100644 index 0000000000000000000000000000000000000000..95265635e44423855345b32cb252b059e37d3787 GIT binary patch literal 21184 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/images/weixin.jpg b/docs/source/_static/images/weixin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e1ad7c351eefcc25bd5d8f651f5dcf74bb2bab5b GIT binary patch literal 41618 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 I_*>Qg1#$A^_W%F@ literal 0 HcmV?d00001 diff --git a/docs/source/bridge.rst b/docs/source/bridge.rst index da9ccbd49..b048167a7 100644 --- a/docs/source/bridge.rst +++ b/docs/source/bridge.rst @@ -9,7 +9,7 @@ Bridge Guide emqttd Bridge ------------- -Two or more emqttd brokers could be bridged together. Bridge forward PUBLISH message from one broker node to another:: +Two or more emqttd brokers could be bridged together. Bridge forward MQTT messages from one broker node to another:: --------- --------- --------- Publisher --> | node1 | --Bridge Forward--> | node2 | --Bridge Forward--> | node3 | --> Subscriber @@ -18,7 +18,7 @@ Two or more emqttd brokers could be bridged together. Bridge forward PUBLISH mes Configure Bridge ---------------- -We create two emqttd brokers on localhost, for example: +Suppose that We create two emqttd brokers on localhost:: +---------+---------------------+----------+ | Name | Node | MQTT Port | @@ -28,7 +28,7 @@ We create two emqttd brokers on localhost, for example: | emqttd2 | emqttd2@127.0.0.1 | 2883 | +---------+---------------------+----------+ -And then create a bridge that forwards all the 'sensor/#' messages from emqttd1 to emqttd2. +Create a bridge that forwards all the 'sensor/#' messages from emqttd1 to emqttd2. 1. Start Brokers ................ @@ -69,7 +69,7 @@ And then create a bridge that forwards all the 'sensor/#' messages from emqttd1 ./bin/emqttd_ctl bridges stop emqttd2@127.0.0.1 sensor/# ------------------- +----------------- emqttd Bridge CLI ----------------- diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst index 8415aef3f..da0b0e48a 100644 --- a/docs/source/cluster.rst +++ b/docs/source/cluster.rst @@ -1,13 +1,15 @@ .. _cluster: -==================== -Cluster -==================== +============= +Cluster Guide +============= ------------------------------------ -Erlang/OPT Distributed ------------------------------------ +---------------------- +Distributed Erlang/OPT +---------------------- + +Erlang/OTP is a concurrent, fault-tolerant, distributed programming platform. A distributed Erlang/OTP system consists of a number of Erlang runtime systems called 'node'. Nodes connect each oother with TCP/IP sockets and communite by Message Passing. .. code:: @@ -23,15 +25,20 @@ Erlang/OPT Distributed --------- --------- Node ----------- +---- -.. code:: shell +An distributed erlang runtime system called 'node' is identified by a unique name like email addreass. Erlang nodes communicate with each other by the name. + +Suppose we start four Erlang nodes on localhost: + +.. code:: console erl -name node1@127.0.0.1 erl -name node2@127.0.0.1 erl -name node3@127.0.0.1 erl -name node4@127.0.0.1 +connect all the nodes:: (node1@127.0.0.1)1> net_kernel:connect_node('node2@127.0.0.1'). true @@ -43,7 +50,11 @@ Node ['node2@127.0.0.1','node3@127.0.0.1','node4@127.0.0.1'] epmd ----------- +---- + +epmd(Erlang Port Mapper Daemon) is a daemon service that is responsible for mapping node names to machine addresses(TCP sockets). The daemon is started automatically on every host where an Erlang node started. + +.. code:: console (node1@127.0.0.1)6> net_adm:names(). {ok,[{"node1",62740}, @@ -52,28 +63,53 @@ epmd {"node4",62895}]} Cookie --------- +------ + +Erlang nodes authenticate each other by a magic cookie when communicating. The cookie could be configured by:: 1. $HOME/.erlang.cookie 2. erl -setcookie -From: http://erlang.org/doc/reference_manual/distributed.html +.. NOTE:: Content of this chapter is from: http://erlang.org/doc/reference_manual/distributed.html ----------------------- -Cluster Design ----------------------- +--------------------- +emqttd Cluster Design +--------------------- +The cluster architecture of emqttd broker is based on distrubuted Erlang/OTP and Mnesia database. + +The cluster design could be summarized by the following two rules:: + +1. When a MQTT client SUBSCRIBE a Topic on a node, the node will tell all the other nodes in the cluster: I subscribed a Topic. + +2. When a MQTT Client PUBLISH a message to a node, the node will lookup the Topic table and forard the message to nodes that subscribed the Topic. + +Finally there will be a global route table(Topic -> Node) that replicated to all nodes in the cluster:: topic1 -> node1, node2 topic2 -> node3 topic3 -> node2, node4 - Topic Trie and Route Table ---------------------------- +-------------------------- + +Every node in the cluster will store a topic trie and route table in mnesia database. + +Suppose that we create subscriptions:: + ++----------------+-------------+----------------------------+ +| Client | Node | Topics | ++----------------+-------------+----------------------------+ +| client1 | node1 | t/+/x, t/+/y | ++----------------+-------------+----------------------------+ +| client2 | node2 | t/# | ++----------------+-------------+----------------------------+ +| client3 | node3 | t/+/x, t/a | ++----------------+-------------+----------------------------+ + +Finally the global topic trie and route table in the cluster:: -:: -------------------------- | t | | / \ | @@ -87,11 +123,13 @@ Topic Trie and Route Table | t/a -> node3 | -------------------------- +Message Route and Deliver +-------------------------- -Subscription and Message Dispatch ---------------------------------- +The brokers in the cluster route messages by topic trie and route table, deliver messages to MQTT clients by subscriptions. Subscriptions are mapping from topic to subscribers, are stored only in the local node, will not be replicated to other nodes. + +Suppose client1 PUBLISH a message to the topic 't/a', the message Route and Deliver process:: -:: title: Message Route and Deliver client1->node1: Publish[t/a] @@ -102,17 +140,92 @@ Subscription and Message Dispatch .. image:: _static/images/route.png - ------------------------ +------------- Cluster Setup ------------------------ +------------- + +Suppose we deploy two nodes cluster on host1, host2: + ++----------------+-----------+---------------------+ +| Node | Host | IP and Port | ++----------------+-----------+---------------------+ +| emqttd@host1 | host1 | 192.168.1.10:1883 | ++----------------+-----------+---------------------+ +| emqttd@host2 | host2 | 192.168.1.20:1883 | ++----------------+-----------+---------------------+ + +emqttd@host1 setting +-------------------- + +emqttd/etc/vm.args:: + + -name emqttd@host1 + + or + + -name emqttd@192.168.0.10 + +.. WARNING:: The name cannot be changed after node joined the cluster. + +emqttd@host2 setting +-------------------- + +emqttd/etc/vm.args:: + + -name emqttd@host2 + + or + + -name emqttd@192.168.0.20 + +Join the cluster +---------------- + +Start the two broker nodes, and 'cluster join ' on emqttd@host2:: + + $ ./bin/emqttd_ctl cluster join emqttd@host1 + + Join the cluster successfully. + Cluster status: [{running_nodes,['emqttd@host1','emqttd@host2']}] + +Or 'cluster join' on emqttd@host1:: + + $ ./bin/emqttd_ctl cluster join emqttd@host2 + + Join the cluster successfully. + Cluster status: [{running_nodes,['emqttd@host1','emqttd@host2']}] + +Query the cluster status:: + + $ ./bin/emqttd_ctl cluster status + + Cluster status: [{running_nodes,['emqttd@host1','emqttd@host2']}] + +Leave the cluster +----------------- + +Two ways to leave the cluster: + +1. leave: this node leave the cluster + +2. remove: remove other nodes from the cluster + +emqttd@host2 node tried to leave the cluster:: + + $ ./bin/emqttd_ctl cluster leave + +Or remove the emqttd@host2 node from the cluster on emqttd@host1:: + + $ ./bin/emqttd_ctl cluster remove emqttd@host2 +-------------------- +Session across Nodes +-------------------- ------------------------ -Sessions across Nodes ------------------------ +The persistent MQTT sessions (clean session = false) are across nodes in the cluster. +If a persistent MQTT client connected to node1 first, then disconnected and connects to node2, the MQTT connection and session will be located on different nodes:: node1 ----------- @@ -123,13 +236,17 @@ Sessions across Nodes client-->| connection |<--| -------------- - ------------------- +---------------- Notice: NetSplit ------------------- +---------------- + +The emqttd cluster does not support deployment across IDC, and the cluster will not handle NetSplit automatically. If NetSplit occures, nodes have to be rebooted manually. ------------------------- +----------------------- Consistent Hash and DHT ------------------------- +----------------------- + +Consistent Hash and DHT are popular in the design of NoSQL databases. Cluster of emqttd broker could support 10 millions size of global routing table now. We could use the Consistent Hash or DHT to partition the routing table, and evolve the cluster to larger size. + From 209593254887a896a26902030e0bd96ade6530ce Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 23 Feb 2016 16:10:31 +0800 Subject: [PATCH 12/69] install --- docs/TODO | 8 + docs/source/bridge.rst | 18 +- docs/source/cluster.rst | 26 +- docs/source/commands.rst | 623 +++++++++++++++++++++++++++++++++++-- docs/source/getstarted.rst | 9 +- docs/source/guide.rst | 18 ++ docs/source/index.rst | 1 + docs/source/install.rst | 245 +++++++++++++-- docs/source/plugins.rst | 4 +- 9 files changed, 884 insertions(+), 68 deletions(-) create mode 100644 docs/TODO create mode 100644 docs/source/guide.rst diff --git a/docs/TODO b/docs/TODO new file mode 100644 index 000000000..a4defbe42 --- /dev/null +++ b/docs/TODO @@ -0,0 +1,8 @@ + +1. Getstarted + +2. Commands + +3. Plugins + +4. User Guide diff --git a/docs/source/bridge.rst b/docs/source/bridge.rst index b048167a7..73539c8b4 100644 --- a/docs/source/bridge.rst +++ b/docs/source/bridge.rst @@ -9,7 +9,7 @@ Bridge Guide emqttd Bridge ------------- -Two or more emqttd brokers could be bridged together. Bridge forward MQTT messages from one broker node to another:: +Two or more emqttd brokers could be bridged together. Bridges forward MQTT messages from one broker node to another:: --------- --------- --------- Publisher --> | node1 | --Bridge Forward--> | node2 | --Bridge Forward--> | node3 | --> Subscriber @@ -18,15 +18,15 @@ Two or more emqttd brokers could be bridged together. Bridge forward MQTT messag Configure Bridge ---------------- -Suppose that We create two emqttd brokers on localhost:: +Suppose that we create two emqttd brokers on localhost: -+---------+---------------------+----------+ ++---------+---------------------+-----------+ | Name | Node | MQTT Port | -+---------+---------------------+----------+ -| emqttd1 | emqttd1@127.0.0.1 | 1883 | -+---------+---------------------+----------+ -| emqttd2 | emqttd2@127.0.0.1 | 2883 | -+---------+---------------------+----------+ ++---------+---------------------+-----------+ +| emqttd1 | emqttd1@127.0.0.1 | 1883 | ++---------+---------------------+-----------+ +| emqttd2 | emqttd2@127.0.0.1 | 2883 | ++---------+---------------------+-----------+ Create a bridge that forwards all the 'sensor/#' messages from emqttd1 to emqttd2. @@ -119,7 +119,7 @@ A bridge configured in mosquitto.conf:: rsmb Bridge ------------- -Bridge a RSMB to emqttd broker, same settings as mosquitto. +Bridge RSMB to emqttd broker, same settings as mosquitto. broker.cfg:: diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst index da0b0e48a..5ee1dfa11 100644 --- a/docs/source/cluster.rst +++ b/docs/source/cluster.rst @@ -9,7 +9,7 @@ Cluster Guide Distributed Erlang/OPT ---------------------- -Erlang/OTP is a concurrent, fault-tolerant, distributed programming platform. A distributed Erlang/OTP system consists of a number of Erlang runtime systems called 'node'. Nodes connect each oother with TCP/IP sockets and communite by Message Passing. +Erlang/OTP is a concurrent, fault-tolerant, distributed programming platform. A distributed Erlang/OTP system consists of a number of Erlang runtime systems called 'node'. Nodes connect to each other with TCP/IP sockets and communite by Message Passing. .. code:: @@ -27,7 +27,7 @@ Erlang/OTP is a concurrent, fault-tolerant, distributed programming platform. A Node ---- -An distributed erlang runtime system called 'node' is identified by a unique name like email addreass. Erlang nodes communicate with each other by the name. +An erlang runtime system called 'node' is identified by a unique name like email addreass. Erlang nodes communicate with each other by the name. Suppose we start four Erlang nodes on localhost: @@ -96,11 +96,11 @@ Topic Trie and Route Table Every node in the cluster will store a topic trie and route table in mnesia database. -Suppose that we create subscriptions:: +Suppose that we create subscriptions: +----------------+-------------+----------------------------+ | Client | Node | Topics | -+----------------+-------------+----------------------------+ ++================+=============+============================+ | client1 | node1 | t/+/x, t/+/y | +----------------+-------------+----------------------------+ | client2 | node2 | t/# | @@ -108,14 +108,14 @@ Suppose that we create subscriptions:: | client3 | node3 | t/+/x, t/a | +----------------+-------------+----------------------------+ -Finally the global topic trie and route table in the cluster:: +Finally the topic trie and route table in the cluster:: -------------------------- - | t | - | / \ | - | + # | - | / \ | - | x y | + | t | + | / \ | + | + # | + | / \ | + | x y | -------------------------- | t/+/x -> node1, node3 | | t/+/y -> node1 | @@ -206,15 +206,15 @@ Leave the cluster Two ways to leave the cluster: -1. leave: this node leave the cluster +1. leave: this node leaves the cluster 2. remove: remove other nodes from the cluster -emqttd@host2 node tried to leave the cluster:: +emqttd@host2 node tries to leave the cluster:: $ ./bin/emqttd_ctl cluster leave -Or remove the emqttd@host2 node from the cluster on emqttd@host1:: +Or remove emqttd@host2 node from the cluster on emqttd@host1:: $ ./bin/emqttd_ctl cluster remove emqttd@host2 diff --git a/docs/source/commands.rst b/docs/source/commands.rst index 9e4f536ca..a02c231a7 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -1,68 +1,647 @@ .. _commands:: -=================== +============ Command Line -=================== +============ +emqttd消息服务器提供了'./bin/emqttd_ctl'的管理命令行。 + +.. WARNING:: 限制: Windows平台无法使用。 ---------- status ---------- +查询emqttd消息服务器运行状态:: + + $ ./bin/emqttd_ctl status + + Node 'emqttd@127.0.0.1' is started + emqttd 0.16.0 is running + + ---------- broker ---------- ------------ +broker命令查询服务器基本信息,启动时间,统计数据与性能数据。 + ++----------------+-----------------------------------------------+ +| broker | 查询emqttd消息服务器描述、版本、启动时间 | ++----------------+-----------------------------------------------+ +| broker pubsub | 查询核心的Erlang PubSub进程状态(调试) | ++----------------+-----------------------------------------------+ +| broker stats | 查询连接(Client)、会话(Session)、主题(Topic)、| +| | 订阅(Subscription)、路由(Route)统计信息 | ++----------------+-----------------------------------------------+ +| broker metrics | 查询MQTT报文(Packet)、消息(Message)收发统计 | ++----------------+-----------------------------------------------+ + +查询emqttd消息服务器基本信息包括版本、启动时间等:: + + $ ./bin/emqttd_ctl broker + + sysdescr : Erlang MQTT Broker + version : 0.15.0 + uptime : 1 hours, 25 minutes, 24 seconds + datetime : 2016-01-16 13:17:32 + +查询服务器客户端连接(Client)、会话(Session)、主题(Topic)、订阅(Subscription)、路由(Route)统计:: + + $ ./bin/emqttd_ctl broker stats + + clients/count : 1 + clients/max : 1 + queues/count : 0 + queues/max : 0 + retained/count : 2 + retained/max : 2 + routes/count : 2 + routes/reverse : 2 + sessions/count : 0 + sessions/max : 0 + subscriptions/count : 1 + subscriptions/max : 1 + topics/count : 54 + topics/max : 54 + +查询服务器流量(Bytes)、MQTT报文(Packets)、消息(Messages)收发统计:: + + $ ./bin/emqttd_ctl broker metrics + + bytes/received : 297 + bytes/sent : 40 + messages/dropped : 348 + messages/qos0/received : 0 + messages/qos0/sent : 0 + messages/qos1/received : 0 + messages/qos1/sent : 0 + messages/qos2/received : 0 + messages/qos2/sent : 0 + messages/received : 0 + messages/retained : 2 + messages/sent : 0 + packets/connack : 5 + packets/connect : 5 + packets/disconnect : 0 + packets/pingreq : 0 + packets/pingresp : 0 + packets/puback/received : 0 + packets/puback/sent : 0 + packets/pubcomp/received: 0 + packets/pubcomp/sent : 0 + packets/publish/received: 0 + packets/publish/sent : 0 + packets/pubrec/received : 0 + packets/pubrec/sent : 0 + packets/pubrel/received : 0 + packets/pubrel/sent : 0 + packets/received : 9 + packets/sent : 9 + packets/suback : 4 + packets/subscribe : 4 + packets/unsuback : 0 + packets/unsubscribe : 0 + + +------- cluster ------------ +------- ------------ +cluster命令集群多个emqttd消息服务器节点(进程): + ++-----------------------+---------------------+ +| cluster join | 加入集群 | ++-----------------------+---------------------+ +| cluster leave | 离开集群 | ++-----------------------+---------------------+ +| cluster remove | 从集群删除节点 | ++-----------------------+---------------------+ +| cluster status | 查询集群状态 | ++-----------------------+---------------------+ + +cluster命令集群本机两个emqttd节点示例: + ++-----------+---------------------+-------------+ +| 目录 | 节点名 | MQTT端口 | ++-----------+---------------------+-------------+ +| emqttd1 | emqttd1@127.0.0.1 | 1883 | ++-----------+---------------------+-------------+ +| emqttd2 | emqttd2@127.0.0.1 | 2883 | ++-----------+---------------------+-------------+ + +启动emqttd1:: + + cd emqttd1 && ./bin/emqttd start + +启动emqttd2:: + + cd emqttd2 && ./bin/emqttd start + +emqttd2节点与emqttd1集群,emqttd2目录下:: + + $ ./bin/emqttd_ctl cluster join emqttd1@127.0.0.1 + + Join the cluster successfully. + Cluster status: [{running_nodes,['emqttd1@127.0.0.1','emqttd2@127.0.0.1']}] + +任意节点目录下查询集群状态:: + + $ ./bin/emqttd_ctl cluster status + + Cluster status: [{running_nodes,['emqttd2@127.0.0.1','emqttd1@127.0.0.1']}] + +集群消息路由测试:: + + # emqttd1节点上订阅x + mosquitto_sub -t x -q 1 -p 1883 + + # emqttd2节点上向x发布消息 + mosquitto_pub -t x -q 1 -p 2883 -m hello + +emqttd2节点离开集群:: + + cd emqttd2 && ./bin/emqttd_ctl cluster leave + +emqttd1节点下删除emqttd2:: + + cd emqttd1 && ./bin/emqttd_ctl cluster remove emqttd2@127.0.0.1 + +------- clients ------------ +------- +clients命令查询连接的MQTT客户端。 ------------ ++-------------------------+-----------------------------+ +| clients list | 查询全部客户端连接 | ++-------------------------+-----------------------------+ +| clients show | 根据ClientId查询客户端 | ++-------------------------+-----------------------------+ +| clients kick | 根据ClientId踢出客户端 | ++-------------------------+-----------------------------+ + +查询全部客户端连接:: + + $ ./bin/emqttd_ctl clients list + + Client(mosqsub/43832-airlee.lo, clean_sess=true, username=test, peername=127.0.0.1:64896, connected_at=1452929113) + Client(mosqsub/44011-airlee.lo, clean_sess=true, username=test, peername=127.0.0.1:64961, connected_at=1452929275) + ... + +根据ClientId查询客户端:: + + ./bin/emqttd_ctl clients show "mosqsub/43832-airlee.lo" + + Client(mosqsub/43832-airlee.lo, clean_sess=true, username=test, peername=127.0.0.1:64896, connected_at=1452929113) + +根据ClientId踢出客户端:: + + ./bin/emqttd_ctl clients kick "clientid" + +返回Client对象的属性: + ++--------------+-----------------------------+ +| clean_sess | 清除会话标记 | ++--------------+-----------------------------+ +| username | 用户名 | ++--------------+-----------------------------+ +| peername | 对端TCP地址 | ++--------------+-----------------------------+ +| connected_at | 客户端连接时间 | ++--------------+-----------------------------+ + +-------- sessions ------------ +-------- +sessions命令查询MQTT连接会话。emqttd消息服务器会为每个连接创建会话,clean_session标记true,创建临时(transient)会话;clean_session标记为false,创建持久会话(persistent)。 ------------ ++--------------------------+-----------------------------+ +| sessions list | 查询全部会话 | ++--------------------------+-----------------------------+ +| sessions list persistent | 查询全部持久会话 | ++--------------------------+-----------------------------+ +| sessions list transient | 查询全部临时会话 | ++--------------------------+-----------------------------+ +| sessions show | 根据ClientID查询会话 | ++--------------------------+-----------------------------+ + +查询全部会话:: + + $ ./bin/emqttd_ctl sessions list + + Session(clientid, clean_sess=false, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935508) + Session(mosqsub/44101-airlee.lo, clean_sess=true, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935401) + +查询全部持久会话:: + + $ ./bin/emqttd_ctl sessions list persistent + + Session(clientid, clean_sess=false, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935508) + +查询全部临时会话:: + + $ ./bin/emqttd_ctl sessions list transient + + Session(mosqsub/44101-airlee.lo, clean_sess=true, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935401) + +根据ClientId查询会话:: + + $ ./bin/emqttd_ctl sessions show clientid + + Session(clientid, clean_sess=false, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935508) + +返回Session对象属性: + ++-------------------+------------------------------------+ +| clean_sess | false: 持久会话,true: 临时会话 | ++-------------------+------------------------------------+ +| max_inflight | 飞行窗口(最大允许同时下发消息数) | ++-------------------+------------------------------------+ +| inflight_queue | 当前正在下发的消息数 | ++-------------------+------------------------------------+ +| message_queue | 当前缓存消息数 | ++-------------------+------------------------------------+ +| message_dropped | 会话丢掉的消息数 | ++-------------------+------------------------------------+ +| awaiting_rel | 等待客户端发送PUBREL的QoS2消息数 | ++-------------------+------------------------------------+ +| awaiting_ack | 等待客户端响应PUBACK的QoS1/2消息数 | ++-------------------+------------------------------------+ +| awaiting_comp | 等待客户端响应PUBCOMP的QoS2消息数 | ++-------------------+------------------------------------+ +| created_at | 会话创建时间戳 | ++-------------------+------------------------------------+ + +------ topics ------------ +------ +topics命令查询emqttd消息服务器当前的主题(Topic)表。 ------------------ +'topics list'查询全部主题(Topic):: + + $ ./bin/emqttd_ctl topics list + + y: ['emqttd2@127.0.0.1'] + x: ['emqttd1@127.0.0.1','emqttd2@127.0.0.1'] + +'topics show '查询某个主题(Topic):: + + $ ./bin/emqttd_ctl topics show x + + x: ['emqttd1@127.0.0.1','emqttd2@127.0.0.1'] + +返回结果显示主题(Topic)所在集群节点列表。 + +------------- subscriptions ------------------ +------------- + +subscriptions命令查询消息服务器的订阅(Subscription)表。 + ++--------------------------------------------+-------------------------+ +| subscriptions list | 查询全部订阅 | ++--------------------------------------------+-------------------------+ +| subscriptions show | 查询某个ClientId的订阅 | ++--------------------------------------------+-------------------------+ +| subscriptions add | 手工添加一条订阅 | ++--------------------------------------------+-------------------------+ +| subscriptions del | 手工删除一条订阅 | ++--------------------------------------------+-------------------------+ + +查询全部订阅:: + + $ ./bin/emqttd_ctl subscriptions list + + mosqsub/45744-airlee.lo: [{<<"y">>,0},{<<"x">>,0}] + +.. todo:: 打印结果格式需修改。 + +查询某个ClientId的订阅:: + + $ ./bin/emqttd_ctl subscriptions show clientid + + clientid: [{<<"x">>,1},{<<"topic2">>,1},{<<"topic3">>,1}] + +手工添加一条订阅:: + + $ ./bin/emqttd_ctl subscriptions add clientid new_topic 1 + ok + +手工删除一条订阅:: + + $ ./bin/emqttd_ctl subscriptions del clientid new_topic + ok ------------ +------- plugins ------------ +------- ----------- +plugins命令用于加载、卸载、查询插件应用。emqttd消息服务器通过插件扩展认证、定制功能,插件置于plugins/目录下。 + ++---------------------------+-------------------------+ +| plugins list | 列出全部插件(Plugin) | ++---------------------------+-------------------------+ +| plugins load | 加载插件(Plugin) | ++---------------------------+-------------------------+ +| plugins unload | 卸载插件(Plugin) | ++---------------------------+-------------------------+ + +列出插件:: + + $ ./bin/emqttd_ctl plugins list + + Plugin(emqttd_dashboard, version=0.16.0, description=emqttd web dashboard, active=true) + Plugin(emqttd_plugin_mysql, version=0.16.0, description=emqttd Authentication/ACL with MySQL, active=false) + Plugin(emqttd_plugin_pgsql, version=0.16.0, description=emqttd PostgreSQL Plugin, active=false) + Plugin(emqttd_plugin_redis, version=0.16.0, description=emqttd Redis Plugin, active=false) + Plugin(emqttd_plugin_template, version=0.16.0, description=emqttd plugin template, active=false) + Plugin(emqttd_recon, version=0.16.0, description=emqttd recon plugin, active=false) + Plugin(emqttd_stomp, version=0.16.0, description=Stomp Protocol Plugin for emqttd broker, active=false) + +插件属性: + ++-------------+-----------------+ +| version | 插件版本 | ++-------------+-----------------+ +| description | 插件描述 | ++-------------+-----------------+ +| active | 是否已加载 | ++-------------+-----------------+ + +加载插件:: + + $ ./bin/emqttd_ctl plugins load emqttd_recon + + Start apps: [recon,emqttd_recon] + Plugin emqttd_recon loaded successfully. + +卸载插件:: + + $ ./bin/emqttd_ctl plugins unload emqttd_recon + + Plugin emqttd_recon unloaded successfully. + + +------- bridges ----------- +------- ------- +plugins命令用于加载、卸载、查询插件应用。emqttd消息服务器通过插件扩展认证、定制功能,插件置于plugins/目录下。 + ++---------------------------+-------------------------+ +| plugins list | 列出全部插件(Plugin) | ++---------------------------+-------------------------+ +| plugins load | 加载插件(Plugin) | ++---------------------------+-------------------------+ +| plugins unload | 卸载插件(Plugin) | ++---------------------------+-------------------------+ + +列出插件:: + + $ ./bin/emqttd_ctl plugins list + + Plugin(emqttd_dashboard, version=0.16.0, description=emqttd web dashboard, active=true) + Plugin(emqttd_plugin_mysql, version=0.16.0, description=emqttd Authentication/ACL with MySQL, active=false) + Plugin(emqttd_plugin_pgsql, version=0.16.0, description=emqttd PostgreSQL Plugin, active=false) + Plugin(emqttd_plugin_redis, version=0.16.0, description=emqttd Redis Plugin, active=false) + Plugin(emqttd_plugin_template, version=0.16.0, description=emqttd plugin template, active=false) + Plugin(emqttd_recon, version=0.16.0, description=emqttd recon plugin, active=false) + Plugin(emqttd_stomp, version=0.16.0, description=Stomp Protocol Plugin for emqttd broker, active=false) + +插件属性: + ++-------------+-----------------+ +| version | 插件版本 | ++-------------+-----------------+ +| description | 插件描述 | ++-------------+-----------------+ +| active | 是否已加载 | ++-------------+-----------------+ + +加载插件:: + + $ ./bin/emqttd_ctl plugins load emqttd_recon + + Start apps: [recon,emqttd_recon] + Plugin emqttd_recon loaded successfully. + +卸载插件:: + + $ ./bin/emqttd_ctl plugins unload emqttd_recon + + Plugin emqttd_recon unloaded successfully. + + +------- +bridges +------- + +bridges命令用于在多台emqttd服务器节点间创建桥接。 + ++----------------------------------------+---------------------------+ +| bridges list | 查询全部桥接 | ++----------------------------------------+---------------------------+ +| bridges options | 查询创建桥接选项 | ++----------------------------------------+---------------------------+ +| bridges start | 创建桥接 | ++----------------------------------------+---------------------------+ +| bridges start | 创建桥接并带选项设置 | ++----------------------------------------+---------------------------+ +| bridges stop | 删除桥接 | ++----------------------------------------+---------------------------+ + +创建一条emqttd1 -> emqttd2节点的桥接,转发传感器主题(Topic)消息到emqttd2:: + + $ ./bin/emqttd_ctl bridges start emqttd2@127.0.0.1 sensor/# + + bridge is started. + + $ ./bin/emqttd_ctl bridges list + + bridge: emqttd1@127.0.0.1--sensor/#-->emqttd2@127.0.0.1 + +测试emqttd1--sensor/#-->emqttd2的桥接:: + + #emqttd2节点上 + + mosquitto_sub -t sensor/# -p 2883 -d + + #emqttd1节点上 + + mosquitto_pub -t sensor/1/temperature -m "37.5" -d + +查询bridge创建选项设置:: + + $ ./bin/emqttd_ctl bridges options + + Options: + qos = 0 | 1 | 2 + prefix = string + suffix = string + queue = integer + Example: + qos=2,prefix=abc/,suffix=/yxz,queue=1000 + +删除emqttd1--sensor/#-->emqttd2的桥接:: + + $ ./bin/emqttd_ctl bridges stop emqttd2@127.0.0.1 sensor/# + + bridge is stopped. + +-- vm ------- +-- +vm命令用于查询Erlang虚拟机负载、内存、进程、IO信息。 ---------- ++-------------+------------------------+ +| vm all | 查询VM全部信息 | ++-------------+------------------------+ +| vm load | 查询VM负载 | ++-------------+------------------------+ +| vm memory | 查询VM内存 | ++-------------+------------------------+ +| vm process | 查询VM Erlang进程数量 | ++-------------+------------------------+ +| vm io | 查询VM io最大文件句柄 | ++-------------+------------------------+ + +查询VM负载:: + + $ ./bin/emqttd_ctl vm load + + cpu/load1 : 2.21 + cpu/load5 : 2.60 + cpu/load15 : 2.36 + +查询VM内存:: + + $ ./bin/emqttd_ctl vm memory + + memory/total : 23967736 + memory/processes : 3594216 + memory/processes_used : 3593112 + memory/system : 20373520 + memory/atom : 512601 + memory/atom_used : 491955 + memory/binary : 51432 + memory/code : 13401565 + memory/ets : 1082848 + +查询Erlang进程数量:: + + $ ./bin/emqttd_ctl vm process + + process/limit : 8192 + process/count : 221 + +查询IO最大句柄数:: + + $ ./bin/emqttd_ctl vm io + + io/max_fds : 2560 + io/active_fds : 1 + +----- trace +----- + +trace命令用于追踪某个客户端或Topic,打印日志信息到文件。 + ++-----------------------------------+-----------------------------------+ +| trace list | 查询全部开启的追踪 | ++-----------------------------------+-----------------------------------+ +| trace client | 开启Client追踪,日志到文件 | ++-----------------------------------+-----------------------------------+ +| trace client off | 关闭Client追踪 | ++-----------------------------------+-----------------------------------+ +| trace topic | 开启Topic追踪,日志到文件 | ++-----------------------------------+-----------------------------------+ +| trace topic off | 关闭Topic追踪 | ++-----------------------------------+-----------------------------------+ + +开启Client追踪:: + + $ ./bin/emqttd_ctl trace client clientid log/clientid_trace.log + + trace client clientid successfully. + +关闭Client追踪:: + + $ ./bin/emqttd_ctl trace client clientid off + + stop to trace client clientid successfully. + +开启Topic追踪:: + + $ ./bin/emqttd_ctl trace topic topic log/topic_trace.log + + trace topic topic successfully. + +关闭Topic追踪:: + + $ ./bin/emqttd_ctl trace topic topic off + + stop to trace topic topic successfully. + +查询全部开启的追踪:: + + $ ./bin/emqttd_ctl trace list + + trace client clientid -> log/clientid_trace.log + trace topic topic -> log/topic_trace.log + + +--------- +listeners --------- +listeners命令用于查询开启的TCP服务监听器:: ----------- -listeners ----------- + $ ./bin/emqttd_ctl listeners + + listener on http:8083 + acceptors : 4 + max_clients : 64 + current_clients : 0 + shutdown_count : [] + listener on mqtts:8883 + acceptors : 4 + max_clients : 512 + current_clients : 0 + shutdown_count : [] + listener on mqtt:1883 + acceptors : 16 + max_clients : 8192 + current_clients : 1 + shutdown_count : [{closed,1}] + listener on http:18083 + acceptors : 4 + max_clients : 512 + current_clients : 0 + shutdown_count : [] + +listener参数说明: + ++-----------------+-----------------------------------+ +| acceptors | TCP Acceptor池 | ++-----------------+-----------------------------------+ +| max_clients | 最大允许连接数 | ++-----------------+-----------------------------------+ +| current_clients | 当前连接数 | ++-----------------+-----------------------------------+ +| shutdown_count | Socket关闭原因统计 | ++-----------------+-----------------------------------+ ---------- mnesia ---------- +查询mnesia数据库当前状态,用于调试。 diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index d3932547a..ce70535f3 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -1,17 +1,20 @@ .. _getstarted: -============ +=========== Get Started -============ +=========== -------------------- Overview -------------------- +-------------------- +Goals +-------------------- -------------------- -MQTT PubSub +MQTT Protocol -------------------- -------------------------- diff --git a/docs/source/guide.rst b/docs/source/guide.rst new file mode 100644 index 000000000..1cc2c5e15 --- /dev/null +++ b/docs/source/guide.rst @@ -0,0 +1,18 @@ + +.. _guide: + +========== +User Guide +========== + +---------- +MQTT Basic +---------- + +Understanding QoS +------------------ + +-------- +HTTP API +-------- + diff --git a/docs/source/index.rst b/docs/source/index.rst index 72d41c8e7..742e0b798 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,6 +16,7 @@ Contents: config cluster bridge + guide commands plugins diff --git a/docs/source/install.rst b/docs/source/install.rst index 4ba42200f..cd5ac17b2 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -3,14 +3,15 @@ Installation ======================= +emqttd broker is cross-platform, could deploy on Linux, FreeBSD, Mac OS X and Windows. .. NOTE:: Linux, FreeBSD Recommended. ----------------- -Download ----------------- +----------------- +Download Package +----------------- Download binary package from: http://emqtt.io/downloads @@ -26,23 +27,27 @@ Download binary package from: http://emqtt.io/downloads | Windows | http://emqtt.io/downloads/windows | +-----------+-----------------------------------+ +The package name consists of platform, version and release time. + +For example: emqttd-centos64-0.16.0-beta-20160216.zip -------------------- Installing on Linux -------------------- -CentOS: http://emqtt.io/downloads/centos +Download CentOS Package from: http://emqtt.io/downloads/centos, and then unzip: .. code:: console unzip emqttd-centos64-0.16.0-beta-20160216.zip - +Start the broker in console mode:: .. code:: console cd emqttd && ./bin/emqttd console +If the broker started successfully, console will print: .. code:: console @@ -71,16 +76,17 @@ CentOS: http://emqtt.io/downloads/centos Eshell V6.4 (abort with ^G) (emqttd@127.0.0.1)1> +CTRL+C to close the console and stop the broker. + +Start the broker in daemon mode: .. code:: console ./bin/emqttd start +The boot logs in log/emqttd_sasl.log file. -.. code:: console - - ./bin/emqttd_ctl status - +Check the running status of the broker: .. code:: console @@ -88,31 +94,93 @@ CentOS: http://emqtt.io/downloads/centos Node 'emqttd@127.0.0.1' is started emqttd 0.16.0 is running +Or check the status by URL:: http://localhost:8083/status +Stop the broker:: ./bin/emqttd stop + --------------------- Installing on FreeBSD --------------------- -FreeBSD: http://emqtt.io/downloads/freebsd +Download FreeBSD Package from: http://emqtt.io/downloads/freebsd ------------------------ -Installing on Mac ------------------------ +The installing process is same to Linux. + + +---------------------- +Installing on Mac OS X +---------------------- + +We could install the broker on Mac OS X to develop and debug MQTT applications. + +Download Mac Package from: http://emqtt.io/downloads/macosx + +Configure 'lager' log level in 'etc/emqttd.config', all MQTT messages recevied/sent will be printd on console: .. code:: erlang ------------------------ -Installing on Windows ------------------------ + {lager, [ + ... + {handlers, [ + {lager_console_backend, info}, + ... + ]} + ]}, ------------------------ +The install and boot process on Mac is same to Linux. + +--------------------- +Installing on Windows +--------------------- + +Download Package from: http://emqtt.io/downloads/windows. + +Unzip the package to install folder. Open the command line window and 'cd' to the folder. + +Start the broker in console mode:: + + .\bin\emqttd console + +If the broker started successfully, a Erlang console window will popup. + +Close the console window and stop the emqttd broker. Prepare to register emqttd as window service. + +Install emqttd serivce:: + + .\bin\emqttd install + +Start emqttd serivce:: + + .\bin\emqttd start + +Stop emqttd serivce:: + + .\bin\emqttd stop + +Uninstall emqttd service:: + + .\bin\emqttd uninstall + +.. WARNING:: './bin/emqttd_ctl' command line cannot work on Windows. + +---------------------- Installing From Source ------------------------ +---------------------- + +The emqttd broker requires Erlang/OTP R17+ and git client to build: + +Install Erlang: http://www.erlang.org/ + +Install Git Client: http://www.git-scm.com/ + +Could use apt-get on Ubuntu, yum on CentOS/RedHat and brew on Mac to install Erlang and Git. + +When all dependencies are ready, clone the emqttd project from github.com and build: .. code:: console @@ -122,14 +190,153 @@ Installing From Source make && make dist -Binary Package:: +The binary package output in folder:: rel/emqttd +------------------ +TCP Ports Occupied +------------------ + ++-----------+-----------------------------------+ +| 1883 | MQTT Port | ++-----------+-----------------------------------+ +| 8883 | MQTT Over SSL Port | ++-----------+-----------------------------------+ +| 8083 | MQTT(WebSocket), HTTP API Port | ++-----------+-----------------------------------+ +| 18083 | Dashboard Port | ++-----------+-----------------------------------+ + +The TCP ports could be configured in etc/emqttd.config: + +.. code:: erlang + + {listeners, [ + {mqtt, 1883, [ + ... + ]}, + + {mqtts, 8883, [ + ... + ]}, + %% HTTP and WebSocket Listener + {http, 8083, [ + ... + ]} + ]}, + +The 18083 port is used by Web Dashboard of the broker. Default login: admin, Password: public + +----------- +Quick Setup +----------- + +emqttd消息服务器主要配置文件: + ++-------------------+-----------------------------------+ +| etc/vm.args | Erlang VM的启动参数设置 | ++-------------------+-----------------------------------+ +| etc/emqttd.config | emqttd消息服务器参数设置 | ++-------------------+-----------------------------------+ + +etc/vm.args中两个重要的启动参数: + ++-------+------------------------------------------------------------------+ +| +P | Erlang虚拟机允许的最大进程数,emqttd一个连接会消耗2个Erlang进程 | ++-------+------------------------------------------------------------------+ +| +Q | Erlang虚拟机允许的最大Port数量,emqttd一个连接消耗1个Port | ++-------+------------------------------------------------------------------+ + ++P 参数值 > 最大允许连接数 * 2 + ++Q 参数值 > 最大允许连接数 + +.. WARNING:: 实际连接数量超过Erlang虚拟机参数设置,会引起emqttd消息服务器宕机! + +etc/emqttd.config文件listeners段落设置最大允许连接数: + +.. code:: erlang + + {listeners, [ + {mqtt, 1883, [ + %% TCP Acceptor池设置 + {acceptors, 16}, + + %% 最大允许连接数设置 + {max_clients, 8192}, + + ... + + ]}, + +emqttd消息服务器详细设置,请参见文档: :ref:`config` + + ------------------- /etc/init.d/emqttd ------------------- +.. code:: shell + + #!/bin/sh + # + # emqttd Startup script for emqttd. + # + # chkconfig: 2345 90 10 + # description: emqttd is mqtt broker. + + # source function library + . /etc/rc.d/init.d/functions + + # export HOME=/root + + start() { + echo "starting emqttd..." + cd /opt/emqttd && ./bin/emqttd start + } + + stop() { + echo "stopping emqttd..." + cd /opt/emqttd && ./bin/emqttd stop + } + + restart() { + stop + start + } + + case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + *) + echo $"Usage: $0 {start|stop}" + RETVAL=2 + esac + + +chkconfig:: + + chmod +x /etc/init.d/emqttd + chkconfig --add emqttd + chkconfig --list + +boot test:: + + service emqttd start + +.. NOTE:: + + ## erlexec: HOME must be set + uncomment '# export HOME=/root' if "HOME must be set" error. + diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index f5bba4374..33d99f635 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -1,9 +1,9 @@ .. _plugins: -================= +======= Plugins -================= +======= ------------------------------------ emqttd_plugin_template From 5558df8ba97a59af6a3909e701a16e883a045237 Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 25 Feb 2016 19:05:37 +0800 Subject: [PATCH 13/69] commands --- docs/source/commands.rst | 547 +++++++++++++++++++++------------------ 1 file changed, 299 insertions(+), 248 deletions(-) diff --git a/docs/source/commands.rst b/docs/source/commands.rst index a02c231a7..6f01c348b 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -5,40 +5,40 @@ Command Line ============ -emqttd消息服务器提供了'./bin/emqttd_ctl'的管理命令行。 +The './bin/emqttd_ctl' command line could be used to query and administrate emqttd broker. -.. WARNING:: 限制: Windows平台无法使用。 +.. WARNING:: Cannot work on Windows ----------- +------ status ----------- +------ + +Show running status of the broker:: -查询emqttd消息服务器运行状态:: - $ ./bin/emqttd_ctl status Node 'emqttd@127.0.0.1' is started emqttd 0.16.0 is running - ----------- +------ broker ----------- +------ -broker命令查询服务器基本信息,启动时间,统计数据与性能数据。 +Query basic information, statistics and metrics of the broker. -+----------------+-----------------------------------------------+ -| broker | 查询emqttd消息服务器描述、版本、启动时间 | -+----------------+-----------------------------------------------+ -| broker pubsub | 查询核心的Erlang PubSub进程状态(调试) | -+----------------+-----------------------------------------------+ -| broker stats | 查询连接(Client)、会话(Session)、主题(Topic)、| -| | 订阅(Subscription)、路由(Route)统计信息 | -+----------------+-----------------------------------------------+ -| broker metrics | 查询MQTT报文(Packet)、消息(Message)收发统计 | -+----------------+-----------------------------------------------+ ++----------------+-------------------------------------------------+ +| broker | Show version, description, uptime of the broker | ++----------------+-------------------------------------------------+ +| broker pubsub | Show status of the core pubsub process | ++----------------+-------------------------------------------------+ +| broker stats | Show statistics of client, session, topic, | +| | subscription and route of the broker | ++----------------+-------------------------------------------------+ +| broker metrics | Show metrics of MQTT bytes, packets, messages | +| | sent/received. | ++----------------+-------------------------------------------------+ -查询emqttd消息服务器基本信息包括版本、启动时间等:: +Query version, description and uptime of the broker:: $ ./bin/emqttd_ctl broker @@ -47,7 +47,10 @@ broker命令查询服务器基本信息,启动时间,统计数据与性能 uptime : 1 hours, 25 minutes, 24 seconds datetime : 2016-01-16 13:17:32 -查询服务器客户端连接(Client)、会话(Session)、主题(Topic)、订阅(Subscription)、路由(Route)统计:: +broker stats +------------ + +Query statistics of MQTT Client, Session, Topic, Subscription and Route:: $ ./bin/emqttd_ctl broker stats @@ -66,7 +69,10 @@ broker命令查询服务器基本信息,启动时间,统计数据与性能 topics/count : 54 topics/max : 54 -查询服务器流量(Bytes)、MQTT报文(Packets)、消息(Messages)收发统计:: +broker metrics +-------------- + +Query metrics of Bytes, MQTT Packets and Messages(sent/received):: $ ./bin/emqttd_ctl broker metrics @@ -109,62 +115,62 @@ broker命令查询服务器基本信息,启动时间,统计数据与性能 cluster ------- -cluster命令集群多个emqttd消息服务器节点(进程): +Cluster two or more emqttd brokers. -+-----------------------+---------------------+ -| cluster join | 加入集群 | -+-----------------------+---------------------+ -| cluster leave | 离开集群 | -+-----------------------+---------------------+ -| cluster remove | 从集群删除节点 | -+-----------------------+---------------------+ -| cluster status | 查询集群状态 | -+-----------------------+---------------------+ ++-----------------------+--------------------------------+ +| cluster join | Join the cluster | ++-----------------------+--------------------------------+ +| cluster leave | Leave the cluster | ++-----------------------+--------------------------------+ +| cluster remove | Remove a node from the cluster | ++-----------------------+--------------------------------+ +| cluster status | Query cluster status and nodes | ++-----------------------+--------------------------------+ -cluster命令集群本机两个emqttd节点示例: +Suppose we create two emqttd nodes and cluster them on localhost: +-----------+---------------------+-------------+ -| 目录 | 节点名 | MQTT端口 | +| Folder | Node | MQTT Port | +-----------+---------------------+-------------+ | emqttd1 | emqttd1@127.0.0.1 | 1883 | +-----------+---------------------+-------------+ | emqttd2 | emqttd2@127.0.0.1 | 2883 | +-----------+---------------------+-------------+ -启动emqttd1:: +Start emqttd1 node:: cd emqttd1 && ./bin/emqttd start -启动emqttd2:: +Start emqttd2 node:: cd emqttd2 && ./bin/emqttd start -emqttd2节点与emqttd1集群,emqttd2目录下:: +Under emqttd2 folder:: $ ./bin/emqttd_ctl cluster join emqttd1@127.0.0.1 Join the cluster successfully. Cluster status: [{running_nodes,['emqttd1@127.0.0.1','emqttd2@127.0.0.1']}] -任意节点目录下查询集群状态:: +Query cluster status:: $ ./bin/emqttd_ctl cluster status Cluster status: [{running_nodes,['emqttd2@127.0.0.1','emqttd1@127.0.0.1']}] -集群消息路由测试:: +Message Route between nodes:: - # emqttd1节点上订阅x + # Subscribe topic 'x' on emqttd1 node mosquitto_sub -t x -q 1 -p 1883 - # emqttd2节点上向x发布消息 + # Publish to topic 'x' on emqttd2 node mosquitto_pub -t x -q 1 -p 2883 -m hello -emqttd2节点离开集群:: +emqttd2 leaves the cluster:: cd emqttd2 && ./bin/emqttd_ctl cluster leave -emqttd1节点下删除emqttd2:: +Or remove emqttd2 from the cluster on emqttd1 node:: cd emqttd1 && ./bin/emqttd_ctl cluster remove emqttd2@127.0.0.1 @@ -172,17 +178,20 @@ emqttd1节点下删除emqttd2:: clients ------- -clients命令查询连接的MQTT客户端。 +Query MQTT clients connected to the broker: -+-------------------------+-----------------------------+ -| clients list | 查询全部客户端连接 | -+-------------------------+-----------------------------+ -| clients show | 根据ClientId查询客户端 | -+-------------------------+-----------------------------+ -| clients kick | 根据ClientId踢出客户端 | -+-------------------------+-----------------------------+ ++-------------------------+----------------------------------+ +| clients list | Show all MQTT clients | ++-------------------------+----------------------------------+ +| clients show | Show a MQTT Client | ++-------------------------+----------------------------------+ +| clients kick | Kick out a MQTT client | ++-------------------------+----------------------------------+ -查询全部客户端连接:: +clients lists +------------- + +Query All MQTT clients connected to the broker:: $ ./bin/emqttd_ctl clients list @@ -190,169 +199,208 @@ clients命令查询连接的MQTT客户端。 Client(mosqsub/44011-airlee.lo, clean_sess=true, username=test, peername=127.0.0.1:64961, connected_at=1452929275) ... -根据ClientId查询客户端:: +Properties of the Client: + ++--------------+---------------------------------------------------+ +| clean_sess | Clean Session Flag | ++--------------+---------------------------------------------------+ +| username | Username of the client | ++--------------+---------------------------------------------------+ +| peername | Peername of the TCP connection | ++--------------+---------------------------------------------------+ +| connected_at | The timestamp when client connected to the broker | ++--------------+---------------------------------------------------+ + +clients show +----------------------- + +Show a specific MQTT Client:: ./bin/emqttd_ctl clients show "mosqsub/43832-airlee.lo" Client(mosqsub/43832-airlee.lo, clean_sess=true, username=test, peername=127.0.0.1:64896, connected_at=1452929113) + +clients kick +----------------------- -根据ClientId踢出客户端:: +Kick out a MQTT Client:: ./bin/emqttd_ctl clients kick "clientid" -返回Client对象的属性: - -+--------------+-----------------------------+ -| clean_sess | 清除会话标记 | -+--------------+-----------------------------+ -| username | 用户名 | -+--------------+-----------------------------+ -| peername | 对端TCP地址 | -+--------------+-----------------------------+ -| connected_at | 客户端连接时间 | -+--------------+-----------------------------+ -------- sessions -------- -sessions命令查询MQTT连接会话。emqttd消息服务器会为每个连接创建会话,clean_session标记true,创建临时(transient)会话;clean_session标记为false,创建持久会话(persistent)。 +Query all MQTT sessions. The broker will create a session for each MQTT client. Persistent Session if clean_session flag is true, transient session otherwise. -+--------------------------+-----------------------------+ -| sessions list | 查询全部会话 | -+--------------------------+-----------------------------+ -| sessions list persistent | 查询全部持久会话 | -+--------------------------+-----------------------------+ -| sessions list transient | 查询全部临时会话 | -+--------------------------+-----------------------------+ -| sessions show | 根据ClientID查询会话 | -+--------------------------+-----------------------------+ ++--------------------------+-------------------------------+ +| sessions list | Query all Sessions | ++--------------------------+-------------------------------+ +| sessions list persistent | Query all persistent Sessions | ++--------------------------+-------------------------------+ +| sessions list transient | Query all transient Sessions | ++--------------------------+-------------------------------+ +| sessions show | Show a session | ++--------------------------+-------------------------------+ -查询全部会话:: +sessions list +------------- + +Query all sessions:: $ ./bin/emqttd_ctl sessions list Session(clientid, clean_sess=false, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935508) Session(mosqsub/44101-airlee.lo, clean_sess=true, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935401) -查询全部持久会话:: +Properties of Session: + +TODO:?? + ++-------------------+----------------------------------------------------------------+ +| clean_sess | clean sess flag. false: persistent, true: transient | ++-------------------+----------------------------------------------------------------+ +| max_inflight | Inflight window (Max number of messages delivering) | ++-------------------+----------------------------------------------------------------+ +| inflight_queue | Inflight Queue Size | ++-------------------+----------------------------------------------------------------+ +| message_queue | Message Queue Size | ++-------------------+----------------------------------------------------------------+ +| message_dropped | Number of Messages Dropped for queue is full | ++-------------------+----------------------------------------------------------------+ +| awaiting_rel | The number of QoS2 messages received and waiting for PUBREL | ++-------------------+----------------------------------------------------------------+ +| awaiting_ack | The number of QoS1/2 messages delivered and waiting for PUBACK | ++-------------------+----------------------------------------------------------------+ +| awaiting_comp | The number of QoS2 messages delivered and waiting for PUBCOMP | ++-------------------+----------------------------------------------------------------+ +| created_at | Timestamp when the session is created | ++-------------------+----------------------------------------------------------------+ + +sessions list persistent +------------------------ + +Query all persistent sessions:: $ ./bin/emqttd_ctl sessions list persistent Session(clientid, clean_sess=false, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935508) -查询全部临时会话:: +sessions list transient +----------------------- + +Query all transient sessions:: $ ./bin/emqttd_ctl sessions list transient Session(mosqsub/44101-airlee.lo, clean_sess=true, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935401) -根据ClientId查询会话:: +sessions show +------------------------ + +Show a session:: $ ./bin/emqttd_ctl sessions show clientid Session(clientid, clean_sess=false, max_inflight=100, inflight_queue=0, message_queue=0, message_dropped=0, awaiting_rel=0, awaiting_ack=0, awaiting_comp=0, created_at=1452935508) -返回Session对象属性: - -+-------------------+------------------------------------+ -| clean_sess | false: 持久会话,true: 临时会话 | -+-------------------+------------------------------------+ -| max_inflight | 飞行窗口(最大允许同时下发消息数) | -+-------------------+------------------------------------+ -| inflight_queue | 当前正在下发的消息数 | -+-------------------+------------------------------------+ -| message_queue | 当前缓存消息数 | -+-------------------+------------------------------------+ -| message_dropped | 会话丢掉的消息数 | -+-------------------+------------------------------------+ -| awaiting_rel | 等待客户端发送PUBREL的QoS2消息数 | -+-------------------+------------------------------------+ -| awaiting_ack | 等待客户端响应PUBACK的QoS1/2消息数 | -+-------------------+------------------------------------+ -| awaiting_comp | 等待客户端响应PUBCOMP的QoS2消息数 | -+-------------------+------------------------------------+ -| created_at | 会话创建时间戳 | -+-------------------+------------------------------------+ ------ topics ------ -topics命令查询emqttd消息服务器当前的主题(Topic)表。 +Query topic table of the broker. -'topics list'查询全部主题(Topic):: +topics list +----------- + +Query all the topics:: $ ./bin/emqttd_ctl topics list - y: ['emqttd2@127.0.0.1'] - x: ['emqttd1@127.0.0.1','emqttd2@127.0.0.1'] + topic1: ['emqttd2@127.0.0.1'] + topic2: ['emqttd1@127.0.0.1','emqttd2@127.0.0.1'] -'topics show '查询某个主题(Topic):: +topics show +------------------- - $ ./bin/emqttd_ctl topics show x +Show a topic:: - x: ['emqttd1@127.0.0.1','emqttd2@127.0.0.1'] + $ ./bin/emqttd_ctl topics show topic2 -返回结果显示主题(Topic)所在集群节点列表。 + topic2: ['emqttd1@127.0.0.1','emqttd2@127.0.0.1'] + +The result will show which nodes the topic is on. ------------- subscriptions ------------- -subscriptions命令查询消息服务器的订阅(Subscription)表。 +Query the subscription table of the broker: -+--------------------------------------------+-------------------------+ -| subscriptions list | 查询全部订阅 | -+--------------------------------------------+-------------------------+ -| subscriptions show | 查询某个ClientId的订阅 | -+--------------------------------------------+-------------------------+ -| subscriptions add | 手工添加一条订阅 | -+--------------------------------------------+-------------------------+ -| subscriptions del | 手工删除一条订阅 | -+--------------------------------------------+-------------------------+ ++--------------------------------------------+--------------------------------------+ +| subscriptions list | Query all subscriptions | ++--------------------------------------------+--------------------------------------+ +| subscriptions show | Show a subscription | ++--------------------------------------------+--------------------------------------+ +| subscriptions add | Add a static subscription manually | ++--------------------------------------------+--------------------------------------+ +| subscriptions del | Remove a static subscription manually| ++--------------------------------------------+--------------------------------------+ -查询全部订阅:: +subscriptions list +------------------ + +Query all subscriptions:: $ ./bin/emqttd_ctl subscriptions list mosqsub/45744-airlee.lo: [{<<"y">>,0},{<<"x">>,0}] -.. todo:: 打印结果格式需修改。 +subscriptions show +----------------------------- -查询某个ClientId的订阅:: +Show the subscriptions of a MQTT client:: $ ./bin/emqttd_ctl subscriptions show clientid clientid: [{<<"x">>,1},{<<"topic2">>,1},{<<"topic3">>,1}] -手工添加一条订阅:: +subscriptions add +------------------------------------------ + +Add a static subscription manually:: $ ./bin/emqttd_ctl subscriptions add clientid new_topic 1 ok -手工删除一条订阅:: +subscriptions del +------------------------------------ + +Remove a static subscription manually:: $ ./bin/emqttd_ctl subscriptions del clientid new_topic ok - ------- plugins ------- - -plugins命令用于加载、卸载、查询插件应用。emqttd消息服务器通过插件扩展认证、定制功能,插件置于plugins/目录下。 +List, load or unload plugins of emqttd broker. +---------------------------+-------------------------+ -| plugins list | 列出全部插件(Plugin) | +| plugins list | List all plugins | +---------------------------+-------------------------+ -| plugins load | 加载插件(Plugin) | +| plugins load | Load Plugin | +---------------------------+-------------------------+ -| plugins unload | 卸载插件(Plugin) | +| plugins unload | Unload (Plugin) | +---------------------------+-------------------------+ -列出插件:: +plugins list +------------ + +List all plugins:: $ ./bin/emqttd_ctl plugins list @@ -364,99 +412,70 @@ plugins命令用于加载、卸载、查询插件应用。emqttd消息服务器 Plugin(emqttd_recon, version=0.16.0, description=emqttd recon plugin, active=false) Plugin(emqttd_stomp, version=0.16.0, description=Stomp Protocol Plugin for emqttd broker, active=false) -插件属性: +Properties of a plugin: -+-------------+-----------------+ -| version | 插件版本 | -+-------------+-----------------+ -| description | 插件描述 | -+-------------+-----------------+ -| active | 是否已加载 | -+-------------+-----------------+ ++-------------+--------------------------+ +| version | Plugin Version | ++-------------+--------------------------+ +| description | Plugin Description | ++-------------+--------------------------+ +| active | If the plugin is Loaded | ++-------------+--------------------------+ -加载插件:: +load +------------- + +Load a Plugin:: $ ./bin/emqttd_ctl plugins load emqttd_recon Start apps: [recon,emqttd_recon] Plugin emqttd_recon loaded successfully. -卸载插件:: +unload +--------------- + +Unload a Plugin:: $ ./bin/emqttd_ctl plugins unload emqttd_recon Plugin emqttd_recon unloaded successfully. - ------- bridges ------- -plugins命令用于加载、卸载、查询插件应用。emqttd消息服务器通过插件扩展认证、定制功能,插件置于plugins/目录下。 +Bridge two or more emqttd brokers:: -+---------------------------+-------------------------+ -| plugins list | 列出全部插件(Plugin) | -+---------------------------+-------------------------+ -| plugins load | 加载插件(Plugin) | -+---------------------------+-------------------------+ -| plugins unload | 卸载插件(Plugin) | -+---------------------------+-------------------------+ + --------- --------- + Publisher --> | node1 | --Bridge Forward--> | node2 | --> Subscriber + --------- --------- -列出插件:: +commands for bridge: - $ ./bin/emqttd_ctl plugins list ++----------------------------------------+------------------------------+ +| bridges list | List all bridges | ++----------------------------------------+------------------------------+ +| bridges options | Show bridge options | ++----------------------------------------+------------------------------+ +| bridges start | Create a bridge | ++----------------------------------------+------------------------------+ +| bridges start | Create a bridge with options | ++----------------------------------------+------------------------------+ +| bridges stop | Delete a bridge | ++----------------------------------------+------------------------------+ - Plugin(emqttd_dashboard, version=0.16.0, description=emqttd web dashboard, active=true) - Plugin(emqttd_plugin_mysql, version=0.16.0, description=emqttd Authentication/ACL with MySQL, active=false) - Plugin(emqttd_plugin_pgsql, version=0.16.0, description=emqttd PostgreSQL Plugin, active=false) - Plugin(emqttd_plugin_redis, version=0.16.0, description=emqttd Redis Plugin, active=false) - Plugin(emqttd_plugin_template, version=0.16.0, description=emqttd plugin template, active=false) - Plugin(emqttd_recon, version=0.16.0, description=emqttd recon plugin, active=false) - Plugin(emqttd_stomp, version=0.16.0, description=Stomp Protocol Plugin for emqttd broker, active=false) +Suppose we create a bridge between emqttd1 and emqttd2 on localhost: -插件属性: ++---------+---------------------+-----------+ +| Name | Node | MQTT Port | ++---------+---------------------+-----------+ +| emqttd1 | emqttd1@127.0.0.1 | 1883 | ++---------+---------------------+-----------+ +| emqttd2 | emqttd2@127.0.0.1 | 2883 | ++---------+---------------------+-----------+ -+-------------+-----------------+ -| version | 插件版本 | -+-------------+-----------------+ -| description | 插件描述 | -+-------------+-----------------+ -| active | 是否已加载 | -+-------------+-----------------+ - -加载插件:: - - $ ./bin/emqttd_ctl plugins load emqttd_recon - - Start apps: [recon,emqttd_recon] - Plugin emqttd_recon loaded successfully. - -卸载插件:: - - $ ./bin/emqttd_ctl plugins unload emqttd_recon - - Plugin emqttd_recon unloaded successfully. - - -------- -bridges -------- - -bridges命令用于在多台emqttd服务器节点间创建桥接。 - -+----------------------------------------+---------------------------+ -| bridges list | 查询全部桥接 | -+----------------------------------------+---------------------------+ -| bridges options | 查询创建桥接选项 | -+----------------------------------------+---------------------------+ -| bridges start | 创建桥接 | -+----------------------------------------+---------------------------+ -| bridges start | 创建桥接并带选项设置 | -+----------------------------------------+---------------------------+ -| bridges stop | 删除桥接 | -+----------------------------------------+---------------------------+ - -创建一条emqttd1 -> emqttd2节点的桥接,转发传感器主题(Topic)消息到emqttd2:: +The bridge will forward all the the 'sensor/#' messages from emqttd1 to emqttd2:: $ ./bin/emqttd_ctl bridges start emqttd2@127.0.0.1 sensor/# @@ -466,9 +485,9 @@ bridges命令用于在多台emqttd服务器节点间创建桥接。 bridge: emqttd1@127.0.0.1--sensor/#-->emqttd2@127.0.0.1 -测试emqttd1--sensor/#-->emqttd2的桥接:: +The the 'emqttd1--sensor/#-->emqttd2' bridge:: - #emqttd2节点上 + #emqttd2 node mosquitto_sub -t sensor/# -p 2883 -d @@ -476,7 +495,10 @@ bridges命令用于在多台emqttd服务器节点间创建桥接。 mosquitto_pub -t sensor/1/temperature -m "37.5" -d -查询bridge创建选项设置:: +bridges options +--------------- + +Show bridge options:: $ ./bin/emqttd_ctl bridges options @@ -488,31 +510,37 @@ bridges命令用于在多台emqttd服务器节点间创建桥接。 Example: qos=2,prefix=abc/,suffix=/yxz,queue=1000 -删除emqttd1--sensor/#-->emqttd2的桥接:: +bridges stop +--------------------------- + +Delete the emqttd1--sensor/#-->emqttd2 bridge:: $ ./bin/emqttd_ctl bridges stop emqttd2@127.0.0.1 sensor/# bridge is stopped. - + -- vm -- -vm命令用于查询Erlang虚拟机负载、内存、进程、IO信息。 +Query the load, cpu, memory, processes and IO information of the Erlang VM. -+-------------+------------------------+ -| vm all | 查询VM全部信息 | -+-------------+------------------------+ -| vm load | 查询VM负载 | -+-------------+------------------------+ -| vm memory | 查询VM内存 | -+-------------+------------------------+ -| vm process | 查询VM Erlang进程数量 | -+-------------+------------------------+ -| vm io | 查询VM io最大文件句柄 | -+-------------+------------------------+ ++-------------+-----------------------------------+ +| vm all | Query all | ++-------------+-----------------------------------+ +| vm load | Query VM Load | ++-------------+-----------------------------------+ +| vm memory | Query Memory Usage | ++-------------+-----------------------------------+ +| vm process | Query Number of Erlang Processes | ++-------------+-----------------------------------+ +| vm io | Query Max Fds of VM | ++-------------+-----------------------------------+ -查询VM负载:: +vm load +------- + +Query load:: $ ./bin/emqttd_ctl vm load @@ -520,7 +548,10 @@ vm命令用于查询Erlang虚拟机负载、内存、进程、IO信息。 cpu/load5 : 2.60 cpu/load15 : 2.36 -查询VM内存:: +vm memory +--------- + +Query memory:: $ ./bin/emqttd_ctl vm memory @@ -534,14 +565,20 @@ vm命令用于查询Erlang虚拟机负载、内存、进程、IO信息。 memory/code : 13401565 memory/ets : 1082848 -查询Erlang进程数量:: +vm process +---------- + +Query number of erlang processes:: $ ./bin/emqttd_ctl vm process process/limit : 8192 process/count : 221 -查询IO最大句柄数:: +vm io +----- + +Query max, active file descriptors of IO:: $ ./bin/emqttd_ctl vm io @@ -552,57 +589,71 @@ vm命令用于查询Erlang虚拟机负载、内存、进程、IO信息。 trace ----- -trace命令用于追踪某个客户端或Topic,打印日志信息到文件。 +Trace MQTT packets, messages(sent/received) by ClientId or Topic. +-----------------------------------+-----------------------------------+ -| trace list | 查询全部开启的追踪 | +| trace list | List all the traces | +-----------------------------------+-----------------------------------+ -| trace client | 开启Client追踪,日志到文件 | +| trace client | Trace a client | +-----------------------------------+-----------------------------------+ -| trace client off | 关闭Client追踪 | +| trace client off | Stop to trace the client | +-----------------------------------+-----------------------------------+ -| trace topic | 开启Topic追踪,日志到文件 | +| trace topic | Trace a topic | +-----------------------------------+-----------------------------------+ -| trace topic off | 关闭Topic追踪 | +| trace topic off | Stop to trace the topic | +-----------------------------------+-----------------------------------+ -开启Client追踪:: +trace client +--------------------------------- + +Start to trace a client:: $ ./bin/emqttd_ctl trace client clientid log/clientid_trace.log trace client clientid successfully. -关闭Client追踪:: +trace client off +--------------------------- + +Stop to trace the client:: $ ./bin/emqttd_ctl trace client clientid off stop to trace client clientid successfully. -开启Topic追踪:: +trace topic +----------------------------- + +Start to trace a topic:: $ ./bin/emqttd_ctl trace topic topic log/topic_trace.log trace topic topic successfully. -关闭Topic追踪:: +trace topic off +----------------------- + +Stop to trace the topic:: $ ./bin/emqttd_ctl trace topic topic off stop to trace topic topic successfully. -查询全部开启的追踪:: +trace list +---------- + +List all traces:: $ ./bin/emqttd_ctl trace list trace client clientid -> log/clientid_trace.log trace topic topic -> log/topic_trace.log - --------- listeners --------- -listeners命令用于查询开启的TCP服务监听器:: +Show all the TCP listeners:: $ ./bin/emqttd_ctl listeners @@ -627,21 +678,21 @@ listeners命令用于查询开启的TCP服务监听器:: current_clients : 0 shutdown_count : [] -listener参数说明: +listener parameters: -+-----------------+-----------------------------------+ -| acceptors | TCP Acceptor池 | -+-----------------+-----------------------------------+ -| max_clients | 最大允许连接数 | -+-----------------+-----------------------------------+ -| current_clients | 当前连接数 | -+-----------------+-----------------------------------+ -| shutdown_count | Socket关闭原因统计 | -+-----------------+-----------------------------------+ ++-----------------+--------------------------------------+ +| acceptors | TCP Acceptor Pool | ++-----------------+--------------------------------------+ +| max_clients | Max number of clients | ++-----------------+--------------------------------------+ +| current_clients | Count of current clients | ++-----------------+--------------------------------------+ +| shutdown_count | Statistics of client shutdown reason | ++-----------------+---------------------------------------+ ----------- +------ mnesia ----------- +------ -查询mnesia数据库当前状态,用于调试。 +Query system_info of mnesia database. From b94776a42de76e6af0467000a5955d0bb291fdc6 Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 25 Feb 2016 20:49:25 +0800 Subject: [PATCH 14/69] plugins --- docs/source/plugins.rst | 487 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 455 insertions(+), 32 deletions(-) diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 33d99f635..d7db15156 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -5,43 +5,466 @@ Plugins ======= ------------------------------------- -emqttd_plugin_template ------------------------------------- +The emqttd broker could be extended by plugins. Users could develop plugins to customize authentication, ACL and functions of the broker, or integrate the broker with other systems. ------------------------------------ +The plugins that emqtt project released: + ++---------------------------+---------------------------+ +| Plugin | Description | ++===========================+===========================+ +| `emqttd_plugin_template`_ | Plugin Template | ++---------------------------+---------------------------+ +| `emqttd_dashboard`_ | Web Dashboard | ++---------------------------+---------------------------+ +| `emqttd_plugin_mysql`_ | MySQL Auth/ACL Plugin | ++---------------------------+---------------------------+ +| `emqttd_plugin_pgsql`_ | PostgreSQL Auth/ACL Plugin| ++---------------------------+---------------------------+ +| `emqttd_plugin_redis`_ | Redis Auth/ACL Plugin | ++---------------------------+---------------------------+ +| `emqttd_stomp`_ | Stomp Protocol Plugin | ++---------------------------+---------------------------+ +| `emqttd_sockjs`_ | Stomp over SockJS Plugin | ++---------------------------+---------------------------+ +| `emqttd_recon`_ | Recon Plugin | ++---------------------------+---------------------------+ + +---------------------------------------- +emqttd_plugin_template - Plugin Template +---------------------------------------- + +A plugin is just a normal Erlang application under the 'emqttd/plugins' folder. Each plugin has e configuration file: 'etc/plugin.config'. + +plugins/emqttd_plugin_template is a demo plugin: + ++------------------------+---------------------------+ +| File | Description | ++========================+===========================+ +| etc/plugin.config | Plugin config file | ++------------------------+---------------------------+ +| ebin/ | Erlang program files | ++------------------------+---------------------------+ + +Load, unload Plugin +------------------- + +Use 'bin/emqttd_ctl plugins' CLI to load, unload a plugin:: + + ./bin/emqttd_ctl plugins load + + ./bin/emqttd_ctl plugins unload + + ./bin/emqttd_ctl plugins list + +---------------------------------- emqttd_dashboard: Dashboard Plugin ------------------------------------ +---------------------------------- + +The Web Dashboard of emqttd broker. Address: http://localhost:18083, Default User: admin, Password: public + +.. image:: _static/images/dashboard.png Configure Dashboard +------------------- + +plugins/emqttd_dashboard/etc/plugin.config:: + + [ + {emqttd_dashboard, [ + {default_admin, [ + {login, "admin"}, + {password, "public"} + ]}, + {listener, + {emqttd_dashboard, 18083, [ + {acceptors, 4}, + {max_clients, 512}]} + } + ]} + ]. + +------------------------------------------- +emqttd_plugin_mysql - MySQL Auth/ACL Plugin +------------------------------------------- + +MQTT Authentication, ACL with MySQL database. + +MQTT User Table +--------------- + +.. code:: sql + + CREATE TABLE `mqtt_user` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(100) DEFAULT NULL, + `password` varchar(100) DEFAULT NULL, + `salt` varchar(20) DEFAULT NULL, + `created` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `mqtt_username` (`username`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +MQTT ACL Table +-------------- + +.. code:: sql + + CREATE TABLE `mqtt_acl` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `allow` int(1) DEFAULT NULL COMMENT '0: deny, 1: allow', + `ipaddr` varchar(60) DEFAULT NULL COMMENT 'IpAddress', + `username` varchar(100) DEFAULT NULL COMMENT 'Username', + `clientid` varchar(100) DEFAULT NULL COMMENT 'ClientId', + `access` int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub', + `topic` varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +emqttd_plugin_mysql/etc/plugin.config +------------------------------------- + +Configure MySQL host, username, password and database:: + + [ + + {emqttd_plugin_mysql, [ + + {mysql_pool, [ + %% ecpool options + {pool_size, 4}, + {auto_reconnect, 3}, + + %% mysql options + {host, "localhost"}, + {port, 3306}, + {user, ""}, + {password, ""}, + {database, "mqtt"}, + {encoding, utf8} + ]}, + + %% select password only + {authquery, "select password from mqtt_user where username = '%u' limit 1"}, + + %% hash algorithm: md5, sha, sha256, pbkdf2? + {password_hash, sha256}, + + %% select password with salt + %% {authquery, "select password, salt from mqtt_user where username = '%u'"}, + + %% sha256 with salt prefix + %% {password_hash, {salt, sha256}}, + + %% sha256 with salt suffix + %% {password_hash, {sha256, salt}}, + + %% comment this query, the acl will be disabled + {aclquery, "select * from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"}, + + %% If no rules matched, return... + {acl_nomatch, allow} + ]} + ]. + +Load emqttd_plugin_mysql plugin +------------------------------- + +.. code:: + + ./bin/emqttd_ctl plugins load emqttd_plugin_mysql + +------------------------------------------------ +emqttd_plugin_pgsql - PostgreSQL Auth/ACL Plugin +------------------------------------------------ + +MQTT Authentication, ACL with PostgreSQL Database. + +MQTT User Table +--------------- + +.. code:: sql + + CREATE TABLE mqtt_user ( + id SERIAL primary key, + username character varying(100), + password character varying(100), + salt character varying(40) + ); + +MQTT ACL Table +-------------- + +.. code:: sql + + CREATE TABLE mqtt_acl ( + id SERIAL primary key, + allow integer, + ipaddr character varying(60), + username character varying(100), + clientid character varying(100), + access integer, + topic character varying(100) + ); + + INSERT INTO mqtt_acl (id, allow, ipaddr, username, clientid, access, topic) + VALUES + (1,1,NULL,'$all',NULL,2,'#'), + (2,0,NULL,'$all',NULL,1,'$SYS/#'), + (3,0,NULL,'$all',NULL,1,'eq #'), + (5,1,'127.0.0.1',NULL,NULL,2,'$SYS/#'), + (6,1,'127.0.0.1',NULL,NULL,2,'#'), + (7,1,NULL,'dashboard',NULL,1,'$SYS/#'); + +Configure emqttd_plugin_pgsql/etc/plugin.config +----------------------------------------------- + +Configure host, username, password and database of PostgreSQL:: + + [ + + {emqttd_plugin_pgsql, [ + + {pgsql_pool, [ + %% ecpool options + {pool_size, 4}, + {auto_reconnect, 3}, + + %% pgsql options + {host, "localhost"}, + {port, 5432}, + {username, "feng"}, + {password, ""}, + {database, "mqtt"}, + {encoding, utf8} + ]}, + + %% select password only + {authquery, "select password from mqtt_user where username = '%u' limit 1"}, + + %% hash algorithm: md5, sha, sha256, pbkdf2? + {password_hash, sha256}, + + %% select password with salt + %% {authquery, "select password, salt from mqtt_user where username = '%u'"}, + + %% sha256 with salt prefix + %% {password_hash, {salt, sha256}}, + + %% sha256 with salt suffix + %% {password_hash, {sha256, salt}}, + + %% Comment this query, the acl will be disabled. Notice: don't edit this query! + {aclquery, "select allow, ipaddr, username, clientid, access, topic from mqtt_acl + where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"}, + + %% If no rules matched, return... + {acl_nomatch, allow} + ]} + ]. + +Load emqttd_plugin_pgsql Plugin +------------------------------- + +.. code:: shell + + ./bin/emqttd_ctl plugins load emqttd_plugin_pgsql + +------------------------------------------- +emqttd_plugin_redis - Redis Auth/ACL Plugin +------------------------------------------- + +MQTT Authentication, ACL with Redis. + +Configure emqttd_plugin_redis/etc/plugin.config +----------------------------------------------- + +.. code:: erlang + + [ + {emqttd_plugin_redis, [ + + {eredis_pool, [ + %% ecpool options + {pool_size, 8}, + {auto_reconnect, 2}, + + %% eredis options + {host, "127.0.0.1"}, + {port, 6379}, + {database, 0}, + {password, ""} + ]}, + + %% HMGET mqtt_user:%u password + {authcmd, ["HGET", "mqtt_user:%u", "password"]}, + + %% Password hash algorithm: plain, md5, sha, sha256, pbkdf2? + {password_hash, sha256}, + + %% SMEMBERS mqtt_acl:%u + {aclcmd, ["SMEMBERS", "mqtt_acl:%u"]}, + + %% If no rules matched, return... + {acl_nomatch, deny}, + + %% Store subscriptions to redis when SUBSCRIBE packets received. + {subcmd, ["HMSET", "mqtt_subs:%u"]}, + + %% Load Subscriptions form Redis when client connected. + {loadsub, ["HGETALL", "mqtt_subs:%u"]}, + + %% Remove subscriptions from redis when UNSUBSCRIBE packets received. + {unsubcmd, ["HDEL", "mqtt_subs:%u"]} + + ]} + ]. + +Load emqttd_plugin_redis Plugin +------------------------------- + +.. code:: console + + ./bin/emqttd_ctl plugins load emqttd_plugin_redis + +----------------------------- +emqttd_stomp - STOMP Protocol +----------------------------- + +Support STOMP 1.0/1.1/1.2 clients to connect to emqttd broker and communicate with MQTT Clients. + +Configure emqttd_stomp/etc/plugin.config +---------------------------------------- + +.. NOTE:: TCP Port for STOMP Protocol: 61613 + +.. code:: erlang + + [ + {emqttd_stomp, [ + + {default_user, [ + {login, "guest"}, + {passcode, "guest"} + ]}, + + {allow_anonymous, true}, + + %%TODO: unused... + {frame, [ + {max_headers, 10}, + {max_header_length, 1024}, + {max_body_length, 8192} + ]}, + + {listeners, [ + {emqttd_stomp, 61613, [ + {acceptors, 4}, + {max_clients, 512} + ]} + ]} + + ]} + ]. + +Load emqttd_stomp Plugin +------------------------ + +.. code:: + + ./bin/emqttd_ctl plugins load emqttd_stomp + + +----------------------------------- +emqttd_sockjs - Stomp/SockJS Plugin +----------------------------------- + +emqttd_sockjs plugin enables web browser to connect to emqttd broker and communicate with MQTT clients. + +.. NOTE:: Default TCP Port: 61616 + +Configure emqttd_sockjs +----------------------- + +.. code:: erlang + + [ + {emqttd_sockjs, [ + + {sockjs, []}, + + {cowboy_listener, {stomp_sockjs, 61616, 4}}, + + ]} + ]. + +Load emqttd_sockjs Plugin +------------------------- + +.. NOTE:: emqttd_stomp Plugin required. + +.. code:: console + + ./bin/emqttd_ctl plugins load emqttd_stomp + + ./bin/emqttd_ctl plugins load emqttd_sockjs + +SockJS Demo Page +---------------- + +http://localhost:61616/index.html + +--------------------------- +emqttd_recon - Recon Plugin +--------------------------- + +The plugin will load `recon`_ library on a running emqttd broker. Recon libray helps to debug and optimize an Erlang application. + +Load emqttd_recon Plugin +------------------------ + +.. code:: console + + ./bin/emqttd_ctl plugins load emqttd_recon + +Recon CLI +--------- + +.. code:: console + + ./bin/emqttd_ctl recon + + recon memory #recon_alloc:memory/2 + recon allocated #recon_alloc:memory(allocated_types, current|max) + recon bin_leak #recon:bin_leak(100) + recon node_stats #recon:node_stats(10, 1000) + recon remote_load Mod #recon:remote_load(Mod) + + +------------------------ +Plugin Development Guide +------------------------ + +Create a Plugin Project +----------------------- + +Register Auth/ACL Modules +------------------------- + +Register Handlers for Hooks +--------------------------- + +Register CLI Modules -------------------- -------------------------------------------- -emqttd_plugin_mysql: -------------------------------------------- - ------------------------------------------------- -emqttd_plugin_pgsql: ------------------------------------------------- - --------------------------------------------- -emqttd_plugin_redis: --------------------------------------------- - ------------------------------ -emqttd_stomp: ------------------------------ - --------------------------------- -emqttd_sockjs: --------------------------------- - --------------------------------- -emqttd_recon: Recon Plugin --------------------------------- - -------------------------- -Plugin Development Guide -------------------------- +.. _emqttd_dashboard: https://github.com/emqtt/emqttd_dashboard +.. _emqttd_plugin_mysql: https://github.com/emqtt/emqttd_plugin_mysql +.. _emqttd_plugin_pgsql: https://github.com/emqtt/emqttd_plugin_pgsql +.. _emqttd_plugin_redis: https://github.com/emqtt/emqttd_plugin_redis +.. _emqttd_stomp: https://github.com/emqtt/emqttd_stomp +.. _emqttd_sockjs: https://github.com/emqtt/emqttd_sockjs +.. _emqttd_recon: https://github.com/emqtt/emqttd_recon +.. _emqttd_plugin_template: https://github.com/emqtt/emqttd_plugin_template +.. _recon: http://ferd.github.io/recon/ From bfe34e28f086a61704d3109b9e2b1342548eefd7 Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 25 Feb 2016 21:46:59 +0800 Subject: [PATCH 15/69] upgrade --- docs/source/commands.rst | 8 +- docs/source/plugins.rst | 183 +++++++++++++++++++++++++++++++++++---- 2 files changed, 169 insertions(+), 22 deletions(-) diff --git a/docs/source/commands.rst b/docs/source/commands.rst index 6f01c348b..91a29be72 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -127,7 +127,7 @@ Cluster two or more emqttd brokers. | cluster status | Query cluster status and nodes | +-----------------------+--------------------------------+ -Suppose we create two emqttd nodes and cluster them on localhost: +Suppose we create two emqttd nodes on localhost and cluster them: +-----------+---------------------+-------------+ | Folder | Node | MQTT Port | @@ -181,7 +181,7 @@ clients Query MQTT clients connected to the broker: +-------------------------+----------------------------------+ -| clients list | Show all MQTT clients | +| clients list | List all MQTT clients | +-------------------------+----------------------------------+ | clients show | Show a MQTT Client | +-------------------------+----------------------------------+ @@ -235,7 +235,7 @@ sessions Query all MQTT sessions. The broker will create a session for each MQTT client. Persistent Session if clean_session flag is true, transient session otherwise. +--------------------------+-------------------------------+ -| sessions list | Query all Sessions | +| sessions list | List all Sessions | +--------------------------+-------------------------------+ | sessions list persistent | Query all persistent Sessions | +--------------------------+-------------------------------+ @@ -340,7 +340,7 @@ subscriptions Query the subscription table of the broker: +--------------------------------------------+--------------------------------------+ -| subscriptions list | Query all subscriptions | +| subscriptions list | List all subscriptions | +--------------------------------------------+--------------------------------------+ | subscriptions show | Show a subscription | +--------------------------------------------+--------------------------------------+ diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index d7db15156..071518602 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -1,9 +1,9 @@ .. _plugins: -======= -Plugins -======= +============ +Plugin Guide +============ The emqttd broker could be extended by plugins. Users could develop plugins to customize authentication, ACL and functions of the broker, or integrate the broker with other systems. @@ -12,7 +12,7 @@ The plugins that emqtt project released: +---------------------------+---------------------------+ | Plugin | Description | +===========================+===========================+ -| `emqttd_plugin_template`_ | Plugin Template | +| `emqttd_plugin_template`_ | Template Plugin | +---------------------------+---------------------------+ | `emqttd_dashboard`_ | Web Dashboard | +---------------------------+---------------------------+ @@ -22,20 +22,20 @@ The plugins that emqtt project released: +---------------------------+---------------------------+ | `emqttd_plugin_redis`_ | Redis Auth/ACL Plugin | +---------------------------+---------------------------+ -| `emqttd_stomp`_ | Stomp Protocol Plugin | +| `emqttd_stomp`_ | STOMP Protocol Plugin | +---------------------------+---------------------------+ -| `emqttd_sockjs`_ | Stomp over SockJS Plugin | +| `emqttd_sockjs`_ | STOMP over SockJS Plugin | +---------------------------+---------------------------+ | `emqttd_recon`_ | Recon Plugin | +---------------------------+---------------------------+ ---------------------------------------- -emqttd_plugin_template - Plugin Template +emqttd_plugin_template - Template Plugin ---------------------------------------- A plugin is just a normal Erlang application under the 'emqttd/plugins' folder. Each plugin has e configuration file: 'etc/plugin.config'. -plugins/emqttd_plugin_template is a demo plugin: +plugins/emqttd_plugin_template is a demo plugin. The folder structure: +------------------------+---------------------------+ | File | Description | @@ -56,18 +56,26 @@ Use 'bin/emqttd_ctl plugins' CLI to load, unload a plugin:: ./bin/emqttd_ctl plugins list ----------------------------------- -emqttd_dashboard: Dashboard Plugin ----------------------------------- +----------------------------------- +emqttd_dashboard - Dashboard Plugin +----------------------------------- -The Web Dashboard of emqttd broker. Address: http://localhost:18083, Default User: admin, Password: public +The Web Dashboard for emqttd broker. The plugin will be loaded automatically when the broker started successfully. + ++------------------+---------------------------+ +| Address | http://localhost:18083 | ++------------------+---------------------------+ +| Default User | admin | ++------------------+---------------------------+ +| Default Password | public | ++------------------+---------------------------+ .. image:: _static/images/dashboard.png Configure Dashboard ------------------- -plugins/emqttd_dashboard/etc/plugin.config:: +emqttd_dashboard/etc/plugin.config:: [ {emqttd_dashboard, [ @@ -121,8 +129,8 @@ MQTT ACL Table ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -emqttd_plugin_mysql/etc/plugin.config -------------------------------------- +Configure emqttd_plugin_mysql/etc/plugin.config +----------------------------------------------- Configure MySQL host, username, password and database:: @@ -336,7 +344,7 @@ Support STOMP 1.0/1.1/1.2 clients to connect to emqttd broker and communicate wi Configure emqttd_stomp/etc/plugin.config ---------------------------------------- -.. NOTE:: TCP Port for STOMP Protocol: 61613 +.. NOTE:: Default Port for STOMP Protocol: 61613 .. code:: erlang @@ -376,7 +384,7 @@ Load emqttd_stomp Plugin ----------------------------------- -emqttd_sockjs - Stomp/SockJS Plugin +emqttd_sockjs - STOMP/SockJS Plugin ----------------------------------- emqttd_sockjs plugin enables web browser to connect to emqttd broker and communicate with MQTT clients. @@ -418,7 +426,7 @@ http://localhost:61616/index.html emqttd_recon - Recon Plugin --------------------------- -The plugin will load `recon`_ library on a running emqttd broker. Recon libray helps to debug and optimize an Erlang application. +The plugin loads `recon`_ library on a running emqttd broker. Recon libray helps to debug and optimize an Erlang application. Load emqttd_recon Plugin ------------------------ @@ -448,15 +456,154 @@ Plugin Development Guide Create a Plugin Project ----------------------- +Clone emqttd source from github.com:: + + git clone https://github.com/emqtt/emqttd.git + +Create a plugin project under 'plugins' folder:: + + cd plugins && mkdir emqttd_my_plugin + + cd emqttd_my_plugin && rebar create-app appid=emqttd_my_plugin + +Template Plugin: https://github.com/emqtt/emqttd_plugin_template + Register Auth/ACL Modules ------------------------- +emqttd_auth_demo.erl - demo authentication module: + +.. code:: erlang + + -module(emqttd_auth_demo). + + -behaviour(emqttd_auth_mod). + + -include("../../../include/emqttd.hrl"). + + -export([init/1, check/3, description/0]). + + init(Opts) -> {ok, Opts}. + + check(#mqtt_client{client_id = ClientId, username = Username}, Password, _Opts) -> + io:format("Auth Demo: clientId=~p, username=~p, password=~p~n", + [ClientId, Username, Password]), + ok. + + description() -> "Demo Auth Module". + +emqttd_acl_demo.erl - demo ACL module: + +.. code:: erlang + + -module(emqttd_acl_demo). + + -include("../../../include/emqttd.hrl"). + + %% ACL callbacks + -export([init/1, check_acl/2, reload_acl/1, description/0]). + + init(Opts) -> + {ok, Opts}. + + check_acl({Client, PubSub, Topic}, Opts) -> + io:format("ACL Demo: ~p ~p ~p~n", [Client, PubSub, Topic]), + allow. + + reload_acl(_Opts) -> + ok. + + description() -> "ACL Module Demo". + +emqttd_plugin_template_app.erl - Register the auth/ACL modules: + +.. code:: erlang + + ok = emqttd_access_control:register_mod(auth, emqttd_auth_demo, []), + ok = emqttd_access_control:register_mod(acl, emqttd_acl_demo, []), + + Register Handlers for Hooks --------------------------- +The plugin could register handlers for hooks. The hooks will be called by the broker when a client connected/disconnected, a topic subscribed/unsubscribed or a message published/delivered. + +emqttd_plugin_template.erl for example:: + + %% Called when the plugin application start + load(Env) -> + + emqttd_broker:hook('client.connected', {?MODULE, on_client_connected}, + {?MODULE, on_client_connected, [Env]}), + + emqttd_broker:hook('client.disconnected', {?MODULE, on_client_disconnected}, + {?MODULE, on_client_disconnected, [Env]}), + + emqttd_broker:hook('client.subscribe', {?MODULE, on_client_subscribe}, + {?MODULE, on_client_subscribe, [Env]}), + + emqttd_broker:hook('client.subscribe.after', {?MODULE, on_client_subscribe_after}, + {?MODULE, on_client_subscribe_after, [Env]}), + + emqttd_broker:hook('client.unsubscribe', {?MODULE, on_client_unsubscribe}, + {?MODULE, on_client_unsubscribe, [Env]}), + + emqttd_broker:hook('message.publish', {?MODULE, on_message_publish}, + {?MODULE, on_message_publish, [Env]}), + + emqttd_broker:hook('message.acked', {?MODULE, on_message_acked}, + {?MODULE, on_message_acked, [Env]}). + +Hook List: + ++------------------------+-------------+---------------------------------------+ +| Name | Type | Description | ++------------------------+-------------+---------------------------------------+ +| client.connected | foreach | Run when a client connected to the | +| | | broker successfully | ++------------------------+-------------+---------------------------------------+ +| client.subscribe | foldl | Run before a client subscribes topics | ++------------------------+-------------+---------------------------------------+ +| client.subscribe.after | foreach | Run after a client subscribed topics | ++------------------------+-------------+---------------------------------------+ +| client.unsubscribe | foldl | Run when a client unsubscribes topics | ++------------------------+-------------+---------------------------------------+ +| message.publish | foldl | Run when a message is published | ++------------------------+-------------+---------------------------------------+ +| message.acked | foreach | Run when a message is delivered | ++------------------------+-------------+---------------------------------------+ +| client.disconnected | foreach | Run when a client is disconnnected | ++----------------------- +-------------+---------------------------------------+ + Register CLI Modules -------------------- +emqttd_cli_demo.erl: + +.. code:: erlang + + -module(emqttd_cli_demo). + + -include("../../../include/emqttd_cli.hrl"). + + -export([cmd/1]). + + cmd(["arg1", "arg2"]) -> + ?PRINT_MSG("ok"); + + cmd(_) -> + ?USAGE([{"cmd arg1 arg2", "cmd demo"}]). + +emqttd_plugin_template_app.erl - register the CLI module to emqttd broker: + +.. code:: erlang + + emqttd_ctl:register_cmd(cmd, {emqttd_cli_demo, cmd}, []). + +There will be a new CLI after the plugin loaded:: + + ./bin/emqttd_ctl cmd arg1 arg2 + .. _emqttd_dashboard: https://github.com/emqtt/emqttd_dashboard .. _emqttd_plugin_mysql: https://github.com/emqtt/emqttd_plugin_mysql From 18be4ee3b295eafbaa6d8fb08a83bd845b288558 Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 25 Feb 2016 22:37:36 +0800 Subject: [PATCH 16/69] supports --- docs/source/index.rst | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 742e0b798..b1e9d785a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,17 +20,30 @@ Contents: commands plugins -Support and Contacts: -+------------+--------------------------------+ -| Site: | http://emqtt.io | -+------------+--------------------------------+ -| GitHub: | https://github.com/emqtt | -+------------+--------------------------------+ -| Twitter: | @emqtt | -+------------+--------------------------------+ -| Author: | Feng Lee | -+------------+--------------------------------+ +--------------------- +Supports and Contacts +--------------------- ++---------------+-----------------------------------------+ +| Homepage: | http://emqtt.io | ++---------------+-----------------------------------------+ +| Downloads: | http://emqtt.io/downloads | ++---------------+-----------------------------------------+ +| GitHub: | https://github.com/emqtt | ++---------------+-----------------------------------------+ +| Twitter: | @emqtt | ++---------------+-----------------------------------------+ +| Forum: | https://groups.google.com/d/forum/emqtt | ++---------------+-----------------------------------------+ +| Mailing List: | emqtt@googlegroups.com | ++---------------+-----------------------------------------+ +| Author: | Feng Lee | ++---------------+-----------------------------------------+ +------- +License +------- + +Apache License Version 2.0 From e368afea362a8899514f7913f4dd5d11511d293d Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 25 Feb 2016 22:37:52 +0800 Subject: [PATCH 17/69] supports --- docs/source/getstarted.rst | 39 +++++++++++++++++-------------- docs/source/plugins.rst | 48 ++++++++++++++++++-------------------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index ce70535f3..cf50de096 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -5,21 +5,24 @@ Get Started =========== --------------------- +-------- Overview --------------------- +-------- --------------------- -Goals --------------------- +emqttd is a massively scalable and clusterable MQTT V3.1/V3.1.1 broker written in Erlang/OTP. --------------------- -MQTT Protocol --------------------- +emqttd is aimed to provide a solid, enterprise grade, extensible open-source MQTT broker for IoT, M2M and Mobile applications that need to support ten millions of concurrent MQTT clients. --------------------------- +* Easy to install +* Massively scalable +* Easy to extend +* Solid stable + +----------- Quick Start --------------------------- +----------- + +Download binary packeges for Linux, Mac, FreeBSD and Windows from http://emqtt.io/downloads. .. code:: console @@ -34,9 +37,9 @@ Quick Start # Stop emqttd ./bin/emqttd stop --------------------- -Compile from Source --------------------- +----------------- +Build from Source +----------------- .. code:: console @@ -166,9 +169,9 @@ Test Client sysctl -w net.ipv4.ip_local_port_range="500 65535" echo 1000000 > /proc/sys/fs/nr_open ----------------------------- +---------------------- emqtt Client Libraries ----------------------------- +---------------------- GitHub: https://github.com/emqtt @@ -182,8 +185,8 @@ GitHub: https://github.com/emqtt | `QMQTT`_ | QT MQTT Client | +--------------------+----------------------+ -.. _emqttc: https://github.com/emqtt/emqttc +.. _emqttc: https://github.com/emqtt/emqttc .. _emqtt_benchmark: https://github.com/emqtt/emqtt_benchmark -.. _CocoaMQTT: https://github.com/emqtt/CocoaMQTT -.. _QMQTT: https://github.com/emqtt/qmqtt +.. _CocoaMQTT: https://github.com/emqtt/CocoaMQTT +.. _QMQTT: https://github.com/emqtt/qmqtt diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 071518602..eb6b5ce6b 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -1,9 +1,9 @@ .. _plugins: -============ -Plugin Guide -============ +======= +Plugins +======= The emqttd broker could be extended by plugins. Users could develop plugins to customize authentication, ACL and functions of the broker, or integrate the broker with other systems. @@ -526,7 +526,26 @@ emqttd_plugin_template_app.erl - Register the auth/ACL modules: Register Handlers for Hooks --------------------------- -The plugin could register handlers for hooks. The hooks will be called by the broker when a client connected/disconnected, a topic subscribed/unsubscribed or a message published/delivered. +The plugin could register handlers for hooks. The hooks will be called by the broker when a client connected/disconnected, a topic subscribed/unsubscribed or a message published/delivered: + ++------------------------+-------------+---------------------------------------+ +| Name | Type | Description | ++------------------------+-------------+---------------------------------------+ +| client.connected | foreach | Run when a client connected to the | +| | | broker successfully | ++------------------------+-------------+---------------------------------------+ +| client.subscribe | foldl | Run before a client subscribes topics | ++------------------------+-------------+---------------------------------------+ +| client.subscribe.after | foreach | Run after a client subscribed topics | ++------------------------+-------------+---------------------------------------+ +| client.unsubscribe | foldl | Run when a client unsubscribes topics | ++------------------------+-------------+---------------------------------------+ +| message.publish | foldl | Run when a message is published | ++------------------------+-------------+---------------------------------------+ +| message.acked | foreach | Run when a message is delivered | ++------------------------+-------------+---------------------------------------+ +| client.disconnected | foreach | Run when a client is disconnnected | ++----------------------- +-------------+---------------------------------------+ emqttd_plugin_template.erl for example:: @@ -554,27 +573,6 @@ emqttd_plugin_template.erl for example:: emqttd_broker:hook('message.acked', {?MODULE, on_message_acked}, {?MODULE, on_message_acked, [Env]}). -Hook List: - -+------------------------+-------------+---------------------------------------+ -| Name | Type | Description | -+------------------------+-------------+---------------------------------------+ -| client.connected | foreach | Run when a client connected to the | -| | | broker successfully | -+------------------------+-------------+---------------------------------------+ -| client.subscribe | foldl | Run before a client subscribes topics | -+------------------------+-------------+---------------------------------------+ -| client.subscribe.after | foreach | Run after a client subscribed topics | -+------------------------+-------------+---------------------------------------+ -| client.unsubscribe | foldl | Run when a client unsubscribes topics | -+------------------------+-------------+---------------------------------------+ -| message.publish | foldl | Run when a message is published | -+------------------------+-------------+---------------------------------------+ -| message.acked | foreach | Run when a message is delivered | -+------------------------+-------------+---------------------------------------+ -| client.disconnected | foreach | Run when a client is disconnnected | -+----------------------- +-------------+---------------------------------------+ - Register CLI Modules -------------------- From d8f176943a9e1ed35638f31ab1d07ab648618c1a Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 26 Feb 2016 00:27:36 +0800 Subject: [PATCH 18/69] index --- docs/source/bridge.rst | 6 +++--- docs/source/cluster.rst | 6 +++--- docs/source/index.rst | 20 +++++++++++++++++--- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/source/bridge.rst b/docs/source/bridge.rst index 73539c8b4..f2552f6bd 100644 --- a/docs/source/bridge.rst +++ b/docs/source/bridge.rst @@ -1,9 +1,9 @@ .. _bridge:: -============ -Bridge Guide -============ +====== +Bridge +====== ------------- emqttd Bridge diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst index 5ee1dfa11..1403fb0aa 100644 --- a/docs/source/cluster.rst +++ b/docs/source/cluster.rst @@ -1,9 +1,9 @@ .. _cluster: -============= -Cluster Guide -============= +========== +Clustering +========== ---------------------- Distributed Erlang/OPT diff --git a/docs/source/index.rst b/docs/source/index.rst index b1e9d785a..f41a9a6ef 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,8 +3,23 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to Erlang MQTT Broker's documentation! -============================================== +=========================== +emqttd - Erlang MQTT Broker +=========================== + +emqttd(Erlang MQTT Broker) is a massively scalable, clusterable MQTT V3.1/V3.1.1 broker written in Erlang/OTP. + +emqttd is fully open source and licensed under the Apache Version 2.0. emqttd implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports WebSocket, STOMP, SockJS, CoAP and MQTT-SN in the same time. + +Latest release of the emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. + +.. image:: ./_static/images/emqtt.png + +emqttd provides a scalable, enterprise grade, extensible open-source MQTT broker for IoT, M2M, Smart Hardware, Mobile Messaging and HTML5 Web Messaging Applications. + +Sensors, Mobiles, Web Browsers and Application Servers could be connected by emqttd brokers with asynchronous PUB/SUB MQTT messages. + +.. NOTE:: MQTT-SN,CoAP Protocols are planned to 1.x release. Contents: @@ -20,7 +35,6 @@ Contents: commands plugins - --------------------- Supports and Contacts --------------------- From f91fc9787ed115511fbee4e63f7cca1431c94964 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 26 Feb 2016 00:31:35 +0800 Subject: [PATCH 19/69] project --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index f41a9a6ef..7f5fb7d70 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,7 +15,7 @@ Latest release of the emqttd broker is scaling to 1.3 million MQTT connections o .. image:: ./_static/images/emqtt.png -emqttd provides a scalable, enterprise grade, extensible open-source MQTT broker for IoT, M2M, Smart Hardware, Mobile Messaging and HTML5 Web Messaging Applications. +The emqttd project provides a scalable, enterprise grade, extensible open-source MQTT broker for IoT, M2M, Smart Hardware, Mobile Messaging and HTML5 Web Messaging Applications. Sensors, Mobiles, Web Browsers and Application Servers could be connected by emqttd brokers with asynchronous PUB/SUB MQTT messages. From dcd738dcb9a4564a4bb9dcf4248a94af0e41343b Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 26 Feb 2016 11:16:59 +0800 Subject: [PATCH 20/69] index --- docs/TODO | 2 ++ docs/source/getstarted.rst | 8 +++++++- docs/source/index.rst | 36 ++++++++++++++++-------------------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/docs/TODO b/docs/TODO index a4defbe42..0aef6667e 100644 --- a/docs/TODO +++ b/docs/TODO @@ -6,3 +6,5 @@ 3. Plugins 4. User Guide + +5. Tuning Guide diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index cf50de096..391924000 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -18,10 +18,17 @@ emqttd is aimed to provide a solid, enterprise grade, extensible open-source MQT * Easy to extend * Solid stable +-------- +Features +-------- + ----------- Quick Start ----------- +Download and Install +-------------------- + Download binary packeges for Linux, Mac, FreeBSD and Windows from http://emqtt.io/downloads. .. code:: console @@ -37,7 +44,6 @@ Download binary packeges for Linux, Mac, FreeBSD and Windows from http://emqtt.i # Stop emqttd ./bin/emqttd stop ------------------ Build from Source ----------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 7f5fb7d70..123c96275 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,26 +19,6 @@ The emqttd project provides a scalable, enterprise grade, extensible open-source Sensors, Mobiles, Web Browsers and Application Servers could be connected by emqttd brokers with asynchronous PUB/SUB MQTT messages. -.. NOTE:: MQTT-SN,CoAP Protocols are planned to 1.x release. - -Contents: - -.. toctree:: - :maxdepth: 2 - - getstarted - install - config - cluster - bridge - guide - commands - plugins - ---------------------- -Supports and Contacts ---------------------- - +---------------+-----------------------------------------+ | Homepage: | http://emqtt.io | +---------------+-----------------------------------------+ @@ -55,6 +35,22 @@ Supports and Contacts | Author: | Feng Lee | +---------------+-----------------------------------------+ +.. NOTE:: MQTT-SN,CoAP Protocols are planned to 1.x release. + +Contents: + +.. toctree:: + :maxdepth: 2 + + getstarted + install + config + cluster + bridge + guide + commands + plugins + ------- License ------- From 09ab8bc44690965514f867ed102752d1304e1325 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 26 Feb 2016 13:57:23 +0800 Subject: [PATCH 21/69] config and getstarted --- docs/source/config.rst | 671 ++++++++++++++++++++++++++++++++++++- docs/source/getstarted.rst | 165 ++++++--- 2 files changed, 778 insertions(+), 58 deletions(-) diff --git a/docs/source/config.rst b/docs/source/config.rst index 5ac233112..4acf4e27b 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -1,14 +1,33 @@ .. _configuration: -====================== +============= Configuration -====================== +============= +emqttd消息服务器通过etc/目录下配置文件进行设置,主要配置文件包括: ------------- ++-------------------+-----------------------------------+ +| 配置文件 | 说明 | ++-------------------+-----------------------------------+ +| etc/vm.args | Erlang 虚拟机的参数设置 | ++-------------------+-----------------------------------+ +| etc/emqttd.config | emqttd消息服务器参数设置 | ++-------------------+-----------------------------------+ +| etc/acl.config | ACL(访问控制规则)设置 | ++-------------------+-----------------------------------+ +| etc/clients.config| 基于ClientId认证设置 | ++-------------------+-----------------------------------+ +| etc/rewrite.config| Rewrite扩展模块规则配置 | ++-------------------+-----------------------------------+ +| etc/ssl/* | SSL证书设置 | ++-------------------+-----------------------------------+ + +----------- etc/vm.args ------------- +----------- + +Configure parameters of Erlang VM: .. code:: @@ -65,14 +84,650 @@ etc/vm.args ## Tweak GC to run more often -env ERL_FULLSWEEP_AFTER 1000 ------------------- +etc/vm.args中两个最重要的参数: + ++-------+----------------------------------------------------------------------------------------------+ +| +P | Erlang虚拟机允许的最大进程数,一个MQTT连接会消耗2个Erlang进程,所以参数值 > 最大连接数 * 2 | ++-------+----------------------------------------------------------------------------------------------+ +| +Q | Erlang虚拟机允许的最大Port数量,一个MQTT连接消耗1个Port,所以参数值 > 最大连接数 | ++-------+----------------------------------------------------------------------------------------------+ + +etc/vm.args设置Erlang节点名、节点间通信Cookie:: + + -name emqttd@127.0.0.1 + + ## Cookie for distributed erlang + -setcookie emqttdsecretcookie + +.. NOTE:: + + Erlang/OTP平台应用多由分布的Erlang节点(进程)组成,每个Erlang节点(进程)需指配一个节点名,用于节点间通信互访。 + 所有互相通信的Erlang节点(进程)间通过一个共用的Cookie进行安全认证。 + +----------------- etc/emqttd.config ------------------- +----------------- +etc/emqttd.config是消息服务器的核心配置文件。Erlang程序由多个应用(application)组成,每个应用(application)有自身的环境参数, -Log Level -------------- +启动时候通过etc/emqttd.config文件加载。 + +etc/emqttd.config文件采用的是Erlang数据格式,kernel, sasl, emqttd是Erlang应用(application)名称,'[]'内是应用的环境参数列表。 + +.. code:: erlang + + [{kernel, [ + {start_timer, true}, + {start_pg2, true} + ]}, + {sasl, [ + {sasl_error_logger, {file, "log/emqttd_sasl.log"}} + ]}, + + ... + + {emqttd, [ + ... + ]} + ]. + +emqttd.config格式简要说明: + +1. [ ] : 列表,逗号分隔元素 + +2. { } : 元组,配置元组一般两个元素{Env, Value} + +3. % : 注释 + +Log Level and Destination +------------------------- + +emqttd消息服务器日志由lager应用(application)提供,日志相关设置在lager应用段落:: + + {lager, [ + ... + ]}, + +产品环境下默认只开启error日志,日志输出到logs/emqttd_error.log文件。'handlers'段落启用其他级别日志:: + + {handlers, [ + {lager_console_backend, info}, + + {lager_file_backend, [ + {formatter_config, [time, " ", pid, " [",severity,"] ", message, "\n"]}, + {file, "log/emqttd_info.log"}, + {level, info}, + {size, 104857600}, + {date, "$D0"}, + {count, 30} + ]}, + + {lager_file_backend, [ + {formatter_config, [time, " ", pid, " [",severity,"] ", message, "\n"]}, + {file, "log/emqttd_error.log"}, + {level, error}, + {size, 104857600}, + {date, "$D0"}, + {count, 30} + ]} + ]} + +.. WARNING:: 过多日志打印严重影响服务器性能,产品环境下建议开启error级别日志。 Broker Parameters ------------------ +emqttd消息服务器参数设置在emqttd应用段落,包括用户认证与访问控制设置,MQTT协议、会话、队列设置,扩展模块设置,TCP服务监听器设置:: + + {emqttd, [ + %% 用户认证与访问控制设置 + {access, [ + ... + ]}, + %% MQTT连接、协议、会话、队列设置 + {mqtt, [ + ... + ]}, + %% 消息服务器设置 + {broker, [ + ... + ]}, + %% 扩展模块设置 + {modules, [ + ... + ]}, + %% 插件目录设置 + {plugins, [ + ... + ]}, + + %% TCP监听器设置 + {listeners, [ + ... + ]}, + + %% Erlang虚拟机监控设置 + {sysmon, [ + ]} + ]} + +access用户认证设置 +------------------ + +emqttd消息服务器认证由一系列认证模块(module)或插件(plugin)提供,系统默认支持用户名、ClientID、LDAP、匿名(anonymouse)认证模块:: + + %% Authetication. Anonymous Default + {auth, [ + %% Authentication with username, password + %% Add users: ./bin/emqttd_ctl users add Username Password + %% {username, [{"test", "public"}]}, + + %% Authentication with clientid + % {clientid, [{password, no}, {file, "etc/clients.config"}]}, + + %% Authentication with LDAP + % {ldap, [ + % {servers, ["localhost"]}, + % {port, 389}, + % {timeout, 30}, + % {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, + % {ssl, fasle}, + % {sslopts, [ + % {"certfile", "ssl.crt"}, + % {"keyfile", "ssl.key"}]} + % ]}, + + %% Allow all + {anonymous, []} + ]}, + +系统默认采用匿名认证(anonymous),通过删除注释可开启其他认证方式。同时开启的多个认证模块组成认证链:: + + ---------------- ---------------- ------------ + Client --> | Username认证 | -ignore-> | ClientID认证 | -ignore-> | 匿名认证 | + ---------------- ---------------- ------------ + | | | + \|/ \|/ \|/ + allow | deny allow | deny allow | deny + +.. NOTE:: emqttd消息服务器还提供了MySQL、PostgreSQL、Redis、MongoDB认证插件, + 认证插件加载后认证模块失效。 + + +用户名密码认证 +.............. + +.. code:: erlang + + {username, [{test1, "passwd1"}, {test2, "passwd2"}]}, + +两种方式添加用户: + +1. 直接在[]中明文配置默认用户:: + + [{test1, "passwd1"}, {test2, "passwd2"}] + +2. 通过'./bin/emqttd_ctl'管理命令行添加用户:: + + $ ./bin/emqttd_ctl users add + +ClientID认证 +............ + +.. code:: erlang + + {clientid, [{password, no}, {file, "etc/clients.config"}]}, + +etc/clients.config文件中添加ClientID:: + + testclientid0 + testclientid1 127.0.0.1 + testclientid2 192.168.0.1/24 + + +LDAP认证 +........ + +.. code:: erlang + + {ldap, [ + {servers, ["localhost"]}, + {port, 389}, + {timeout, 30}, + {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, + {ssl, fasle}, + {sslopts, [ + {"certfile", "ssl.crt"}, + {"keyfile", "ssl.key"}]} + ]}, + + +匿名认证 +........ + +默认开启。允许任意客户端登录:: + + {anonymous, []} + + +access用户访问控制(ACL) +----------------------- + +emqttd消息服务器支持基于etc/acl.config文件或MySQL、PostgreSQL插件的访问控制规则。 + +默认开启基于etc/acl.config文件的访问控制:: + + %% ACL config + {acl, [ + %% Internal ACL module + {internal, [{file, "etc/acl.config"}, {nomatch, allow}]} + ]} + +etc/acl.config访问控制规则定义:: + + 允许|拒绝 用户|IP地址|ClientID 发布|订阅 主题列表 + +etc/acl.config默认访问规则设置:: + + {allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. + + {allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. + + {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. + + {allow, all}. + +.. NOTE:: 默认规则只允许本机用户订阅'$SYS/#'与'#' + +emqttd消息服务器接收到MQTT客户端发布(PUBLISH)或订阅(SUBSCRIBE)请求时,会逐条匹配ACL访问控制规则, + +直到匹配成功返回allow或deny。 + + +MQTT报文(Packet)尺寸与ClientID长度限制 +-------------------------------------- + +'packet'段落设置最大报文尺寸、最大客户端ID长度:: + + {packet, [ + + %% ClientID长度, 默认1024 + {max_clientid_len, 1024}, + + %% 最大报文长度,默认64K + {max_packet_size, 65536} + ]}, + + +MQTT客户端(Client)连接闲置时间 +------------------------------ + +'client'段落设置客户端最大允许闲置时间(Socket连接建立,但未发送CONNECT报文):: + + {client, [ + %% 单位: 秒 + {idle_timeout, 10} + ]}, + + +MQTT会话(Session)参数设置 +------------------------- + +'session'段落设置MQTT会话参数:: + + {session, [ + %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. + %% 0 means no limit + {max_inflight, 100}, + + %% Retry interval for redelivering QoS1/2 messages. + {unack_retry_interval, 20}, + + %% Awaiting PUBREL Timeout + {await_rel_timeout, 20}, + + %% Max Packets that Awaiting PUBREL, 0 means no limit + {max_awaiting_rel, 0}, + + %% Statistics Collection Interval(seconds) + {collect_interval, 20}, + + %% Expired after 2 days + {expired_after, 48} + + ]}, + +会话参数详细说明: + ++----------------------+----------------------------------------------------------+ +| max_inflight | 飞行窗口。最大允许同时下发的Qos1/2报文数,0表示没有限制。| +| | 窗口值越大,吞吐越高;窗口值越小,消息顺序越严格 | ++----------------------+----------------------------------------------------------+ +| unack_retry_interval | 下发QoS1/2消息未收到PUBACK响应的重试间隔 | ++----------------------+----------------------------------------------------------+ +| await_rel_timeout | 收到QoS2消息,等待PUBREL报文超时时间 | ++----------------------+----------------------------------------------------------+ +| max_awaiting_rel | 最大等待PUBREL的QoS2报文数 | ++----------------------+----------------------------------------------------------+ +| collect_interval | 采集会话统计数据间隔,默认0表示关闭统计 | ++----------------------+----------------------------------------------------------+ +| expired_after | 持久会话到期时间,从客户端断开算起,单位:小时 | ++----------------------+----------------------------------------------------------+ + +MQTT会话消息队列(MQueue)设置 +---------------------------- + +emqttd消息服务器会话通过队列缓存Qos1/Qos2消息: + +1. 持久会话(Session)的离线消息 + +2. 飞行窗口满而延迟下发的消息 + +队列参数设置:: + + {queue, [ + %% simple | priority + {type, simple}, + + %% Topic Priority: 0~255, Default is 0 + %% {priority, [{"topic/1", 10}, {"topic/2", 8}]}, + + %% Max queue length. Enqueued messages when persistent client disconnected, + %% or inflight window is full. + {max_length, infinity}, + + %% Low-water mark of queued messages + {low_watermark, 0.2}, + + %% High-water mark of queued messages + {high_watermark, 0.6}, + + %% Queue Qos0 messages? + {queue_qos0, true} + ]} + +队列参数说明: + ++----------------------+---------------------------------------------------+ +| type | 队列类型。simple: 简单队列,priority: 优先级队列 | ++----------------------+---------------------------------------------------+ +| priority | 主题(Topic)队列优先级设置 | ++----------------------+---------------------------------------------------+ +| max_length | 队列长度, infinity表示不限制 | ++----------------------+---------------------------------------------------+ +| low_watermark | 解除告警水位线 | ++----------------------+---------------------------------------------------+ +| high_watermark | 队列满告警水位线 | ++----------------------+---------------------------------------------------+ +| queue_qos0 | 是否缓存QoS0消息 | ++----------------------+---------------------------------------------------+ + +broker消息服务器参数 +-------------------- + +'broker'段落设置消息服务器内部模块参数。 + +sys_interval设置系统发布$SYS消息周期:: + + {sys_interval, 60}, + +broker retained消息设置 +----------------------- + +retained设置MQTT retain消息处理参数:: + + {retained, [ + %% retain消息过期时间,单位: 秒 + {expired_after, 0}, + + %% 最大retain消息数量 + {max_message_num, 100000}, + + %% retain消息payload最大尺寸 + {max_playload_size, 65536} + ]}, + ++-----------------+-------------------------------------+ +| expired_after | Retained消息过期时间,0表示永不过期 | ++-----------------+-------------------------------------+ +| max_message_num | 最大存储的Retained消息数量 | ++-----------------+-------------------------------------+ +| max_packet_size | Retained消息payload最大允许尺寸 | ++-----------------+-------------------------------------+ + +broker pubsub路由设置 +----------------------- + +发布/订阅(Pub/Sub)路由模块参数:: + + {pubsub, [ + %% PubSub Erlang进程池 + {pool_size, 8}, + + %% 订阅存储类型,ram: 内存, disc: 磁盘, false: 不保存 + {subscription, ram}, + + %% 路由老化时间 + {route_aging, 5} + ]}, + +broker bridge桥接参数 +----------------------- + +桥接参数设置:: + + {bridge, [ + %% 最大缓存桥接消息数 + {max_queue_len, 10000}, + + %% 桥接节点宕机检测周期,单位: 秒 + {ping_down_interval, 1} + ]} + + +modules扩展模块设置 +----------------------- + +emqtt消息服务器支持简单的扩展模块,用于定制服务器功能。默认支持presence、subscription、rewrite模块。 + +'presence'扩展模块会向$SYS主题(Topic)发布客户端上下线消息:: + + {presence, [{qos, 0}]}, + +'subscription'扩展模块支持客户端上线时,自动订阅或恢复订阅某些主题(Topic):: + + %% Subscribe topics automatically when client connected + {subscription, [ + %% Subscription from stored table + stored, + + %% $u will be replaced with username + {"$Q/username/$u", 1}, + + %% $c will be replaced with clientid + {"$Q/client/$c", 1} + ]} + +'rewrite'扩展模块支持重写主题(Topic)路径, 重写规则定义在etc/rewrite.config文件:: + + %% Rewrite rules + %% {rewrite, [{file, "etc/rewrite.config"}]} + +关于扩展模块详细介绍,请参考<用户指南>文档。 + +plugins插件目录设置 +------------------- + +.. code:: erlang + + {plugins, [ + %% Plugin App Library Dir + {plugins_dir, "./plugins"}, + + %% File to store loaded plugin names. + {loaded_file, "./data/loaded_plugins"} + ]}, + + +listeners监听器设置 +----------------------- + +emqttd消息服务器开启的MQTT协议、HTTP协议服务端,可通过listener设置TCP服务端口、最大允许连接数等参数。 + +emqttd消息服务器默认开启的TCP服务端口包括: + ++-----------+-----------------------------------+ +| 1883 | MQTT协议端口 | ++-----------+-----------------------------------+ +| 8883 | MQTT(SSL)端口 | ++-----------+-----------------------------------+ +| 8083 | MQTT(WebSocket), HTTP API端口 | ++-----------+-----------------------------------+ + +.. code:: erlang + + {listeners, [ + + {mqtt, 1883, [ + %% Size of acceptor pool + {acceptors, 16}, + + %% Maximum number of concurrent clients + {max_clients, 8192}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Connection Options + {connopts, [ + %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec + %% {rate_limit, "100,10"} %% 100K burst, 10K rate + ]}, + + %% Socket Options + {sockopts, [ + %Set buffer if hight thoughtput + %{recbuf, 4096}, + %{sndbuf, 4096}, + %{buffer, 4096}, + %{nodelay, true}, + {backlog, 1024} + ]} + ]}, + + {mqtts, 8883, [ + %% Size of acceptor pool + {acceptors, 4}, + + %% Maximum number of concurrent clients + {max_clients, 512}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% SSL certificate and key files + {ssl, [{certfile, "etc/ssl/ssl.crt"}, + {keyfile, "etc/ssl/ssl.key"}]}, + + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} + ]}, + %% WebSocket over HTTPS Listener + %% {https, 8083, [ + %% %% Size of acceptor pool + %% {acceptors, 4}, + %% %% Maximum number of concurrent clients + %% {max_clients, 512}, + %% %% Socket Access Control + %% {access, [{allow, all}]}, + %% %% SSL certificate and key files + %% {ssl, [{certfile, "etc/ssl/ssl.crt"}, + %% {keyfile, "etc/ssl/ssl.key"}]}, + %% %% Socket Options + %% {sockopts, [ + %% %{buffer, 4096}, + %% {backlog, 1024} + %% ]} + %%]}, + + %% HTTP and WebSocket Listener + {http, 8083, [ + %% Size of acceptor pool + {acceptors, 4}, + %% Maximum number of concurrent clients + {max_clients, 64}, + %% Socket Access Control + {access, [{allow, all}]}, + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} + ]} + ]}, + +listener参数说明: + ++-------------+-----------------------------------------------------------+ +| acceptors | TCP Acceptor池 | ++-------------+-----------------------------------------------------------+ +| max_clients | 最大允许TCP连接数 | ++-------------+-----------------------------------------------------------+ +| access | 允许访问的IP地址段设置,例如: [{allow, "192.168.1.0/24"}] | ++-------------+-----------------------------------------------------------+ +| connopts | 连接限速配置,例如限速10KB/秒: {rate_limit, "100,10"} | ++-------------+-----------------------------------------------------------+ +| sockopts | Socket参数设置 | ++-------------+-----------------------------------------------------------+ + +.. _config_acl: + +-------------- +etc/acl.config +-------------- + +emqttd消息服务器默认访问控制规则配置在etc/acl.config文件。 + +访问控制规则采用Erlang元组格式,访问控制模块逐条匹配规则:: + + --------- --------- --------- + Client -> | Rule1 | --nomatch--> | Rule2 | --nomatch--> | Rule3 | --> Default + --------- --------- --------- + | | | + match match match + \|/ \|/ \|/ + allow | deny allow | deny allow | deny + +etc/acl.config文件默认规则设置:: + + %% 允许'dashboard'用户订阅 '$SYS/#' + {allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. + + %% 允许本机用户发布订阅全部主题 + {allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. + + %% 拒绝用户订阅'$SYS#'与'#'主题 + {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. + + %% 上述规则无匹配,允许 + {allow, all}. + +.. _config_rewrite: + +------------------ +etc/rewrite.config +------------------ + +Rewrite扩展模块的规则配置文件,示例配置:: + + {topic, "x/#", [ + {rewrite, "^x/y/(.+)$", "z/y/$1"}, + {rewrite, "^x/(.+)$", "y/$1"} + ]}. + + {topic, "y/+/z/#", [ + {rewrite, "^y/(.+)/z/(.+)$", "y/z/$2"} + ]}. diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index 391924000..1145d614f 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -9,19 +9,45 @@ Get Started Overview -------- -emqttd is a massively scalable and clusterable MQTT V3.1/V3.1.1 broker written in Erlang/OTP. +emqttd(Erlang MQTT Broker) is an open source MQTT broker written in Erlang/OTP. Erlang/OTP is a concurrent, fault-tolerant, soft-realtime and distributed programming platform. MQTT is anextremely lightweight publish/subscribe messaging protocol powering IoT, M2M applications. -emqttd is aimed to provide a solid, enterprise grade, extensible open-source MQTT broker for IoT, M2M and Mobile applications that need to support ten millions of concurrent MQTT clients. +The emqttd project is aimed to implement a scalable, distributed, extensible open-source MQTT broker for IoT, M2M and Mobile applications that hope to handle ten millions of concurrent MQTT clients. + +The emqttd broker is: + +* Full MQTT V3.1/3.1.1 Protocol Specifications Support +* Easy to Install - Quick Install on Linux, FreeBSD, Mac and Windows +* Massively scalable - Scaling to 1 million connections on a single server +* Easy to extend - Hooks and plugins to customize or extend the broker +* Pluggable Authentication - LDAP, MySQL, PostgreSQL, Redis Authentication Plugins -* Easy to install -* Massively scalable -* Easy to extend -* Solid stable -------- Features -------- +* Full MQTT V3.1/V3.1.1 protocol specification support +* QoS0, QoS1, QoS2 Publish and Subscribe +* Session Management and Offline Messages +* Retained Message +* Last Will Message +* TCP/SSL Connection +* MQTT Over Websocket(SSL) +* HTTP Publish API +* STOMP protocol +* STOMP over SockJS +* $SYS/# Topics +* Client Authentication with clientId, ipaddress +* Client Authentication with username, password +* Client ACL control with ipaddress, clientid, username +* LDAP, Redis, MySQL, PostgreSQL authentication +* Cluster brokers on several servers. +* Bridge brokers locally or remotely +* mosquitto, RSMB bridge +* Extensible architecture with Hooks, Modules and Plugins +* Passed eclipse paho interoperability tests + + ----------- Quick Start ----------- @@ -29,7 +55,7 @@ Quick Start Download and Install -------------------- -Download binary packeges for Linux, Mac, FreeBSD and Windows from http://emqtt.io/downloads. +Download binary package for Linux, Mac, FreeBSD and Windows platform from http://emqtt.io/downloads. .. code:: console @@ -44,8 +70,10 @@ Download binary packeges for Linux, Mac, FreeBSD and Windows from http://emqtt.i # Stop emqttd ./bin/emqttd stop -Build from Source ------------------ +Installing from Source +----------------------- + +.. NOTE:: emqttd requires Erlang R17+ to build. .. code:: console @@ -60,71 +88,108 @@ Web Dashboard .. image:: ./_static/images/dashboard.png --------------------- -Features List --------------------- - -* Full MQTT V3.1/V3.1.1 protocol specification support -* QoS0, QoS1, QoS2 Publish and Subscribe -* Session Management and Offline Messages -* Retained Messages Support -* Last Will Message Support -* TCP/SSL Connection Support -* MQTT Over Websocket(SSL) Support -* HTTP Publish API Support -* [$SYS/brokers/#](https://github.com/emqtt/emqtt/wiki/$SYS-Topics-of-Broker) Support -* Client Authentication with clientId, ipaddress -* Client Authentication with username, password. -* Client ACL control with ipaddress, clientid, username. -* Cluster brokers on several servers. -* [Bridge](https://github.com/emqtt/emqttd/wiki/Bridge) brokers locally or remotely -* 500K+ concurrent clients connections per server -* Extensible architecture with Hooks, Modules and Plugins -* Passed eclipse paho interoperability tests - --------------------- +------------------- Modules and Plugins --------------------- +------------------- + +The emqttd broker could be extended by modules and plugins. Modules --------- +------- -* [emqttd_auth_clientid](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with ClientIds -* [emqttd_auth_username](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with Username and Password -* [emqttd_auth_ldap](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with LDAP -* [emqttd_mod_presence](https://github.com/emqtt/emqttd/wiki/Presence) - Publish presence message to $SYS topics when client connected or disconnected -* emqttd_mod_autosub - Subscribe topics when client connected -* [emqttd_mod_rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) - Topics rewrite like HTTP rewrite module ++-------------------------+-----------------------------------+ +| emqttd_auth_clientid | ClientId认证 | ++-------------------------+-----------------------------------+ +| emqttd_auth_username | 用户名密码认证 | ++-------------------------+-----------------------------------+ +| emqttd_auth_ldap | LDAP认证 | ++-------------------------+-----------------------------------+ +| emqttd_mod_presence | 客户端上下线状态消息发布 | ++-------------------------+-----------------------------------+ +| emqttd_mod_subscription | 客户端上线自动主题订阅 | ++-------------------------+-----------------------------------+ +| emqttd_mod_rewrite | 重写客户端订阅主题(Topic) | ++-------------------------+-----------------------------------+ + +扩展模块通过'etc/emqttd.config'配置文件的auth, modules段落启用。 + +例如启用用户名密码认证:: + + {access, [ + %% Authetication. Anonymous Default + {auth, [ + %% Authentication with username, password + {username, []}, + + ... + +启用客户端状态发布模块:: + + {modules, [ + %% Client presence management module. + %% Publish messages when client connected or disconnected + {presence, [{qos, 0}]} Plugins -------- -* [emqttd_plugin_template](https://github.com/emqtt/emqttd_plugin_template) - Plugin template and demo -* [emqttd_dashboard](https://github.com/emqtt/emqttd_dashboard) - Web Dashboard -* [emqttd_plugin_mysql](https://github.com/emqtt/emqttd_plugin_mysql) - Authentication with MySQL -* [emqttd_plugin_pgsql](https://github.com/emqtt/emqttd_plugin_pgsql) - Authentication with PostgreSQL -* [emqttd_plugin_kafka](https://github.com/emqtt/emqtt_kafka) - Publish MQTT Messages to Kafka -* [emqttd_plugin_redis](https://github.com/emqtt/emqttd_plugin_redis) - Redis Plugin -* [emqttd_plugin_mongo](https://github.com/emqtt/emqttd_plugin_mongo) - MongoDB Plugin -* [emqttd_stomp](https://github.com/emqtt/emqttd_stomp) - Stomp Protocol Plugin -* [emqttd_sockjs](https://github.com/emqtt/emqttd_sockjs) - SockJS(Stomp) Plugin -* [emqttd_recon](https://github.com/emqtt/emqttd_recon) - Recon Plugin ++-------------------------+-----------------------------------+ +| emqttd_plugin_template | 插件模版与演示代码 | ++-------------------------+-----------------------------------+ +| emqttd_dashboard | Web管理控制台,默认加载 | ++-------------------------+-----------------------------------+ +| emqttd_plugin_mysql | MySQL认证插件 | ++-------------------------+-----------------------------------+ +| emqttd_plugin_pgsql | PostgreSQL认证插件 | ++-------------------------+-----------------------------------+ +| emqttd_plugin_redis | Redis认证插件 | ++-------------------------+-----------------------------------+ +| emqttd_plugin_mongo | MongoDB认证插件 | ++-------------------------+-----------------------------------+ +| emqttd_stomp | Stomp协议插件 | ++-------------------------+-----------------------------------+ +| emqttd_sockjs | SockJS插件 | ++-------------------------+-----------------------------------+ +| emqttd_recon | Recon优化调测插件 | ++-------------------------+-----------------------------------+ + +扩展插件通过'bin/emqttd_ctl'管理命令行,加载启动运行。 + +例如启用PostgreSQL认证插件:: + + ./bin/emqttd_ctl plugins load emqttd_plugin_pgsql ---------------------------------- One million Connections ---------------------------------- +.. NOTE:: + + emqttd消息服务器默认设置,允许最大客户端连接是512,因为大部分操作系统'ulimit -n'限制为1024。 + +emqttd消息服务器当前版本,连接压力测试到130万线,8核心/32G内存的CentOS云服务器。 + +操作系统内核参数、TCP协议栈参数、Erlang虚拟机参数、emqttd最大允许连接数设置简述如下: + Linux Kernel Parameters ----------------------- +# 2M - 系统所有进程可打开的文件数量:: + .. code:: sysctl -w fs.file-max=2097152 sysctl -w fs.nr_open=2097152 +# 1M - 系统允许当前进程打开的文件数量:: + + ulimit -n 1048576 + TCP Stack Parameters ----------------------- +# backlog - Socket监听队列长度:: + .. code:: sysctl -w net.core.somaxconn=65536 From f63b826521c209ffb9a82cc64b6e54341e9094a9 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 26 Feb 2016 19:35:02 +0800 Subject: [PATCH 22/69] getstarted --- docs/source/getstarted.rst | 153 ++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 63 deletions(-) diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index 1145d614f..e04890246 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -13,7 +13,7 @@ emqttd(Erlang MQTT Broker) is an open source MQTT broker written in Erlang/OTP. The emqttd project is aimed to implement a scalable, distributed, extensible open-source MQTT broker for IoT, M2M and Mobile applications that hope to handle ten millions of concurrent MQTT clients. -The emqttd broker is: +Highlights of the emqttd broker: * Full MQTT V3.1/3.1.1 Protocol Specifications Support * Easy to Install - Quick Install on Linux, FreeBSD, Mac and Windows @@ -21,7 +21,6 @@ The emqttd broker is: * Easy to extend - Hooks and plugins to customize or extend the broker * Pluggable Authentication - LDAP, MySQL, PostgreSQL, Redis Authentication Plugins - -------- Features -------- @@ -47,7 +46,6 @@ Features * Extensible architecture with Hooks, Modules and Plugins * Passed eclipse paho interoperability tests - ----------- Quick Start ----------- @@ -55,7 +53,9 @@ Quick Start Download and Install -------------------- -Download binary package for Linux, Mac, FreeBSD and Windows platform from http://emqtt.io/downloads. +The emqttd broker is cross-platform, could be deployed on Linux, Mac, FreeBSD, Windows and Raspberry Pi. + +Download binary package from: http://emqtt.io/downloads. Installing on Mac, For example: .. code:: console @@ -71,7 +71,7 @@ Download binary package for Linux, Mac, FreeBSD and Windows platform from http:/ ./bin/emqttd stop Installing from Source ------------------------ +---------------------- .. NOTE:: emqttd requires Erlang R17+ to build. @@ -81,39 +81,56 @@ Installing from Source cd emqttd && make && make dist --------------------- +------------- Web Dashboard --------------------- +------------- + +A Web Dashboard will be loaded automatically when the emqttd broker is started successfully. + +The Dashboard helps to check running status of the broker, monitor statistics and metrics of MQTT packets, + +query clients, sessions, topics and subscriptions. + ++------------------+---------------------------+ +| Default Address | http://localhost:18083 | ++------------------+---------------------------+ +| Default User | admin | ++------------------+---------------------------+ +| Default Password | public | ++------------------+---------------------------+ .. image:: ./_static/images/dashboard.png - ------------------- Modules and Plugins ------------------- -The emqttd broker could be extended by modules and plugins. +The emqttd broker could be extended by Modules and Plugins. + +The authentication and ACL mechanism is usally implemented by a module or plugin. Modules ------- -+-------------------------+-----------------------------------+ -| emqttd_auth_clientid | ClientId认证 | -+-------------------------+-----------------------------------+ -| emqttd_auth_username | 用户名密码认证 | -+-------------------------+-----------------------------------+ -| emqttd_auth_ldap | LDAP认证 | -+-------------------------+-----------------------------------+ -| emqttd_mod_presence | 客户端上下线状态消息发布 | -+-------------------------+-----------------------------------+ -| emqttd_mod_subscription | 客户端上线自动主题订阅 | -+-------------------------+-----------------------------------+ -| emqttd_mod_rewrite | 重写客户端订阅主题(Topic) | -+-------------------------+-----------------------------------+ ++-------------------------+--------------------------------------------+ +| emqttd_auth_clientid | Authentication with ClientId | ++-------------------------+--------------------------------------------+ +| emqttd_auth_username | Authentication with Username and Password | ++-------------------------+--------------------------------------------+ +| emqttd_auth_ldap | Authentication with LDAP | ++-------------------------+--------------------------------------------+ +| emqttd_mod_presence | Publish presence message to $SYS topics | +| | when client connected or disconnected | ++-------------------------+--------------------------------------------+ +| emqttd_mod_subscription | Subscribe topics automatically when client | +| | connected | ++-------------------------+--------------------------------------------+ +| emqttd_mod_rewrite | Topics rewrite like HTTP rewrite module | ++-------------------------+--------------------------------------------+ -扩展模块通过'etc/emqttd.config'配置文件的auth, modules段落启用。 +Configure the 'auth', 'module' paragraph in 'etc/emqttd.config' to enable a module. -例如启用用户名密码认证:: +Enable 'emqttd_auth_username' module:: {access, [ %% Authetication. Anonymous Default @@ -123,7 +140,7 @@ Modules ... -启用客户端状态发布模块:: +Enable 'emqttd_mod_presence' module:: {modules, [ %% Client presence management module. @@ -131,71 +148,70 @@ Modules {presence, [{qos, 0}]} Plugins --------- +------- -+-------------------------+-----------------------------------+ -| emqttd_plugin_template | 插件模版与演示代码 | -+-------------------------+-----------------------------------+ -| emqttd_dashboard | Web管理控制台,默认加载 | -+-------------------------+-----------------------------------+ -| emqttd_plugin_mysql | MySQL认证插件 | -+-------------------------+-----------------------------------+ -| emqttd_plugin_pgsql | PostgreSQL认证插件 | -+-------------------------+-----------------------------------+ -| emqttd_plugin_redis | Redis认证插件 | -+-------------------------+-----------------------------------+ -| emqttd_plugin_mongo | MongoDB认证插件 | -+-------------------------+-----------------------------------+ -| emqttd_stomp | Stomp协议插件 | -+-------------------------+-----------------------------------+ -| emqttd_sockjs | SockJS插件 | -+-------------------------+-----------------------------------+ -| emqttd_recon | Recon优化调测插件 | -+-------------------------+-----------------------------------+ +A plugin is an Erlang application to extend the emqttd broker. -扩展插件通过'bin/emqttd_ctl'管理命令行,加载启动运行。 ++----------------------------+-----------------------------------+ +| `emqttd_plugin_template`_ | Plugin template and demo | ++----------------------------+-----------------------------------+ +| `emqttd_dashboard`_ | Web Dashboard | ++----------------------------+-----------------------------------+ +| `emqttd_plugin_mysql`_ | Authentication with MySQL | ++----------------------------+-----------------------------------+ +| `emqttd_plugin_pgsql`_ | Authentication with PostgreSQL | ++----------------------------+-----------------------------------+ +| `emqttd_plugin_redis`_ | Authentication with Redis | ++----------------------------+-----------------------------------+ +| `emqttd_plugin_mongo`_ | Authentication with MongoDB | ++----------------------------+-----------------------------------+ +| `emqttd_stomp`_ | STOMP Protocol Plugin | ++----------------------------+-----------------------------------+ +| `emqttd_sockjs`_ | SockJS(Stomp) Plugin | ++----------------------------+-----------------------------------+ +| `emqttd_recon`_ | Recon Plugin | ++----------------------------+-----------------------------------+ -例如启用PostgreSQL认证插件:: +A plugin could be enabled by 'bin/emqttd_ctl plugins load' command. + +For example, enable 'emqttd_plugin_pgsql' plugin:: ./bin/emqttd_ctl plugins load emqttd_plugin_pgsql ----------------------------------- +----------------------- One million Connections ----------------------------------- +----------------------- + +Latest release of the emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. .. NOTE:: - emqttd消息服务器默认设置,允许最大客户端连接是512,因为大部分操作系统'ulimit -n'限制为1024。 + The emqttd broker only allows 512 concurrent connections by default, for 'ulimit -n' limit is 1024 on most platform. -emqttd消息服务器当前版本,连接压力测试到130万线,8核心/32G内存的CentOS云服务器。 - -操作系统内核参数、TCP协议栈参数、Erlang虚拟机参数、emqttd最大允许连接数设置简述如下: +We need tune the OS Kernel, TCP Stack, Erlang VM and emqttd broker for one million connections benchmark. Linux Kernel Parameters ----------------------- -# 2M - 系统所有进程可打开的文件数量:: - .. code:: + # 2M: sysctl -w fs.file-max=2097152 sysctl -w fs.nr_open=2097152 -# 1M - 系统允许当前进程打开的文件数量:: - + # 1M: ulimit -n 1048576 TCP Stack Parameters ------------------------ - -# backlog - Socket监听队列长度:: +-------------------- .. code:: + # backlog sysctl -w net.core.somaxconn=65536 Erlang VM ------------------ +--------- emqttd/etc/vm.args:: @@ -210,8 +226,8 @@ emqttd/etc/vm.args:: -env ERTS_MAX_PORTS 1048576 -emqttd.config ------------------ +emqttd broker +------------- emqttd/etc/emqttd.config:: @@ -239,6 +255,7 @@ Test Client sysctl -w net.ipv4.ip_local_port_range="500 65535" echo 1000000 > /proc/sys/fs/nr_open + ulimit -n 100000 ---------------------- emqtt Client Libraries @@ -261,3 +278,13 @@ GitHub: https://github.com/emqtt .. _CocoaMQTT: https://github.com/emqtt/CocoaMQTT .. _QMQTT: https://github.com/emqtt/qmqtt +.. _emqttd_plugin_template: https://github.com/emqtt/emqttd_plugin_template +.. _emqttd_dashboard: https://github.com/emqtt/emqttd_dashboard +.. _emqttd_plugin_mysql: https://github.com/emqtt/emqttd_plugin_mysql +.. _emqttd_plugin_pgsql: https://github.com/emqtt/emqttd_plugin_pgsql +.. _emqttd_plugin_redis: https://github.com/emqtt/emqttd_plugin_redis +.. _emqttd_plugin_mongo: https://github.com/emqtt/emqttd_plugin_mongo +.. _emqttd_stomp: https://github.com/emqtt/emqttd_stomp +.. _emqttd_sockjs: https://github.com/emqtt/emqttd_sockjs +.. _emqttd_recon: https://github.com/emqtt/emqttd_recon + From 1158ccbbcdf6f60a71d7b0f288d2279f21713625 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 26 Feb 2016 20:06:49 +0800 Subject: [PATCH 23/69] misc fix --- docs/source/getstarted.rst | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index e04890246..e3972600a 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -9,7 +9,7 @@ Get Started Overview -------- -emqttd(Erlang MQTT Broker) is an open source MQTT broker written in Erlang/OTP. Erlang/OTP is a concurrent, fault-tolerant, soft-realtime and distributed programming platform. MQTT is anextremely lightweight publish/subscribe messaging protocol powering IoT, M2M applications. +emqttd(Erlang MQTT Broker) is an open source MQTT broker written in Erlang/OTP. Erlang/OTP is a concurrent, fault-tolerant, soft-realtime and distributed programming platform. MQTT is an extremely lightweight publish/subscribe messaging protocol powering IoT, M2M and Mobile applications. The emqttd project is aimed to implement a scalable, distributed, extensible open-source MQTT broker for IoT, M2M and Mobile applications that hope to handle ten millions of concurrent MQTT clients. @@ -31,16 +31,17 @@ Features * Retained Message * Last Will Message * TCP/SSL Connection -* MQTT Over Websocket(SSL) +* MQTT Over WebSocket(SSL) * HTTP Publish API * STOMP protocol * STOMP over SockJS * $SYS/# Topics -* Client Authentication with clientId, ipaddress -* Client Authentication with username, password -* Client ACL control with ipaddress, clientid, username -* LDAP, Redis, MySQL, PostgreSQL authentication -* Cluster brokers on several servers. +* ClientID Authentication +* IpAddress Authentication +* Username and Password Authentication +* Access control based on IpAddress, ClientID, Username +* Authentication with LDAP, Redis, MySQL, PostgreSQL +* Cluster brokers on several servers * Bridge brokers locally or remotely * mosquitto, RSMB bridge * Extensible architecture with Hooks, Modules and Plugins @@ -55,7 +56,9 @@ Download and Install The emqttd broker is cross-platform, could be deployed on Linux, Mac, FreeBSD, Windows and Raspberry Pi. -Download binary package from: http://emqtt.io/downloads. Installing on Mac, For example: +Download binary package from: http://emqtt.io/downloads. + +Installing on Mac, For example: .. code:: console @@ -81,6 +84,8 @@ Installing from Source cd emqttd && make && make dist + cd rel/emqttd && ./bin/emqttd console + ------------- Web Dashboard ------------- @@ -107,7 +112,7 @@ Modules and Plugins The emqttd broker could be extended by Modules and Plugins. -The authentication and ACL mechanism is usally implemented by a module or plugin. +The authentication and ACL mechanism is usually implemented by a module or plugin. Modules ------- From 385ab3ec77eb510b9babec83c8e15d05a76cf288 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 26 Feb 2016 21:52:18 +0800 Subject: [PATCH 24/69] config --- docs/source/config.rst | 92 +++++++++++++++++--------------------- docs/source/getstarted.rst | 11 ++--- docs/source/index.rst | 1 - 3 files changed, 46 insertions(+), 58 deletions(-) diff --git a/docs/source/config.rst b/docs/source/config.rst index 4acf4e27b..72215f476 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -5,31 +5,29 @@ Configuration ============= -emqttd消息服务器通过etc/目录下配置文件进行设置,主要配置文件包括: +Configuration files are under 'etc/' folder, including: +-------------------+-----------------------------------+ -| 配置文件 | 说明 | +| File | Description | +-------------------+-----------------------------------+ -| etc/vm.args | Erlang 虚拟机的参数设置 | +| etc/vm.args | Erlang VM Arguments | +-------------------+-----------------------------------+ -| etc/emqttd.config | emqttd消息服务器参数设置 | +| etc/emqttd.config | emqttd broker Config | +-------------------+-----------------------------------+ -| etc/acl.config | ACL(访问控制规则)设置 | +| etc/acl.config | ACL Config | +-------------------+-----------------------------------+ -| etc/clients.config| 基于ClientId认证设置 | +| etc/clients.config| ClientId Authentication | +-------------------+-----------------------------------+ -| etc/rewrite.config| Rewrite扩展模块规则配置 | +| etc/rewrite.config| Rewrite Rules | +-------------------+-----------------------------------+ -| etc/ssl/* | SSL证书设置 | +| etc/ssl/* | SSL certificate and key files | +-------------------+-----------------------------------+ ----------- etc/vm.args ----------- -Configure parameters of Erlang VM: - -.. code:: +Configure and Optimize Erlang VM:: ##------------------------------------------------------------------------- ## Name of the node @@ -84,35 +82,33 @@ Configure parameters of Erlang VM: ## Tweak GC to run more often -env ERL_FULLSWEEP_AFTER 1000 -etc/vm.args中两个最重要的参数: +The two most important parameters in etc/vm.args: -+-------+----------------------------------------------------------------------------------------------+ -| +P | Erlang虚拟机允许的最大进程数,一个MQTT连接会消耗2个Erlang进程,所以参数值 > 最大连接数 * 2 | -+-------+----------------------------------------------------------------------------------------------+ -| +Q | Erlang虚拟机允许的最大Port数量,一个MQTT连接消耗1个Port,所以参数值 > 最大连接数 | -+-------+----------------------------------------------------------------------------------------------+ ++-------+---------------------------------------------------------------------------+ +| +P | Max number of Erlang proccesses. A MQTT client consumes two proccesses. | +| | The value should be larger than max_clients * 2 | ++-------+---------------------------------------------------------------------------+ +| +Q | Max number of Erlang Ports. A MQTT client consumes one port. | +| | The value should be larger than max_clients. | ++-------+---------------------------------------------------------------------------+ -etc/vm.args设置Erlang节点名、节点间通信Cookie:: +The name and cookie of Erlang Node should be configured when clustering:: - -name emqttd@127.0.0.1 + -name emqttd@host_or_ip ## Cookie for distributed erlang -setcookie emqttdsecretcookie -.. NOTE:: - - Erlang/OTP平台应用多由分布的Erlang节点(进程)组成,每个Erlang节点(进程)需指配一个节点名,用于节点间通信互访。 - 所有互相通信的Erlang节点(进程)间通过一个共用的Cookie进行安全认证。 - ----------------- etc/emqttd.config ----------------- -etc/emqttd.config是消息服务器的核心配置文件。Erlang程序由多个应用(application)组成,每个应用(application)有自身的环境参数, +The main configuration file for emqttd broker. -启动时候通过etc/emqttd.config文件加载。 +File Syntax +----------- -etc/emqttd.config文件采用的是Erlang数据格式,kernel, sasl, emqttd是Erlang应用(application)名称,'[]'内是应用的环境参数列表。 +The config consists of a list of Erlang Applications and their environments. .. code:: erlang @@ -131,24 +127,22 @@ etc/emqttd.config文件采用的是Erlang数据格式,kernel, sasl, emqttd是E ]} ]. -emqttd.config格式简要说明: +The file adopts Erlang Term Syntax: -1. [ ] : 列表,逗号分隔元素 +1. [ ]: List, seperated by comma +2. { }: Tuple, Usually {Env, Value} +3. % : comment -2. { } : 元组,配置元组一般两个元素{Env, Value} +Log Level and File +------------------ -3. % : 注释 - -Log Level and Destination -------------------------- - -emqttd消息服务器日志由lager应用(application)提供,日志相关设置在lager应用段落:: +Logger of emqttd broker is implemented by 'lager' application:: {lager, [ ... ]}, -产品环境下默认只开启error日志,日志输出到logs/emqttd_error.log文件。'handlers'段落启用其他级别日志:: +Configure log handlers:: {handlers, [ {lager_console_backend, info}, @@ -172,47 +166,45 @@ emqttd消息服务器日志由lager应用(application)提供,日志相关设 ]} ]} -.. WARNING:: 过多日志打印严重影响服务器性能,产品环境下建议开启error级别日志。 - -Broker Parameters +emqttd Application ------------------ -emqttd消息服务器参数设置在emqttd应用段落,包括用户认证与访问控制设置,MQTT协议、会话、队列设置,扩展模块设置,TCP服务监听器设置:: +The MQTT broker is implemented by erlang 'emqttd' application:: {emqttd, [ - %% 用户认证与访问控制设置 + %% Authentication and Authorization {access, [ ... ]}, - %% MQTT连接、协议、会话、队列设置 + %% MQTT Protocol Options {mqtt, [ ... ]}, - %% 消息服务器设置 + %% Broker Options {broker, [ ... ]}, - %% 扩展模块设置 + %% Modules {modules, [ ... ]}, - %% 插件目录设置 + %% Plugins {plugins, [ ... ]}, - %% TCP监听器设置 + %% Listeners {listeners, [ ... ]}, - %% Erlang虚拟机监控设置 + %% Erlang System Monitor {sysmon, [ ]} ]} -access用户认证设置 ------------------- +Authentication +-------------- emqttd消息服务器认证由一系列认证模块(module)或插件(plugin)提供,系统默认支持用户名、ClientID、LDAP、匿名(anonymouse)认证模块:: diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index e3972600a..d24e9bb3f 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -18,6 +18,7 @@ Highlights of the emqttd broker: * Full MQTT V3.1/3.1.1 Protocol Specifications Support * Easy to Install - Quick Install on Linux, FreeBSD, Mac and Windows * Massively scalable - Scaling to 1 million connections on a single server +* Cluster and Bridge Support * Easy to extend - Hooks and plugins to customize or extend the broker * Pluggable Authentication - LDAP, MySQL, PostgreSQL, Redis Authentication Plugins @@ -76,7 +77,7 @@ Installing on Mac, For example: Installing from Source ---------------------- -.. NOTE:: emqttd requires Erlang R17+ to build. +.. NOTE:: emqttd broker requires Erlang R17+ to build. .. code:: console @@ -92,9 +93,7 @@ Web Dashboard A Web Dashboard will be loaded automatically when the emqttd broker is started successfully. -The Dashboard helps to check running status of the broker, monitor statistics and metrics of MQTT packets, - -query clients, sessions, topics and subscriptions. +The Dashboard helps to check running status of the broker, monitor statistics and metrics of MQTT packets, query clients, sessions, topics and subscriptions. +------------------+---------------------------+ | Default Address | http://localhost:18083 | @@ -110,9 +109,7 @@ query clients, sessions, topics and subscriptions. Modules and Plugins ------------------- -The emqttd broker could be extended by Modules and Plugins. - -The authentication and ACL mechanism is usually implemented by a module or plugin. +The Authentication and ACL mechanism is usually implemented by a Module or Plugin. Modules ------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 123c96275..941265591 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -47,7 +47,6 @@ Contents: config cluster bridge - guide commands plugins From f7d6ba5c78dc5bb34681aa1efa385a0b07e89adc Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 26 Feb 2016 21:54:22 +0800 Subject: [PATCH 25/69] catalog --- docs/source/getstarted.rst | 10 +++++----- docs/source/guide.rst | 26 ++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index 1145d614f..2b0b407e8 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -81,9 +81,9 @@ Installing from Source cd emqttd && make && make dist --------------------- +------------- Web Dashboard --------------------- +------------- .. image:: ./_static/images/dashboard.png @@ -240,9 +240,9 @@ Test Client sysctl -w net.ipv4.ip_local_port_range="500 65535" echo 1000000 > /proc/sys/fs/nr_open ----------------------- -emqtt Client Libraries ----------------------- +--------------------- +MQTT Client Libraries +--------------------- GitHub: https://github.com/emqtt diff --git a/docs/source/guide.rst b/docs/source/guide.rst index 1cc2c5e15..d3e7a893e 100644 --- a/docs/source/guide.rst +++ b/docs/source/guide.rst @@ -5,14 +5,32 @@ User Guide ========== +------------------------ +Pluggable Authentication +------------------------ + ---------- MQTT Basic ---------- +Publish/Subscribe +----------------- + Understanding QoS ------------------- +----------------- + +Retained Message +---------------- + +Will Message +------------ + +---------------- +HTTP Publish API +---------------- + +------------------- +MQTT Over WebSocket +------------------- --------- -HTTP API --------- From f2638c396a6dca702244b9b99bd2405c3342c54e Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 26 Feb 2016 23:22:54 +0800 Subject: [PATCH 26/69] authentication --- docs/source/config.rst | 204 +++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 112 deletions(-) diff --git a/docs/source/config.rst b/docs/source/config.rst index 461dde235..145426959 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -246,39 +246,38 @@ emqttd消息服务器认证由一系列认证模块(module)或插件(plugin)提 认证插件加载后认证模块失效。 -用户名密码认证 -.............. +Username and Password Authentication +.................................... .. code:: erlang {username, [{test1, "passwd1"}, {test2, "passwd2"}]}, -两种方式添加用户: +Two ways to configure users: -1. 直接在[]中明文配置默认用户:: +1. Configure username and plain password directly:: - [{test1, "passwd1"}, {test2, "passwd2"}] + {username, [{test1, "passwd1"}, {test2, "passwd2"}]}, -2. 通过'./bin/emqttd_ctl'管理命令行添加用户:: +2. Add users by './bin/emqttd_ctl' command:: $ ./bin/emqttd_ctl users add -ClientID认证 -............ +ClientID Authentication +....................... .. code:: erlang {clientid, [{password, no}, {file, "etc/clients.config"}]}, -etc/clients.config文件中添加ClientID:: +Configure ClientIDs in etc/clients.config:: testclientid0 testclientid1 127.0.0.1 testclientid2 192.168.0.1/24 - -LDAP认证 -........ +LDAP Authentication +................... .. code:: erlang @@ -294,85 +293,59 @@ LDAP认证 ]}, -匿名认证 -........ +Anonymous Authentication +........................ -默认开启。允许任意客户端登录:: +Allow any clients connect to the broker:: {anonymous, []} -access用户访问控制(ACL) ------------------------ +ACL Config +---------- -emqttd消息服务器支持基于etc/acl.config文件或MySQL、PostgreSQL插件的访问控制规则。 +Enable the default ACL module:: -默认开启基于etc/acl.config文件的访问控制:: - - %% ACL config {acl, [ %% Internal ACL module {internal, [{file, "etc/acl.config"}, {nomatch, allow}]} ]} -etc/acl.config访问控制规则定义:: +MQTT Packet and ClientID +------------------------ - 允许|拒绝 用户|IP地址|ClientID 发布|订阅 主题列表 - -etc/acl.config默认访问规则设置:: - - {allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. - - {allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. - - {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. - - {allow, all}. - -.. NOTE:: 默认规则只允许本机用户订阅'$SYS/#'与'#' - -emqttd消息服务器接收到MQTT客户端发布(PUBLISH)或订阅(SUBSCRIBE)请求时,会逐条匹配ACL访问控制规则, - -直到匹配成功返回allow或deny。 - - -MQTT报文(Packet)尺寸与ClientID长度限制 --------------------------------------- - -'packet'段落设置最大报文尺寸、最大客户端ID长度:: +.. code:: {packet, [ - %% ClientID长度, 默认1024 + %% Max ClientId Length Allowed {max_clientid_len, 1024}, - %% 最大报文长度,默认64K + %% Max Packet Size Allowed, 64K default {max_packet_size, 65536} ]}, +MQTT Client Idle Timeout +------------------------ -MQTT客户端(Client)连接闲置时间 ------------------------------- - -'client'段落设置客户端最大允许闲置时间(Socket连接建立,但未发送CONNECT报文):: +.. code:: {client, [ - %% 单位: 秒 + %% Socket is connected, but no 'CONNECT' packet received {idle_timeout, 10} ]}, +MQTT Session +------------ -MQTT会话(Session)参数设置 -------------------------- - -'session'段落设置MQTT会话参数:: +.. code:: {session, [ %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. %% 0 means no limit {max_inflight, 100}, - %% Retry interval for redelivering QoS1/2 messages. + %% Retry interval for unacked QoS1/2 messages. {unack_retry_interval, 20}, %% Awaiting PUBREL Timeout @@ -381,7 +354,7 @@ MQTT会话(Session)参数设置 %% Max Packets that Awaiting PUBREL, 0 means no limit {max_awaiting_rel, 0}, - %% Statistics Collection Interval(seconds) + %% Interval of Statistics Collection(seconds) {collect_interval, 20}, %% Expired after 2 days @@ -389,33 +362,33 @@ MQTT会话(Session)参数设置 ]}, -会话参数详细说明: +Session parameters: +----------------------+----------------------------------------------------------+ -| max_inflight | 飞行窗口。最大允许同时下发的Qos1/2报文数,0表示没有限制。| -| | 窗口值越大,吞吐越高;窗口值越小,消息顺序越严格 | +| max_inflight | Max number of QoS1/2 messages that can be delivered in | +| | the same time | +----------------------+----------------------------------------------------------+ -| unack_retry_interval | 下发QoS1/2消息未收到PUBACK响应的重试间隔 | +| unack_retry_interval | Retry interval for unacked QoS1/2 messages. | +----------------------+----------------------------------------------------------+ -| await_rel_timeout | 收到QoS2消息,等待PUBREL报文超时时间 | +| await_rel_timeout | Awaiting PUBREL Timeout | +----------------------+----------------------------------------------------------+ -| max_awaiting_rel | 最大等待PUBREL的QoS2报文数 | +| max_awaiting_rel | Max number of Packets that Awaiting PUBREL | +----------------------+----------------------------------------------------------+ -| collect_interval | 采集会话统计数据间隔,默认0表示关闭统计 | +| collect_interval | Interval of Statistics Collection | +----------------------+----------------------------------------------------------+ -| expired_after | 持久会话到期时间,从客户端断开算起,单位:小时 | +| expired_after | Expired after | +----------------------+----------------------------------------------------------+ -MQTT会话消息队列(MQueue)设置 ----------------------------- +Message Queue +------------- -emqttd消息服务器会话通过队列缓存Qos1/Qos2消息: +The message queue of session stores: -1. 持久会话(Session)的离线消息 +1. Offline messages for persistent session. -2. 飞行窗口满而延迟下发的消息 +2. Pending messages for inflight window is full -队列参数设置:: +Queue parameters:: {queue, [ %% simple | priority @@ -438,79 +411,70 @@ emqttd消息服务器会话通过队列缓存Qos1/Qos2消息: {queue_qos0, true} ]} -队列参数说明: - +----------------------+---------------------------------------------------+ -| type | 队列类型。simple: 简单队列,priority: 优先级队列 | +| type | Queue type: simple or priority | +----------------------+---------------------------------------------------+ -| priority | 主题(Topic)队列优先级设置 | +| priority | Topic priority | +----------------------+---------------------------------------------------+ -| max_length | 队列长度, infinity表示不限制 | +| max_length | Max Queue size, infinity means no limit | +----------------------+---------------------------------------------------+ -| low_watermark | 解除告警水位线 | +| low_watermark | Low watermark | +----------------------+---------------------------------------------------+ -| high_watermark | 队列满告警水位线 | +| high_watermark | High watermark | +----------------------+---------------------------------------------------+ -| queue_qos0 | 是否缓存QoS0消息 | +| queue_qos0 | If Qos0 message queued? | +----------------------+---------------------------------------------------+ -broker消息服务器参数 --------------------- - -'broker'段落设置消息服务器内部模块参数。 - -sys_interval设置系统发布$SYS消息周期:: - - {sys_interval, 60}, - -broker retained消息设置 +Sys Interval of Broker ----------------------- -retained设置MQTT retain消息处理参数:: +.. code:: + + %% System interval of publishing $SYS messages + {sys_interval, 60}, + +Retained messages +----------------- + +.. code:: {retained, [ - %% retain消息过期时间,单位: 秒 + %% Expired after seconds, never expired if 0 {expired_after, 0}, - %% 最大retain消息数量 + %% Maximum number of retained messages {max_message_num, 100000}, - %% retain消息payload最大尺寸 + %% Max Payload Size of retained message {max_playload_size, 65536} ]}, -+-----------------+-------------------------------------+ -| expired_after | Retained消息过期时间,0表示永不过期 | -+-----------------+-------------------------------------+ -| max_message_num | 最大存储的Retained消息数量 | -+-----------------+-------------------------------------+ -| max_packet_size | Retained消息payload最大允许尺寸 | -+-----------------+-------------------------------------+ +PubSub and Router +----------------- -broker pubsub路由设置 ------------------------ - -发布/订阅(Pub/Sub)路由模块参数:: +.. code:: erlang {pubsub, [ - %% PubSub Erlang进程池 + %% PubSub Pool {pool_size, 8}, - %% 订阅存储类型,ram: 内存, disc: 磁盘, false: 不保存 + %% Subscription: disc | ram | false {subscription, ram}, - %% 路由老化时间 + %% Route aging time(seconds) {route_aging, 5} ]}, Bridge Parameters ----------------- +.. code:: erlang + {bridge, [ - %% 最大缓存桥接消息数 + %% Bridge Queue Size {max_queue_len, 10000}, - %% 桥接节点宕机检测周期,单位: 秒 + %% Ping Interval of bridge node {ping_down_interval, 1} ]} @@ -541,7 +505,6 @@ Enable Modules %% Rewrite rules {rewrite, [{file, "etc/rewrite.config"}]} - Plugins Folder -------------- @@ -559,7 +522,7 @@ Plugins Folder TCP Listeners ------------- -Congfigure the TCP listener for MQTT, MQTT(SSL) and HTTP Protocols. +Congfigure the TCP listeners for MQTT, MQTT(SSL) and HTTP Protocols. The most important parameter is 'max_clients' - max concurrent clients allowed. @@ -704,6 +667,23 @@ An ACL rule is an Erlang tuple. The Access control module of emqttd broker match .. _config_rewrite: +------------------ +etc/clients.config +------------------ + +Enable ClientId Authentication in 'etc/emqttd.config':: + + {auth, [ + %% Authentication with clientid + {clientid, [{password, no}, {file, "etc/clients.config"}]} + ]}, + +Configure all allowed ClientIDs, IP Addresses in etc/clients.config:: + + testclientid0 + testclientid1 127.0.0.1 + testclientid2 192.168.0.1/24 + ------------------ etc/rewrite.config ------------------ From 3e9a2caffd5500389865323408b5d3c9eb11b2a0 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 26 Feb 2016 23:30:35 +0800 Subject: [PATCH 27/69] config --- docs/source/config.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/config.rst b/docs/source/config.rst index 145426959..164fcf52e 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -246,8 +246,8 @@ emqttd消息服务器认证由一系列认证模块(module)或插件(plugin)提 认证插件加载后认证模块失效。 -Username and Password Authentication -.................................... +Username Authentication +....................... .. code:: erlang @@ -301,8 +301,8 @@ Allow any clients connect to the broker:: {anonymous, []} -ACL Config ----------- +ACL +--- Enable the default ACL module:: @@ -379,8 +379,8 @@ Session parameters: | expired_after | Expired after | +----------------------+----------------------------------------------------------+ -Message Queue -------------- +MQTT Message Queue +------------------ The message queue of session stores: From 1c523217eb53a729410052af05aa09d787f34be9 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 26 Feb 2016 23:46:23 +0800 Subject: [PATCH 28/69] config --- docs/source/config.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/source/config.rst b/docs/source/config.rst index 164fcf52e..07203e5f1 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -203,10 +203,12 @@ The MQTT broker is implemented by erlang 'emqttd' application:: ]} ]} -Authentication --------------- +Pluggable Authentication +------------------------ -emqttd消息服务器认证由一系列认证模块(module)或插件(plugin)提供,系统默认支持用户名、ClientID、LDAP、匿名(anonymouse)认证模块:: +The emqttd broker supports pluggable authentication mechanism with a list of modules and plugins. + +The broker provides Username, ClientId, LDAP and anonymous authentication modules by default:: %% Authetication. Anonymous Default {auth, [ @@ -233,18 +235,16 @@ emqttd消息服务器认证由一系列认证模块(module)或插件(plugin)提 {anonymous, []} ]}, -系统默认采用匿名认证(anonymous),通过删除注释可开启其他认证方式。同时开启的多个认证模块组成认证链:: +The modules enabled in the same time compose an authentication chain: - ---------------- ---------------- ------------ - Client --> | Username认证 | -ignore-> | ClientID认证 | -ignore-> | 匿名认证 | - ---------------- ---------------- ------------ + ---------------- ---------------- ------------- + Client --> | Username | -ignore-> | ClientID | -ignore-> | Anonymous | + ---------------- ---------------- ------------- | | | \|/ \|/ \|/ allow | deny allow | deny allow | deny -.. NOTE:: emqttd消息服务器还提供了MySQL、PostgreSQL、Redis、MongoDB认证插件, - 认证插件加载后认证模块失效。 - +.. NOTE:: There are also MySQL、PostgreSQL、Redis、MongoDB Authentication Plugins. Username Authentication ....................... From 952d62f39a46f761da7fea07dfc2171b645324e3 Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 27 Feb 2016 11:24:38 +0800 Subject: [PATCH 29/69] config --- docs/source/config.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/config.rst b/docs/source/config.rst index 07203e5f1..a1effb803 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -103,12 +103,12 @@ The name and cookie of Erlang Node should be configured when clustering:: etc/emqttd.config ----------------- -The main configuration file for emqttd broker. +This is the main emqttd broker configuration file. File Syntax ----------- -The config consists of a list of Erlang Applications and their environments. +The file users the standard Erlang config syntax, consists of a list of erlang applications and their environments. .. code:: erlang From 6ced604dc414cb9de28b9f505b44e6b13f95f9ee Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 27 Feb 2016 15:05:11 +0800 Subject: [PATCH 30/69] tune --- docs/source/index.rst | 1 + docs/source/tune.rst | 144 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 docs/source/tune.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 941265591..7025497de 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -49,6 +49,7 @@ Contents: bridge commands plugins + tune ------- License diff --git a/docs/source/tune.rst b/docs/source/tune.rst new file mode 100644 index 000000000..609330d62 --- /dev/null +++ b/docs/source/tune.rst @@ -0,0 +1,144 @@ + +.. _tune: + +============ +Tuning Guide +============ + +Tuning the Linux Kernel, Networking, Erlang VM and emqttd broker for one million concurrent MQTT connections. + +------------------- +Linux Kernel Tuning +------------------- + +The system-wide limit on max opened file handles:: + + # 2 millions system-wide + sysctl -w fs.file-max=2097152 + sysctl -w fs.nr_open=2097152 + echo 2097152 > /proc/sys/fs/nr_open + +The limit on opened file handles for current session:: + + ulimit -n 1048576 + +/etc/sysctl.conf +---------------- + +Add the 'fs.file-max' to /etc/sysctl.conf to make the changes permanent:: + + fs.file-max = 1048576 + +/etc/security/limits.conf +------------------------- + +Persist the limits on opened file handles for users in /etc/security/limits.conf:: + + * soft nofile 1048576 + * hard nofile 1048576 + +-------------- +Network Tuning +-------------- + +Increase number of incoming connections backlog:: + + sysctl -w net.core.somaxconn=32768 + net.ipv4.tcp_max_syn_backlog=16384 + sysctl -w net.core.netdev_max_backlog=16384 + +Local Port Range:: + + sysctl -w net.ipv4.ip_local_port_range=2000 65535 + +Read/Write Buffer for TCP connections:: + + sysctl -w net.core.rmem_default=262144 + sysctl -w net.core.wmem_default=262144 + sysctl -w net.core.rmem_max=16777216 + sysctl -w net.core.wmem_max=16777216 + sysctl -w net.core.optmem_max=16777216 + + #sysctl -w net.ipv4.tcp_mem='16777216 16777216 16777216' + sysctl -w net.ipv4.tcp_rmem='1024 4096 16777216' + sysctl -w net.ipv4.tcp_wmem='1024 4096 16777216' + +Connection tracking:: + + sysctl -w net.nf_conntrack_max=1000000 + sysctl -w net.netfilter.nf_conntrack_max=1000000 + sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30 + +The TIME-WAIT Buckets Pool, Recycling and Reuse:: + + net.ipv4.tcp_max_tw_buckets=1048576 + net.ipv4.tcp_tw_recycle = 1 + net.ipv4.tcp_tw_reuse = 1 + +Timeout for FIN-WAIT-2 sockets:: + + net.ipv4.tcp_fin_timeout = 15 + +--------- +Erlang VM +--------- + +Tuning and optimize the Erlang VM in etc/vm.args file:: + + ## max process numbers + +P 2097152 + + ## Sets the maximum number of simultaneously existing ports for this system + +Q 1048576 + + ## Increase number of concurrent ports/sockets, deprecated in R17 + -env ERL_MAX_PORTS 1048576 + + -env ERTS_MAX_PORTS 1048576 + + ## Mnesia and SSL will create temporary ets tables. + -env ERL_MAX_ETS_TABLES 1024 + + ## Tweak GC to run more often + -env ERL_FULLSWEEP_AFTER 1000 + +------------- +emqttd broker +------------- + +Tune the acceptor pool, max_clients limit and sockopts for TCP listener in etc/emqttd.config:: + + {mqtt, 1883, [ + %% Size of acceptor pool + {acceptors, 64}, + + %% Maximum number of concurrent clients + {max_clients, 1000000}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Connection Options + {connopts, [ + %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec + %% {rate_limit, "100,10"} %% 100K burst, 10K rate + ]}, + ... + +-------------- +Client Machine +-------------- + +Tune the client machine to benchmark emqttd broker:: + + sysctl -w net.ipv4.ip_local_port_range="500 65535" + sysctl -w fs.file-max=1000000 + echo 1000000 > /proc/sys/fs/nr_open + ulimit -n 100000 + +--------------- +emqtt_benchmark +--------------- + +Test tool for concurrent connections: http://github.com/emqtt/emqtt_benchmark + From 4e73476dfe4b39d31aa33bc8998ee400e8ae625e Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 27 Feb 2016 15:22:15 +0800 Subject: [PATCH 31/69] tune --- docs/source/getstarted.rst | 1 + docs/source/tune.rst | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index a9c9fe8a2..ccddcbe94 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -200,6 +200,7 @@ Linux Kernel Parameters # 2M: sysctl -w fs.file-max=2097152 sysctl -w fs.nr_open=2097152 + echo 2097152 > /proc/sys/fs/nr_open # 1M: ulimit -n 1048576 diff --git a/docs/source/tune.rst b/docs/source/tune.rst index 609330d62..8e5f1bba1 100644 --- a/docs/source/tune.rst +++ b/docs/source/tune.rst @@ -25,7 +25,7 @@ The limit on opened file handles for current session:: /etc/sysctl.conf ---------------- -Add the 'fs.file-max' to /etc/sysctl.conf to make the changes permanent:: +Add the 'fs.file-max' to /etc/sysctl.conf, make the changes permanent:: fs.file-max = 1048576 @@ -49,7 +49,7 @@ Increase number of incoming connections backlog:: Local Port Range:: - sysctl -w net.ipv4.ip_local_port_range=2000 65535 + sysctl -w net.ipv4.ip_local_port_range=1000 65535 Read/Write Buffer for TCP connections:: @@ -63,7 +63,7 @@ Read/Write Buffer for TCP connections:: sysctl -w net.ipv4.tcp_rmem='1024 4096 16777216' sysctl -w net.ipv4.tcp_wmem='1024 4096 16777216' -Connection tracking:: +Connection Tracking:: sysctl -w net.nf_conntrack_max=1000000 sysctl -w net.netfilter.nf_conntrack_max=1000000 @@ -85,7 +85,7 @@ Erlang VM Tuning and optimize the Erlang VM in etc/vm.args file:: - ## max process numbers + ## max number of erlang processes +P 2097152 ## Sets the maximum number of simultaneously existing ports for this system From 8b8ceaac2ff060f9869d36b89d107e97a2ecceab Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 27 Feb 2016 15:24:46 +0800 Subject: [PATCH 32/69] Tuning --- docs/source/design.rst | 327 +++++++++++++++++++++++++++++++++++++++++ docs/source/faq.rst | 77 ++++++++++ docs/source/guide.rst | 275 ++++++++++++++++++++++++++++++++++ docs/source/tune.rst | 6 +- 4 files changed, 682 insertions(+), 3 deletions(-) create mode 100644 docs/source/faq.rst diff --git a/docs/source/design.rst b/docs/source/design.rst index 2b8f7e884..729f82dbc 100644 --- a/docs/source/design.rst +++ b/docs/source/design.rst @@ -1,3 +1,330 @@ ============== Design Guide ============== + +--------------- +Pubsub Sequence +--------------- + +## PubSub Sequence + +### Clean Session = 1 + +``` + +title PubSub Sequence(Clean Session = 1) + +ClientA-->PubSub: Publish Message +PubSub-->ClientB: Dispatch Message +``` + +![PubSub_CleanSess_1](http://emqtt.io/static/img/design/PubSub_CleanSess_1.png) + +### Clean Session = 0 + +``` +title PubSub Sequence(Clean Session = 0) + +ClientA-->SessionA: Publish Message +SessionA-->PubSub: Publish Message +PubSub-->SessionB: Dispatch Message +SessionB-->ClientB: Dispatch Message + +``` +![PubSub_CleanSess_0](http://emqtt.io/static/img/design/PubSub_CleanSess_0.png) + + +## Qos + +PubQos | SubQos | In Message | Out Message +-------|--------|------------|------------- +0 | 0 | 0 | 0 +0 | 1 | 0 | 0 +0 | 2 | 0 | 0 +1 | 0 | 1 | 0 +1 | 1 | 1 | 1 +1 | 2 | 1 | 1 +2 | 0 | 2 | 0 +2 | 1 | 2 | 1 +2 | 2 | 2 | 2 + + +## Topic Functions Benchmark + +Mac Air(11): + +Function | Time(microseconds) +-------------|-------------------- +match | 6.25086 +triples | 13.86881 +words | 3.41177 +binary:split | 3.03776 + +iMac: + +Function | Time(microseconds) +-------------|-------------------- +match | 3.2348 +triples | 6.93524 +words | 1.89616 +binary:split | 1.65243 + + +-------------- +Cluster Design +-------------- + +## Cluster Design + +1. One 'disc_copies' node and many 'ram_copies' nodes. + + 2. Topic trie tree will be copied to every clusterd node. + + 3. Subscribers to topic will be stored in each node and will not be copied. + + ## Cluster Strategy + + TODO:... + + 1. A message only gets forwarded to other cluster nodes if a cluster node is interested in it. this reduces the network traffic tremendously, because it prevents nodes from forwarding unnecessary messages. + + 2. As soon as a client on a node subscribes to a topic it becomes known within the cluster. If one of the clients somewhere in the cluster is publishing to this topic, the message will be delivered to its subscriber no matter to which cluster node it is connected. + + .... + +## Cluster Architecture + +![Cluster Design](http://emqtt.io/static/img/Cluster.png) +## Cluster Command + +```sh +./bin/emqttd_ctl cluster DiscNode +``` + +## Mnesia Example + +``` +(emqttd3@127.0.0.1)3> mnesia:info(). +---> Processes holding locks <--- +---> Processes waiting for locks <--- +---> Participant transactions <--- +---> Coordinator transactions <--- +---> Uncertain transactions <--- +---> Active tables <--- +mqtt_retained : with 6 records occupying 221 words of mem +topic_subscriber: with 0 records occupying 305 words of mem +topic_trie_node: with 129 records occupying 3195 words of mem +topic_trie : with 128 records occupying 3986 words of mem +topic : with 93 records occupying 1797 words of mem +schema : with 6 records occupying 1081 words of mem +===> System info in version "4.12.4", debug level = none <=== +opt_disc. Directory "/Users/erylee/Projects/emqttd/rel/emqttd3/data/mnesia" is NOT used. +use fallback at restart = false +running db nodes = ['emqttd2@127.0.0.1','emqttd@127.0.0.1','emqttd3@127.0.0.1'] +stopped db nodes = [] +master node tables = [] +remote = [] +ram_copies = [mqtt_retained,schema,topic,topic_subscriber,topic_trie, +topic_trie_node] +disc_copies = [] +disc_only_copies = [] +[{'emqttd2@127.0.0.1',ram_copies}, +{'emqttd3@127.0.0.1',ram_copies}, +{'emqttd@127.0.0.1',disc_copies}] = [schema] +[{'emqttd2@127.0.0.1',ram_copies}, +{'emqttd3@127.0.0.1',ram_copies}, +{'emqttd@127.0.0.1',ram_copies}] = [topic,topic_trie,topic_trie_node, +mqtt_retained] +[{'emqttd3@127.0.0.1',ram_copies}] = [topic_subscriber] +44 transactions committed, 5 aborted, 0 restarted, 0 logged to disc + 0 held locks, 0 in queue; 0 local transactions, 0 remote + 0 transactions waits for other nodes: [] + ``` + + ## Cluster vs Bridge + + Cluster will copy topic trie tree between nodes, Bridge will not. + + + +------------- +Hooks Design +------------- + +## Overview + +emqttd supported a simple hooks mechanism in 0.8.0 release to extend the broker. The designed is improved in 0.9.0 release. + +## API + +emqttd_broker Hook API: + +``` +-export([hook/3, unhook/2, foreach_hooks/2, foldl_hooks/3]). +``` + +### Hook + +``` +-spec hook(Hook :: atom(), Name :: any(), MFA :: mfa()) -> ok | {error, any()}. +hook(Hook, Name, MFA) -> + ... + ``` + + ### Unhook + + ``` + -spec unhook(Hook :: atom(), Name :: any()) -> ok | {error, any()}. + unhook(Hook, Name) -> + ... + ``` + + ### Foreach Hooks + + ``` + -spec foreach_hooks(Hook :: atom(), Args :: list()) -> any(). + foreach_hooks(Hook, Args) -> + ... + ``` + + ### Foldl Hooks + + ``` + -spec foldl_hooks(Hook :: atom(), Args :: list(), Acc0 :: any()) -> any(). + foldl_hooks(Hook, Args, Acc0) -> + ... + ``` + + ## Hooks + + Name | Type | Description + --------------- | ----------| -------------- + client.connected | foreach | Run when client connected successfully + client.subscribe | foldl | Run before client subscribe topics + client.subscribe.after | foreach | Run After client subscribe topics + client.unsubscribe | foldl | Run when client unsubscribe topics + message.publish | foldl | Run when message is published + message.acked | foreach | Run when message is acked + client.disconnected | foreach | Run when client is disconnnected + + ## End-to-End Message Pub/Ack + + Could use 'message.publish', 'message.acked' hooks to implement end-to-end message pub/ack: + + ``` + PktId <-- --> MsgId <-- --> MsgId <-- --> PktId + |<--- Qos --->|<---PubSub--->|<-- Qos -->| + ``` +## Limit + +The design is experimental. + + +-------------- +Plugin Design +-------------- + +## Overview + +**Notice that 0.11.0 release use rebar to manage plugin's deps.** + +A plugin is just an erlang application that extends emqttd broker. + +The plugin application should be put in "emqttd/plugins/" folder to build. + + +## Plugin Project + +You could create a standalone plugin project outside emqttd, and then add it to "emqttd/plugins/" folder by "git submodule". + +Git submodule to compile emqttd_dashboard plugin with the broker, For example: + +``` +git submodule add https://github.com/emqtt/emqttd_dashboard.git plugins/emqttd_dashboard +make && make dist +``` + +## plugin.config + +**Each plugin should have a 'etc/plugin.config' file** + +For example, project structure of emqttd_dashboard plugin: + +``` +LICENSE +README.md +ebin +etc +priv +rebar.config +src +``` + +etc/plugin.config for emqttd_dashboard plugin: + +``` +[ +{emqttd_dashboard, [ +{listener, +{emqttd_dashboard, 18083, [ +{acceptors, 4}, +{max_clients, 512}]}} +]} +]. +``` + +## rebar.config + +**Plugin should use 'rebar.config' to manage depencies** + +emqttd_plugin_pgsql plugin's rebar.config, for example: + +``` +%% -*- erlang -*- + +{deps, [ +{epgsql, ".*",{git, "https://github.com/epgsql/epgsql.git", {branch, "master"}}} +]}. +``` + +## Build emqttd with plugins + +Put all the plugins you required in 'plugins/' folder of emqttd project, and then: + +``` +make && make dist +``` + +## Load Plugin + +'./bin/emqttd_ctl' to load/unload plugin, when emqttd broker started. + +``` +./bin/emqttd_ctl plugins load emqttd_plugin_demo + +./bin/emqttd_ctl plugins unload emqttd_plugin_demo +``` + +## List Plugins + +``` +./bin/emqttd_ctl plugins list +``` + +## API + +``` +%% Load all active plugins after broker started +emqttd_plugins:load() + +%% Load new plugin +emqttd_plugins:load(Name) + +%% Unload all active plugins before broker stopped +emqttd_plugins:unload() + +%% Unload a plugin +emqttd_plugins:unload(Name) +``` + + diff --git a/docs/source/faq.rst b/docs/source/faq.rst new file mode 100644 index 000000000..e8933b2b9 --- /dev/null +++ b/docs/source/faq.rst @@ -0,0 +1,77 @@ +=== +FAQ +=== + + +##### Q1. Is port 4369 and another random port secure? + +``` +HI, when start emqttd , I found the port 4369 and another random port(63703) is open, are this security ? + +Example: +tcp 0 0 0.0.0.0:4369 0.0.0.0:* LISTEN 13736/epmd +tcp 0 0 0.0.0.0:8083 0.0.0.0:* LISTEN 16745/beam.smp +tcp 0 0 0.0.0.0:8883 0.0.0.0:* LISTEN 16745/beam.smp +tcp 0 0 0.0.0.0:63703 0.0.0.0:* LISTEN 16745/beam.smp +tcp 0 0 0.0.0.0:1883 0.0.0.0:* LISTEN 16745/beam.smp + +1883: mqtt no ssl +8883: mqtt with ssl +8083: websocket +``` + +4369 and some random ports are opened by erlang node for internal communication. Configure your firewall to allow 1883, 8883, 8083 ports to be accessed from outside for security. + +Access control of emqttd broker has two layers: + +eSockd TCP Acceptor, Ipaddress based Access Control, Example: + +``` +{access, [{deny, "192.168.1.1"}, +{allow, "192.168.1.0/24"}, +{deny, all}]} +``` + +MQTT Subscribe/Publish Access Control by etc/acl.config, Example: + +``` +{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. + +{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. + +{allow, all}. +``` + +##### Q2. cannot compile emqttd under Chinese folder? + +It seems that rebar cannot support Chinese folder name. + +##### Q3. emqttd is ready for production ? + +The core features are solid and scalable. A small full-time team with many contributors are developing this project. You could submit issues if any feature requests or bug reports. + +##### Q4. Benchmark and performance issue + +Wiki: https://github.com/emqtt/emqttd/wiki/One-Million-Connections + +##### Q5. 'session' identified by clientID?when session expired what will happen?All queued messages will be deleted and subscribed topics will be deleted too?when reconnected, need redo subscription?(#150) + +When a client connected to broker with 'clean session' flag 0, a session identified by clientId will be created. The session will expire after 48 hours(configured in etc/emqttd.config) if no client connections bind with it, and all queued messages and subscriptions will be dropped. + +##### Q6. "{max_queued_messages, 100}" in 0.8 release or "{queue, {max_length, 1000},..." means queue for one session or one topic?If it stands for session,while one topic has lots of offline messages(100), the user's other topic offline messages will be flushed? (#150) + +For session. Topic just dispatch messages to clients or sessions that matched the subscriptions. Will Flood. + +##### Q7. About the retained message, how to config one topic only keep the latest retained message, the older retained messages will be auto deleted?(#150) + +By default, the broker only keep the latest retained message of one topic. + +##### Q8. When the persistent client with 'clean session' flag 0 is offline but not expired, will the broker put session's subscribed topic new messages to session's queue?(#150) + +Yes + +##### Q9. If max_length of queue is 100, when the session subscribed topic1 and topic2, what will happen when topic1 fill 70 messages, then topic2 fill 80 messages? After the reconnection, will the session lose first 50 message?(#150) + +Lose the oldest 50 messages. + + diff --git a/docs/source/guide.rst b/docs/source/guide.rst index d3e7a893e..039e531f0 100644 --- a/docs/source/guide.rst +++ b/docs/source/guide.rst @@ -29,8 +29,283 @@ Will Message HTTP Publish API ---------------- +emqttd support HTTP API to publish message from your application server to MQTT clients. For example:: + +curl -v --basic -u user:passwd -d "qos=1&retain=0&topic=/a/b/c&message=hello from http..." -k http://localhost:8083/mqtt/publish + +HTTP API URL +------------ + +HTTP POST http://host:8083/mqtt/publish + +HTTP Parameters +--------------- + +Name | Description +--------|--------------- +client | ClientId +qos | QoS(0, 1, 2) +retain | Retain(0, 1) +topic | Topic +message | Message + + ------------------- MQTT Over WebSocket ------------------- +## Overview + +emqttd 0.7.0 release support MQTT over WebSocket(SSL). + +## URI + +``` +ws(s)://host:8083/mqtt +``` + +## Sec-WebSocket-Protocol + +'mqttv3.1', 'mqttv3.1.1' + + +----------------------- +Trace Topic or Client +----------------------- + +## Overview + +emqttd could trace packets received/sent from/to specific client, or trace publish/subscribe to specific topic. + +emqttd use lager:trace_file api and write trace log to file. + + +## Trace Commands + +### Trace client + +``` +./bin/emqttd_ctl trace client "ClientId" "trace_clientid.log" +``` + +### Trace topic + +``` +./bin/emqttd_ctl trace topic "Topic" "trace_topic.log" +``` + +### Stop Trace + +``` +./bin/emqttd_ctl trace client "ClientId" off +./bin/emqttd_ctl trace topic "Topic" off +``` + +### Lookup Traces + +``` +./bin/emqttd_ctl trace list +``` + + + +------------------- +$SYS Topics +------------------- + +NOTICE: This is the design of 0.9.0 release + +## Overview + +For emqttd is clustered, $SYS Topics of broker is started with: + +``` +$SYS/brokers/${node} +``` + +${node} is erlang node of clustered brokers. For example: + +``` +$SYS/brokers/emqttd@host1/version +$SYS/brokers/emqttd@host2/version +``` + +## Broker $SYS Topics + +Topic | Description +-------------------------------|------------ +$SYS/brokers | Broker nodes +$SYS/brokers/${node}/version | Broker Version +$SYS/brokers/${node}/uptime | Broker Uptime +$SYS/brokers/${node}/datetime | Broker DateTime +$SYS/brokers/${node}/sysdescr | Broker Description +  +## Client $SYS Topics + +Start with: $SYS/brokers/${node}/clients/ + +Topic | Payload(json) | Description +----------------------|---------------------|--------------- +${clientid}/connected | {ipaddress: "127.0.0.1", username: "test", session: false, version: 3, connack: 0, ts: 1432648482} | Publish when client connected +${clientid}/disconnected | {reason: "normal" | "keepalive_timeout" | "conn_closed"} + +Parameters of 'connected' Payload: + +``` +ipaddress: "127.0.0.1", +username: "test", +session: false, +protocol: 3, +connack: 0, +ts: 1432648482 +``` + +Parameters of 'disconnected' Payload: + +``` +reason: normal, +ts: 1432648486 +``` + +## Statistics $SYS Topics + +Start with '$SYS/brokers/${node}/stats/' + +### Client Stats + +Topic | Description +-------------------------------------|------------ +clients/count | count of current connected clients +clients/max | max connected clients in the same time + +### Session Stats + +Topic | Description +-----------------|------------ +sessions/count | count of current sessions +sessions/max | max number of sessions + +### Subscriber Stats + +Topic | Description +------------------|------------ +subscriptions/count | count of current subscriptions +subscriptions/max | max number of subscriptions + +### Topic Stats + +Topic | Description +------------------|------------ +topics/count | count of current topics +topics/max | max number of topics + +### Queue Stats + +Topic | Description +------------------|------------ +queues/count | count of current queues +queues/max | max number of queues + + +## Metrics $SYS Topics + +Start with '$SYS/brokers/${node}/metrics/' + +### Bytes sent and received + +Topic | Description +------------------------------------|------------ +bytes/received | MQTT Bytes Received since broker started +bytes/sent | MQTT Bytes Sent since the broker started + +### Packets sent and received + +Topic | Description +-------------------------|------------ +packets/received | MQTT Packets received +packets/sent | MQTT Packets sent +packets/connect | MQTT CONNECT Packet received +packets/connack | MQTT CONNACK Packet sent +packets/publish/received | MQTT PUBLISH packets received +packets/publish/sent | MQTT PUBLISH packets sent +packets/subscribe | MQTT SUBSCRIBE Packets received +packets/suback | MQTT SUBACK packets sent +packets/unsubscribe | MQTT UNSUBSCRIBE Packets received +packets/unsuback | MQTT UNSUBACK Packets sent +packets/pingreq | MQTT PINGREQ packets received +packets/pingresp | MQTT PINGRESP Packets sent +packets/disconnect | MQTT DISCONNECT Packets received + +### Messages sent and received + +Topic | Description +---------------------------------------|------------------- +messages/received | Messages Received +messages/sent | Messages Sent +messages/retained | Messages Retained +messages/stored | TODO: Messages Stored +messages/dropped | Messages Dropped + +## Alarm Topics + +Start with '$SYS/brokers/${node}/alarms/' + +Topic | Description +-----------------|------------------- +${alarmId}/alert | New Alarm +${alarmId}/clear | Clear Alarm + +## Log + +'$SYS/brokers/${node}/logs/${severity}' + +Severity | Description +-----------|------------------- +debug | Debug Log +info | Info Log +notice | Notice Log +warning | Warning Log +error | Error Log +critical | Critical Log + +## Sysmon + +Start with '$SYS/brokers/${node}/sysmon/' + +Topic | Description +-----------------|------------------- +long_gc | Long GC Warning +long_schedule | Long Schedule +large_heap | Large Heap Warning +busy_port | Busy Port Warning +busy_dist_port | Busy Dist Port + +## Log + +'$SYS/brokers/${node}/log/${severity}' + +Severity | Description +------------|------------------- +debug | Debug +info | Info Log +notice | Notice Log +warning | Warning Log +error | Error Log +critical | Critical Log +alert | Alert Log + +## VM Load Topics + +Start with '$SYS/brokers/${node}/vm/' + +Topic | Description +-----------------|------------------- +memory/* | TODO +cpu/* | TODO +processes/* | TODO + +## Sys Interval + +sys_interval: 1 minute default + diff --git a/docs/source/tune.rst b/docs/source/tune.rst index 8e5f1bba1..c4152a532 100644 --- a/docs/source/tune.rst +++ b/docs/source/tune.rst @@ -79,9 +79,9 @@ Timeout for FIN-WAIT-2 sockets:: net.ipv4.tcp_fin_timeout = 15 ---------- -Erlang VM ---------- +---------------- +Erlang VM Tuning +---------------- Tuning and optimize the Erlang VM in etc/vm.args file:: From 3c675d4d1d5c74c6aa8c3ff091a38ff066a9724b Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 28 Feb 2016 13:45:10 +0800 Subject: [PATCH 33/69] guide --- docs/TODO | 10 - docs/source/config.rst | 8 +- docs/source/getstarted.rst | 2 +- docs/source/guide.rst | 526 ++++++++++++++++++++++++++++++++++--- docs/source/mqtt.rst | 102 ++++++- 5 files changed, 589 insertions(+), 59 deletions(-) delete mode 100644 docs/TODO diff --git a/docs/TODO b/docs/TODO deleted file mode 100644 index 0aef6667e..000000000 --- a/docs/TODO +++ /dev/null @@ -1,10 +0,0 @@ - -1. Getstarted - -2. Commands - -3. Plugins - -4. User Guide - -5. Tuning Guide diff --git a/docs/source/config.rst b/docs/source/config.rst index a1effb803..e2d6b070f 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -251,15 +251,15 @@ Username Authentication .. code:: erlang - {username, [{test1, "passwd1"}, {test2, "passwd2"}]}, + {username, [{client1, "passwd1"}, {client2, "passwd2"}]}, Two ways to configure users: 1. Configure username and plain password directly:: - {username, [{test1, "passwd1"}, {test2, "passwd2"}]}, + {username, [{client1, "passwd1"}, {client2, "passwd2"}]}, -2. Add users by './bin/emqttd_ctl' command:: +2. Add user by './bin/emqttd_ctl users' command:: $ ./bin/emqttd_ctl users add @@ -296,7 +296,7 @@ LDAP Authentication Anonymous Authentication ........................ -Allow any clients connect to the broker:: +Allow any client to connect to the broker:: {anonymous, []} diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index ccddcbe94..b5fc505b7 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -181,7 +181,7 @@ For example, enable 'emqttd_plugin_pgsql' plugin:: ./bin/emqttd_ctl plugins load emqttd_plugin_pgsql ----------------------- -One million Connections +One Million Connections ----------------------- Latest release of the emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. diff --git a/docs/source/guide.rst b/docs/source/guide.rst index 039e531f0..4a09880d0 100644 --- a/docs/source/guide.rst +++ b/docs/source/guide.rst @@ -5,73 +5,509 @@ User Guide ========== ------------------------- -Pluggable Authentication ------------------------- +-------------- +Authentication +-------------- ----------- -MQTT Basic ----------- +The emqttd broker supports to authenticate MQTT client with ClientID, Username/Password, IpAddress and even HTTP Cookies. -Publish/Subscribe ------------------ +The authentication is provided by a list of extended modules, or MySQL, PostgreSQL and Redis Plugins. -Understanding QoS ------------------ +Enable an authentication module in etc/emqttd.config:: -Retained Message ----------------- + %% Authentication and Authorization + {access, [ + %% Authetication. Anonymous Default + {auth, [ + %% Authentication with username, password + %{username, []}, + + %% Authentication with clientid + %{clientid, [{password, no}, {file, "etc/clients.config"}]}, -Will Message + %% Authentication with LDAP + % {ldap, [ + % {servers, ["localhost"]}, + % {port, 389}, + % {timeout, 30}, + % {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, + % {ssl, fasle}, + % {sslopts, [ + % {"certfile", "ssl.crt"}, + % {"keyfile", "ssl.key"}]} + % ]}, + + %% Allow all + {anonymous, []} + ]}, + +.. NOTE:: "%%" comments the line. + +If we enable several modules in the same time, the authentication process:: + + ---------------- ---------------- ------------- + Client --> | Username | -ignore-> | ClientID | -ignore-> | Anonymous | + ---------------- ---------------- ------------- + | | | + \|/ \|/ \|/ + allow | deny allow | deny allow | deny + +The authentication plugins developed by emqttd: + ++---------------------------+---------------------------+ +| Plugin | Description | ++===========================+===========================+ +| `emqttd_plugin_mysql`_ | MySQL Auth/ACL Plugin | ++---------------------------+---------------------------+ +| `emqttd_plugin_pgsql`_ | PostgreSQL Auth/ACL Plugin| ++---------------------------+---------------------------+ +| `emqttd_plugin_redis`_ | Redis Auth/ACL Plugin | ++---------------------------+---------------------------+ + +.. NOTE:: If we load an authentication plugin, the authentication modules will be disabled. + +Username +-------- + +Authenticate MQTT client with Username/Password:: + + {username, [{client1, "passwd1"}, {client1, "passwd2"}]}, + +Two ways to add users: + +1. Configure username and plain password directly:: + + {username, [{client1, "passwd1"}, {client1, "passwd2"}]}, + +2. Add user by './bin/emqttd_ctl users' command:: + + $ ./bin/emqttd_ctl users add + +ClientId +-------- + +.. code:: erlang + + {clientid, [{password, no}, {file, "etc/clients.config"}]}, + +Configure ClientIDs in etc/clients.config:: + + testclientid0 + testclientid1 127.0.0.1 + testclientid2 192.168.0.1/24 + +LDAP +---- + +.. code:: erlang + + {ldap, [ + {servers, ["localhost"]}, + {port, 389}, + {timeout, 30}, + {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, + {ssl, fasle}, + {sslopts, [ + {"certfile", "ssl.crt"}, + {"keyfile", "ssl.key"}]} + ]}, + +Anonymous +--------- + +Allow any client to connect to the broker:: + + {anonymous, []} + +MySQL Plugin ------------ +Authenticate against MySQL database. Support we create a mqtt_user table:: + + CREATE TABLE `mqtt_user` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(100) DEFAULT NULL, + `password` varchar(100) DEFAULT NULL, + `salt` varchar(20) DEFAULT NULL, + `created` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `mqtt_username` (`username`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +Configure the 'authquery' and 'password_hash' in emqttd_plugin_mysql/etc/plugin.config:: + + [ + + {emqttd_plugin_mysql, [ + + ... + + %% select password only + {authquery, "select password from mqtt_user where username = '%u' limit 1"}, + + %% hash algorithm: md5, sha, sha256, pbkdf2? + {password_hash, sha256}, + + ... + + ]} + ]. + +Load the plugin:: + + ./bin/emqttd_ctl plugins load emqttd_plugin_mysql + + +PostgreSQL Plugin +----------------- + +Authenticate against PostgreSQL database. Create a mqtt_user table:: + + CREATE TABLE mqtt_user ( + id SERIAL primary key, + username character varying(100), + password character varying(100), + salt character varying(40) + ); + +Configure the 'authquery' and 'password_hash' in emqttd_plugin_pgsql/etc/plugin.config:: + + [ + + {emqttd_plugin_pgsql, [ + + ... + + %% select password only + {authquery, "select password from mqtt_user where username = '%u' limit 1"}, + + %% hash algorithm: md5, sha, sha256, pbkdf2? + {password_hash, sha256}, + + ... + + ]} + ]. + +Load the plugin:: + + ./bin/emqttd_ctl plugins load emqttd_plugin_pgsql + +Redis +----- + +Authenticate against Redis. Support we store mqtt user in an redis HASH, the key is "mqtt_user:". + +Configure 'authcmd' and 'password_hash' in emqttd_plugin_redis/etc/plugin.config:: + + [ + {emqttd_plugin_redis, [ + + ... + + %% HMGET mqtt_user:%u password + {authcmd, ["HGET", "mqtt_user:%u", "password"]}, + + %% Password hash algorithm: plain, md5, sha, sha256, pbkdf2? + {password_hash, sha256}, + + ... + + ]} + ]. + +Load the plugin:: + + ./bin/emqttd_ctl plugins load emqttd_plugin_redis + +--- +ACL +--- + +The ACL of emqttd broker is responsbile for authorizing MQTT clients to publish/subscribe topics. + +The ACL consists of a list rules that define:: + + Allow|Deny Who Publish|Subscribe Topics + +Access Control Module of emqttd broker will match the rules one by one:: + + --------- --------- --------- + Client -> | Rule1 | --nomatch--> | Rule2 | --nomatch--> | Rule3 | --> Default + --------- --------- --------- + | | | + match match match + \|/ \|/ \|/ + allow | deny allow | deny allow | deny + +Internal +-------- + +ACL of emqttd broker is implemented by an 'internal' module by default. + +Enable the 'internal' ACL module in etc/emqttd.config:: + + {acl, [ + %% Internal ACL module + {internal, [{file, "etc/acl.config"}, {nomatch, allow}]} + ]} + +The ACL rules of 'internal' module are defined in 'etc/acl.config' file:: + + %% Allow 'dashboard' to subscribe '$SYS/#' + {allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. + + %% Allow clients from localhost to subscribe any topics + {allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. + + %% Deny clients to subscribe '$SYS#' and '#' + {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. + + %% Allow all by default + {allow, all}. + +MySQL +----- + +ACL against MySQL database. The mqtt_acl table and default data:: + + CREATE TABLE `mqtt_acl` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `allow` int(1) DEFAULT NULL COMMENT '0: deny, 1: allow', + `ipaddr` varchar(60) DEFAULT NULL COMMENT 'IpAddress', + `username` varchar(100) DEFAULT NULL COMMENT 'Username', + `clientid` varchar(100) DEFAULT NULL COMMENT 'ClientId', + `access` int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub', + `topic` varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + INSERT INTO mqtt_acl (id, allow, ipaddr, username, clientid, access, topic) + VALUES + (1,1,NULL,'$all',NULL,2,'#'), + (2,0,NULL,'$all',NULL,1,'$SYS/#'), + (3,0,NULL,'$all',NULL,1,'eq #'), + (5,1,'127.0.0.1',NULL,NULL,2,'$SYS/#'), + (6,1,'127.0.0.1',NULL,NULL,2,'#'), + (7,1,NULL,'dashboard',NULL,1,'$SYS/#'); + +Configure 'aclquery' and 'acl_nomatch' in emqttd_plugin_mysql/etc/plugin.config:: + + [ + + {emqttd_plugin_mysql, [ + + ... + + %% comment this query, the acl will be disabled + {aclquery, "select * from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"}, + + %% If no rules matched, return... + {acl_nomatch, allow} + + ]} + ]. + +PostgreSQL +---------- + +ACL against PostgreSQL database. The mqtt_acl table and default data:: + + CREATE TABLE mqtt_acl ( + id SERIAL primary key, + allow integer, + ipaddr character varying(60), + username character varying(100), + clientid character varying(100), + access integer, + topic character varying(100) + ); + + INSERT INTO mqtt_acl (id, allow, ipaddr, username, clientid, access, topic) + VALUES + (1,1,NULL,'$all',NULL,2,'#'), + (2,0,NULL,'$all',NULL,1,'$SYS/#'), + (3,0,NULL,'$all',NULL,1,'eq #'), + (5,1,'127.0.0.1',NULL,NULL,2,'$SYS/#'), + (6,1,'127.0.0.1',NULL,NULL,2,'#'), + (7,1,NULL,'dashboard',NULL,1,'$SYS/#'); + +Configure 'aclquery' and 'acl_nomatch' in emqttd_plugin_pgsql/etc/plugin.config:: + + [ + + {emqttd_plugin_pgsql, [ + + ... + + %% Comment this query, the acl will be disabled. Notice: don't edit this query! + {aclquery, "select allow, ipaddr, username, clientid, access, topic from mqtt_acl + where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"}, + + %% If no rules matched, return... + {acl_nomatch, allow} + + ... + + ]} + ]. + +Redis +----- + +ACL against Redis. We store ACL rules for each MQTT client in Redis List by defualt. The key is "mqtt_acl:", the value is a list of "publish ", "subscribe " or "pubsub ". + +Configure 'aclcmd' and 'acl_nomatch' in emqttd_plugin_redis/etc/plugin.config:: + + [ + {emqttd_plugin_redis, [ + + ... + + %% SMEMBERS mqtt_acl:%u + {aclcmd, ["SMEMBERS", "mqtt_acl:%u"]}, + + %% If no rules matched, return... + {acl_nomatch, deny}, + + ... + + ]} + ]. + +---------------------- +MQTT Publish/Subscribe +---------------------- + +MQTT is a an extremely lightweight publish/subscribe messaging protocol desgined for IoT, M2M and Mobile applications. + +.. image:: _static/images/pubsub_concept.png + +Install and start the emqttd broker, and then any MQTT client could connect to the broker, subscribe topics and publish messages. + +MQTT Client Libraries: https://github.com/mqtt/mqtt.github.io/wiki/libraries + +For example, we use mosquitto_sub/pub commands:: + + mosquitto_sub -t topic -q 2 + mosquitto_pub -t topic -q 1 -m "Hello, MQTT!" + +MQTT V3.1.1 Protocol Specification: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html + +MQTT Listener of emqttd broker is configured in etc/emqttd.config:: + + {mqtt, 1883, [ + %% Size of acceptor pool + {acceptors, 16}, + + %% Maximum number of concurrent clients + {max_clients, 512}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Connection Options + {connopts, [ + %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec + %% {rate_limit, "100,10"} %% 100K burst, 10K rate + ]}, + + %% Socket Options + {sockopts, [ + %Set buffer if hight thoughtput + %{recbuf, 4096}, + %{sndbuf, 4096}, + %{buffer, 4096}, + %{nodelay, true}, + {backlog, 512} + ]} + ]}, + +MQTT(SSL) Listener, Default Port is 8883:: + + {mqtts, 8883, [ + %% Size of acceptor pool + {acceptors, 4}, + + %% Maximum number of concurrent clients + {max_clients, 512}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% SSL certificate and key files + {ssl, [{certfile, "etc/ssl/ssl.crt"}, + {keyfile, "etc/ssl/ssl.key"}]}, + + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} + ]}, + ---------------- HTTP Publish API ---------------- -emqttd support HTTP API to publish message from your application server to MQTT clients. For example:: +The emqttd broker provides a HTTP API to help application servers to publish messages to MQTT clients. -curl -v --basic -u user:passwd -d "qos=1&retain=0&topic=/a/b/c&message=hello from http..." -k http://localhost:8083/mqtt/publish +HTTP API: POST http://host:8083/mqtt/publish -HTTP API URL ------------- +Web servers such as PHP, Java, Python, NodeJS and Ruby on Rails could use HTTP POST to publish MQTT messages to the broker:: -HTTP POST http://host:8083/mqtt/publish + curl -v --basic -u user:passwd -d "qos=1&retain=0&topic=/a/b/c&message=hello from http..." -k http://localhost:8083/mqtt/publish -HTTP Parameters ---------------- +Parameters of the HTTP API: -Name | Description ---------|--------------- -client | ClientId -qos | QoS(0, 1, 2) -retain | Retain(0, 1) -topic | Topic -message | Message ++---------+----------------+ +| Name | Description | ++=========+================+ +| client | clientid | ++---------+----------------+ +| qos | QoS(0, 1, 2) | ++---------+----------------+ +| retain | Retain(0, 1) | ++---------+----------------+ +| topic | Topic | ++---------+----------------+ +| message | Payload | ++---------+----------------+ +.. NOTE:: The API use HTTP Basic Authentication. ------------------- MQTT Over WebSocket ------------------- -## Overview +Web browsers could connect to the emqttd broker directly by MQTT Over WebSocket. -emqttd 0.7.0 release support MQTT over WebSocket(SSL). ++-------------------------+----------------------------+ +| WebSocket URI: | ws(s)://host:8083/mqtt | ++-------------------------+----------------------------+ +| Sec-WebSocket-Protocol: | 'mqttv3.1' or 'mqttv3.1.1' | ++-------------------------+----------------------------+ -## URI +The Dashboard plugin provides a test page for WebSocket:: -``` -ws(s)://host:8083/mqtt -``` + http://127.0.0.1:18083/websocket.html -## Sec-WebSocket-Protocol +Listener of WebSocket and HTTP Publish API is configured in etc/emqttd.config:: -'mqttv3.1', 'mqttv3.1.1' - - ------------------------ -Trace Topic or Client ------------------------ + %% HTTP and WebSocket Listener + {http, 8083, [ + %% Size of acceptor pool + {acceptors, 4}, + %% Maximum number of concurrent clients + {max_clients, 64}, + %% Socket Access Control + {access, [{allow, all}]}, + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} + ]} ## Overview @@ -109,9 +545,9 @@ emqttd use lager:trace_file api and write trace log to file. -------------------- +----------- $SYS Topics -------------------- +----------- NOTICE: This is the design of 0.9.0 release @@ -308,4 +744,10 @@ processes/* | TODO sys_interval: 1 minute default +--------------------- +Trace Topic or Client +--------------------- +.. _emqttd_plugin_mysql: https://github.com/emqtt/emqttd_plugin_mysql +.. _emqttd_plugin_pgsql: https://github.com/emqtt/emqttd_plugin_pgsql +.. _emqttd_plugin_redis: https://github.com/emqtt/emqttd_plugin_redis diff --git a/docs/source/mqtt.rst b/docs/source/mqtt.rst index f0f1849dd..8cebe5b63 100644 --- a/docs/source/mqtt.rst +++ b/docs/source/mqtt.rst @@ -1,3 +1,101 @@ -============== + +.. _mqtt: + +TODO:... + +============= MQTT Protocol -============== +============= + +---------------------- +MQTT Protocol Tutorial +---------------------- + +MQTT.ORG docs: a publish/subscribe messaging protocol which is extremely lightweight, for IoT, M2M and mobile messaging + +.. image:: _static/images/pubsub_concept.png + +Publish/Subscribe Model +----------------------- + +.. image:: _static/images/pubsub_concept.png + + +MQTT Control Packets +-------------------- + +MQTT Packet Structure +--------------------- + +Compact: 1 byte header + +MQTT Packet Types +----------------- + +MQTT Packet Flags +----------------- + +MQTT Client Libraries +--------------------- + +MQTT Client Libraries +--------------------- + +mosquitto_pub mosquitto_sub co + +mqtt.org: + +TODO: LIST + +Maintained by emqtt.com: + +TODO: LIST + + +------------------------- +QoS0, QoS1, QoS2 Messages +------------------------- + +C->S Sequence... + + +---------------- +Retained Message +---------------- + +publish a retained message:: + + mosquitto_pub -t topic -m msg -q 1 -r + +subscribe to get the message:: + + mosquitto_sub -t topic -m msg -q 1 -r + + +------------ +Will Message +------------ + +------------ +Keep Alive +------------ + +---------------------------------- +Clean Session and Offline Messages +---------------------------------- + + + + +MQTT Client Libraries +--------------------- + +mosquitto_pub mosquitto_sub co + +mqtt.org: + +TODO: LIST + +Maintained by emqtt.com: + +TODO: LIST From 3866dbaffb1cd40c77f9e047a24c9db4429d69a9 Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 28 Feb 2016 13:50:21 +0800 Subject: [PATCH 34/69] guide --- docs/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index 7025497de..753bfb4af 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -47,6 +47,7 @@ Contents: config cluster bridge + guide commands plugins tune From f3607230d4fe6a16d03c9d921fc4385d18092e6b Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 28 Feb 2016 15:49:17 +0800 Subject: [PATCH 35/69] docs.emqtt.com --- docs/README | 7 + docs/source/guide.rst | 421 +++++++++++++++++++++--------------------- 2 files changed, 221 insertions(+), 207 deletions(-) create mode 100644 docs/README diff --git a/docs/README b/docs/README new file mode 100644 index 000000000..d7f4fb801 --- /dev/null +++ b/docs/README @@ -0,0 +1,7 @@ + +http://docs.emqtt.com/ + +or + +http://emqttd-docs.rtfd.org + diff --git a/docs/source/guide.rst b/docs/source/guide.rst index 4a09880d0..f5b54ec25 100644 --- a/docs/source/guide.rst +++ b/docs/source/guide.rst @@ -9,7 +9,7 @@ User Guide Authentication -------------- -The emqttd broker supports to authenticate MQTT client with ClientID, Username/Password, IpAddress and even HTTP Cookies. +The emqttd broker supports to authenticate MQTT clients with ClientID, Username/Password, IpAddress and even HTTP Cookies. The authentication is provided by a list of extended modules, or MySQL, PostgreSQL and Redis Plugins. @@ -41,7 +41,7 @@ Enable an authentication module in etc/emqttd.config:: {anonymous, []} ]}, -.. NOTE:: "%%" comments the line. +.. NOTE:: "%" comments the line. If we enable several modules in the same time, the authentication process:: @@ -119,8 +119,8 @@ Allow any client to connect to the broker:: {anonymous, []} -MySQL Plugin ------------- +MySQL +----- Authenticate against MySQL database. Support we create a mqtt_user table:: @@ -158,8 +158,8 @@ Load the plugin:: ./bin/emqttd_ctl plugins load emqttd_plugin_mysql -PostgreSQL Plugin ------------------ +PostgreSQL +---------- Authenticate against PostgreSQL database. Create a mqtt_user table:: @@ -196,7 +196,7 @@ Load the plugin:: Redis ----- -Authenticate against Redis. Support we store mqtt user in an redis HASH, the key is "mqtt_user:". +Authenticate against Redis. MQTT users could be stored in redis HASH, the key is "mqtt_user:". Configure 'authcmd' and 'password_hash' in emqttd_plugin_redis/etc/plugin.config:: @@ -243,7 +243,7 @@ Access Control Module of emqttd broker will match the rules one by one:: Internal -------- -ACL of emqttd broker is implemented by an 'internal' module by default. +The default ACL of emqttd broker is implemented by an 'internal' module. Enable the 'internal' ACL module in etc/emqttd.config:: @@ -355,7 +355,7 @@ Configure 'aclquery' and 'acl_nomatch' in emqttd_plugin_pgsql/etc/plugin.config: Redis ----- -ACL against Redis. We store ACL rules for each MQTT client in Redis List by defualt. The key is "mqtt_acl:", the value is a list of "publish ", "subscribe " or "pubsub ". +ACL against Redis. We store ACL rules for each MQTT client in a Redis List by defualt. The key is "mqtt_acl:", the value is a list of "publish ", "subscribe " or "pubsub ". Configure 'aclcmd' and 'acl_nomatch' in emqttd_plugin_redis/etc/plugin.config:: @@ -474,7 +474,7 @@ Parameters of the HTTP API: | message | Payload | +---------+----------------+ -.. NOTE:: The API use HTTP Basic Authentication. +.. NOTE:: The API uses HTTP Basic Authentication. ------------------- MQTT Over WebSocket @@ -509,245 +509,252 @@ Listener of WebSocket and HTTP Publish API is configured in etc/emqttd.config:: ]} ]} -## Overview - -emqttd could trace packets received/sent from/to specific client, or trace publish/subscribe to specific topic. - -emqttd use lager:trace_file api and write trace log to file. - - -## Trace Commands - -### Trace client - -``` -./bin/emqttd_ctl trace client "ClientId" "trace_clientid.log" -``` - -### Trace topic - -``` -./bin/emqttd_ctl trace topic "Topic" "trace_topic.log" -``` - -### Stop Trace - -``` -./bin/emqttd_ctl trace client "ClientId" off -./bin/emqttd_ctl trace topic "Topic" off -``` - -### Lookup Traces - -``` -./bin/emqttd_ctl trace list -``` - - - ----------- $SYS Topics ----------- -NOTICE: This is the design of 0.9.0 release +The emqttd broker periodically publishes internal status, MQTT statistics, metrics and client online/offline status to $SYS/# topics. -## Overview +For emqttd broker is clustered, the $SYS topic path is started with:: -For emqttd is clustered, $SYS Topics of broker is started with: + $SYS/brokers/${node}/ -``` -$SYS/brokers/${node} -``` +'${node}' is the erlang node name of emqttd broker. For example:: -${node} is erlang node of clustered brokers. For example: + $SYS/brokers/emqttd@127.0.0.1/version -``` -$SYS/brokers/emqttd@host1/version -$SYS/brokers/emqttd@host2/version -``` + $SYS/brokers/emqttd@host2/uptime -## Broker $SYS Topics +.. NOTE:: The broker only allows clients from localhost to subscribe $SYS topics by default. -Topic | Description --------------------------------|------------ -$SYS/brokers | Broker nodes -$SYS/brokers/${node}/version | Broker Version -$SYS/brokers/${node}/uptime | Broker Uptime -$SYS/brokers/${node}/datetime | Broker DateTime -$SYS/brokers/${node}/sysdescr | Broker Description -  -## Client $SYS Topics +Sys Interval of publishing broker $SYS messages, could be configured in etc/emqttd.config:: -Start with: $SYS/brokers/${node}/clients/ - -Topic | Payload(json) | Description -----------------------|---------------------|--------------- -${clientid}/connected | {ipaddress: "127.0.0.1", username: "test", session: false, version: 3, connack: 0, ts: 1432648482} | Publish when client connected -${clientid}/disconnected | {reason: "normal" | "keepalive_timeout" | "conn_closed"} - -Parameters of 'connected' Payload: - -``` -ipaddress: "127.0.0.1", -username: "test", -session: false, -protocol: 3, -connack: 0, -ts: 1432648482 -``` - -Parameters of 'disconnected' Payload: - -``` -reason: normal, -ts: 1432648486 -``` - -## Statistics $SYS Topics - -Start with '$SYS/brokers/${node}/stats/' - -### Client Stats - -Topic | Description --------------------------------------|------------ -clients/count | count of current connected clients -clients/max | max connected clients in the same time - -### Session Stats - -Topic | Description ------------------|------------ -sessions/count | count of current sessions -sessions/max | max number of sessions - -### Subscriber Stats - -Topic | Description -------------------|------------ -subscriptions/count | count of current subscriptions -subscriptions/max | max number of subscriptions - -### Topic Stats - -Topic | Description -------------------|------------ -topics/count | count of current topics -topics/max | max number of topics - -### Queue Stats - -Topic | Description -------------------|------------ -queues/count | count of current queues -queues/max | max number of queues + {broker, [ + %% System interval of + {sys_interval, 60}, -## Metrics $SYS Topics +Broker Version, Uptime and Description +--------------------------------------- -Start with '$SYS/brokers/${node}/metrics/' ++--------------------------------+-----------------------+ +| Topic | Description | ++================================+=======================+ +| $SYS/brokers | Broker nodes | ++--------------------------------+-----------------------+ +| $SYS/brokers/${node}/version | Broker Version | ++--------------------------------+-----------------------+ +| $SYS/brokers/${node}/uptime | Broker Uptime | ++--------------------------------+-----------------------+ +| $SYS/brokers/${node}/datetime | Broker DateTime | ++--------------------------------+-----------------------+ +| $SYS/brokers/${node}/sysdescr | Broker Description | ++--------------------------------+-----------------------+ -### Bytes sent and received +Online/Offline Status of MQTT Client +------------------------------------ -Topic | Description -------------------------------------|------------ -bytes/received | MQTT Bytes Received since broker started -bytes/sent | MQTT Bytes Sent since the broker started +The topic path is started with: $SYS/brokers/${node}/clients/ -### Packets sent and received - -Topic | Description --------------------------|------------ -packets/received | MQTT Packets received -packets/sent | MQTT Packets sent -packets/connect | MQTT CONNECT Packet received -packets/connack | MQTT CONNACK Packet sent -packets/publish/received | MQTT PUBLISH packets received -packets/publish/sent | MQTT PUBLISH packets sent -packets/subscribe | MQTT SUBSCRIBE Packets received -packets/suback | MQTT SUBACK packets sent -packets/unsubscribe | MQTT UNSUBSCRIBE Packets received -packets/unsuback | MQTT UNSUBACK Packets sent -packets/pingreq | MQTT PINGREQ packets received -packets/pingresp | MQTT PINGRESP Packets sent -packets/disconnect | MQTT DISCONNECT Packets received ++-------------------------+-------------------------------------------+------------------------------------+ +| Topic | Payload(JSON) | Description | ++=========================+===========================================+====================================+ +| ${clientid}/connected | {ipaddress: "127.0.0.1", username: "test",| Publish when a client connected | +| | session: false, version: 3, connack: 0, | | +| | ts: 1432648482} | | ++-------------------------+-------------------------------------------+------------------------------------+ +| ${clientid}/disconnected| {reason: "keepalive_timeout", | Publish when a client disconnected | +| | ts: 1432749431} | | ++-------------------------+-------------------------------------------+------------------------------------+ -### Messages sent and received +Parameters of 'connected' Payload:: -Topic | Description ----------------------------------------|------------------- -messages/received | Messages Received -messages/sent | Messages Sent -messages/retained | Messages Retained -messages/stored | TODO: Messages Stored -messages/dropped | Messages Dropped + ipaddress: "127.0.0.1", + username: "test", + session: false, + protocol: 3, + connack: 0, + ts: 1432648482 -## Alarm Topics +Parameters of 'disconnected' Payload:: -Start with '$SYS/brokers/${node}/alarms/' + reason: normal, + ts: 1432648486 -Topic | Description ------------------|------------------- -${alarmId}/alert | New Alarm -${alarmId}/clear | Clear Alarm +Broker Statistics +----------------- -## Log +Topic path started with: $SYS/brokers/${node}/stats/ -'$SYS/brokers/${node}/logs/${severity}' +Clients +....... -Severity | Description ------------|------------------- -debug | Debug Log -info | Info Log -notice | Notice Log -warning | Warning Log -error | Error Log -critical | Critical Log ++---------------------+---------------------------------------------+ +| Topic | Description | ++---------------------+---------------------------------------------+ +| clients/count | Count of current connected clients | ++---------------------+---------------------------------------------+ +| clients/max | Max number of cocurrent connected clients | ++---------------------+---------------------------------------------+ -## Sysmon +Sessions +........ -Start with '$SYS/brokers/${node}/sysmon/' ++---------------------+---------------------------------------------+ +| Topic | Description | ++---------------------+---------------------------------------------+ +| sessions/count | Count of current sessions | ++---------------------+---------------------------------------------+ +| sessions/max | Max number of sessions | ++---------------------+---------------------------------------------+ -Topic | Description ------------------|------------------- -long_gc | Long GC Warning -long_schedule | Long Schedule -large_heap | Large Heap Warning -busy_port | Busy Port Warning -busy_dist_port | Busy Dist Port +Subscriptions +............. -## Log ++---------------------+---------------------------------------------+ +| Topic | Description | ++---------------------+---------------------------------------------+ +| subscriptions/count | Count of current subscriptions | ++---------------------+---------------------------------------------+ +| subscriptions/max | Max number of subscriptions | ++---------------------+---------------------------------------------+ -'$SYS/brokers/${node}/log/${severity}' +Topics +...... -Severity | Description -------------|------------------- -debug | Debug -info | Info Log -notice | Notice Log -warning | Warning Log -error | Error Log -critical | Critical Log -alert | Alert Log ++---------------------+---------------------------------------------+ +| Topic | Description | ++---------------------+---------------------------------------------+ +| topics/count | Count of current topics | ++---------------------+---------------------------------------------+ +| topics/max | Max number of topics | ++---------------------+---------------------------------------------+ -## VM Load Topics +Broker Metrics +-------------- -Start with '$SYS/brokers/${node}/vm/' +Topic path started with: $SYS/brokers/${node}/metrics/ -Topic | Description ------------------|------------------- -memory/* | TODO -cpu/* | TODO -processes/* | TODO +Bytes Sent/Received +................... -## Sys Interval ++---------------------+---------------------------------------------+ +| Topic | Description | ++---------------------+---------------------------------------------+ +| bytes/received | MQTT Bytes Received since broker started | ++---------------------+---------------------------------------------+ +| bytes/sent | MQTT Bytes Sent since the broker started | ++---------------------+---------------------------------------------+ + +Packets Sent/Received +..................... + ++--------------------------+---------------------------------------------+ +| Topic | Description | ++--------------------------+---------------------------------------------+ +| packets/received | MQTT Packets received | ++--------------------------+---------------------------------------------+ +| packets/sent | MQTT Packets sent | ++--------------------------+---------------------------------------------+ +| packets/connect | MQTT CONNECT Packet received | ++--------------------------+---------------------------------------------+ +| packets/connack | MQTT CONNACK Packet sent | ++--------------------------+---------------------------------------------+ +| packets/publish/received | MQTT PUBLISH packets received | ++--------------------------+---------------------------------------------+ +| packets/publish/sent | MQTT PUBLISH packets sent | ++--------------------------+---------------------------------------------+ +| packets/subscribe | MQTT SUBSCRIBE Packets received | ++--------------------------+---------------------------------------------+ +| packets/suback | MQTT SUBACK packets sent | ++--------------------------+---------------------------------------------+ +| packets/unsubscribe | MQTT UNSUBSCRIBE Packets received | ++--------------------------+---------------------------------------------+ +| packets/unsuback | MQTT UNSUBACK Packets sent | ++--------------------------+---------------------------------------------+ +| packets/pingreq | MQTT PINGREQ packets received | ++--------------------------+---------------------------------------------+ +| packets/pingresp | MQTT PINGRESP Packets sent | ++--------------------------+---------------------------------------------+ +| packets/disconnect | MQTT DISCONNECT Packets received | ++--------------------------+---------------------------------------------+ + +Messages Sent/Received +...................... + ++--------------------------+---------------------------------------------+ +| Topic | Description | ++--------------------------+---------------------------------------------+ +| messages/received | Messages Received | ++--------------------------+---------------------------------------------+ +| messages/sent | Messages Sent | ++--------------------------+---------------------------------------------+ +| messages/retained | Messages Retained | ++--------------------------+---------------------------------------------+ +| messages/stored | TODO: Messages Stored | ++--------------------------+---------------------------------------------+ +| messages/dropped | Messages Dropped | ++--------------------------+---------------------------------------------+ + +Broker Alarms +------------- + +The topic path started with: $SYS/brokers/${node}/alarms/ + ++------------------+------------------+ +| Topic | Description | ++------------------+------------------+ +| ${alarmId}/alert | New Alarm | ++------------------+------------------+ +| ${alarmId}/clear | Clear Alarm | ++------------------+------------------+ + +Broker Sysmon +------------- + +Topic path: '$SYS/brokers/${node}/sysmon/' + ++------------------+--------------------+ +| Topic | Description | ++------------------+--------------------+ +| long_gc | Long GC Warning | ++------------------+--------------------+ +| long_schedule | Long Schedule | ++------------------+--------------------+ +| large_heap | Large Heap Warning | ++------------------+--------------------+ +| busy_port | Busy Port Warning | ++------------------+--------------------+ +| busy_dist_port | Busy Dist Port | ++------------------+--------------------+ -sys_interval: 1 minute default --------------------- Trace Topic or Client --------------------- +The emqttd broker supports to trace MQTT packets received/sent from/to a client, or trace MQTT messages published to a topic. + +Trace a client:: + + ./bin/emqttd_ctl trace client "clientid" "trace_clientid.log" + +Trace a topic:: + + ./bin/emqttd_ctl trace topic "topic" "trace_topic.log" + +Lookup Traces:: + + ./bin/emqttd_ctl trace list + +Stop a Trace:: + + ./bin/emqttd_ctl trace client "clientid" off + + ./bin/emqttd_ctl trace topic "topic" off + + .. _emqttd_plugin_mysql: https://github.com/emqtt/emqttd_plugin_mysql .. _emqttd_plugin_pgsql: https://github.com/emqtt/emqttd_plugin_pgsql .. _emqttd_plugin_redis: https://github.com/emqtt/emqttd_plugin_redis + From 225b2f1ef93c07a5fadf9a765c378e8d326fc262 Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 28 Feb 2016 15:58:32 +0800 Subject: [PATCH 36/69] sys --- docs/source/guide.rst | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/source/guide.rst b/docs/source/guide.rst index f5b54ec25..675af8857 100644 --- a/docs/source/guide.rst +++ b/docs/source/guide.rst @@ -527,10 +527,10 @@ For emqttd broker is clustered, the $SYS topic path is started with:: .. NOTE:: The broker only allows clients from localhost to subscribe $SYS topics by default. -Sys Interval of publishing broker $SYS messages, could be configured in etc/emqttd.config:: +Sys Interval of publishing $SYS messages, could be configured in etc/emqttd.config:: {broker, [ - %% System interval of + %% System interval of publishing broker $SYS messages {sys_interval, 60}, @@ -554,20 +554,20 @@ Broker Version, Uptime and Description Online/Offline Status of MQTT Client ------------------------------------ -The topic path is started with: $SYS/brokers/${node}/clients/ +The topic path started with: $SYS/brokers/${node}/clients/ -+-------------------------+-------------------------------------------+------------------------------------+ -| Topic | Payload(JSON) | Description | -+=========================+===========================================+====================================+ -| ${clientid}/connected | {ipaddress: "127.0.0.1", username: "test",| Publish when a client connected | -| | session: false, version: 3, connack: 0, | | -| | ts: 1432648482} | | -+-------------------------+-------------------------------------------+------------------------------------+ -| ${clientid}/disconnected| {reason: "keepalive_timeout", | Publish when a client disconnected | -| | ts: 1432749431} | | -+-------------------------+-------------------------------------------+------------------------------------+ ++--------------------------+--------------------------------------------+------------------------------------+ +| Topic | Payload(JSON) | Description | ++==========================+============================================+====================================+ +| ${clientid}/connected | {ipaddress: "127.0.0.1", username: "test", | Publish when a client connected | +| | session: false, version: 3, connack: 0, | | +| | ts: 1432648482} | | ++--------------------------+--------------------------------------------+------------------------------------+ +| ${clientid}/disconnected | {reason: "keepalive_timeout", | Publish when a client disconnected | +| | ts: 1432749431} | | ++--------------------------+--------------------------------------------+------------------------------------+ -Parameters of 'connected' Payload:: +Properties of 'connected' Payload:: ipaddress: "127.0.0.1", username: "test", @@ -576,7 +576,7 @@ Parameters of 'connected' Payload:: connack: 0, ts: 1432648482 -Parameters of 'disconnected' Payload:: +Properties of 'disconnected' Payload:: reason: normal, ts: 1432648486 @@ -699,7 +699,7 @@ Messages Sent/Received Broker Alarms ------------- -The topic path started with: $SYS/brokers/${node}/alarms/ +Topic path started with: $SYS/brokers/${node}/alarms/ +------------------+------------------+ | Topic | Description | @@ -712,7 +712,7 @@ The topic path started with: $SYS/brokers/${node}/alarms/ Broker Sysmon ------------- -Topic path: '$SYS/brokers/${node}/sysmon/' +Topic path started with: '$SYS/brokers/${node}/sysmon/' +------------------+--------------------+ | Topic | Description | From 3a87345f8efb1de01a0741c78551fc0114d20bc5 Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 28 Feb 2016 16:00:39 +0800 Subject: [PATCH 37/69] ACL Rules --- docs/source/guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/guide.rst b/docs/source/guide.rst index 675af8857..052821bd9 100644 --- a/docs/source/guide.rst +++ b/docs/source/guide.rst @@ -226,7 +226,7 @@ ACL The ACL of emqttd broker is responsbile for authorizing MQTT clients to publish/subscribe topics. -The ACL consists of a list rules that define:: +The ACL rules define:: Allow|Deny Who Publish|Subscribe Topics From 5575c7d7fe185b526d0bf76b1048633a346cf80a Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 28 Feb 2016 16:11:17 +0800 Subject: [PATCH 38/69] fix error datetime --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ccd767f..937b1629f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -725,7 +725,7 @@ Change: rename project from 'emqtt' to 'emqttd' Change: lager:debug to dump RECV/SENT packets -Feature: emqttd_bridg, emqttd_bridge_sup to support broker bridge +Feature: emqttd_bridge, emqttd_bridge_sup to support broker bridge Feature: emqtt_event to publish client connected/disconnected message to $SYS topics @@ -890,7 +890,7 @@ Bugfix: fix "mosquitto_sub -q 2 ......" bug Bugfix: fix keep alive bug -0.1.3 (2012-01-04) +0.1.3 (2013-01-04) ------------------- Feature: support QOS2 PUBREC, PUBREL,PUBCOMP messages From d1bb0e04fdc9264bf86b99f647f62e2b31ba15a1 Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 28 Feb 2016 23:49:10 +0800 Subject: [PATCH 39/69] fix syntax error --- docs/source/commands.rst | 12 ++++++------ docs/source/getstarted.rst | 6 +++--- docs/source/index.rst | 2 +- docs/source/install.rst | 4 ++-- src/emqttd_cli.erl | 8 ++++---- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/source/commands.rst b/docs/source/commands.rst index 91a29be72..3e2376a6c 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -596,11 +596,11 @@ Trace MQTT packets, messages(sent/received) by ClientId or Topic. +-----------------------------------+-----------------------------------+ | trace client | Trace a client | +-----------------------------------+-----------------------------------+ -| trace client off | Stop to trace the client | +| trace client off | Stop tracing the client | +-----------------------------------+-----------------------------------+ | trace topic | Trace a topic | +-----------------------------------+-----------------------------------+ -| trace topic off | Stop to trace the topic | +| trace topic off | Stop tracing the topic | +-----------------------------------+-----------------------------------+ trace client @@ -615,11 +615,11 @@ Start to trace a client:: trace client off --------------------------- -Stop to trace the client:: +Stop tracing the client:: $ ./bin/emqttd_ctl trace client clientid off - stop to trace client clientid successfully. + stop tracing client clientid successfully. trace topic ----------------------------- @@ -633,11 +633,11 @@ Start to trace a topic:: trace topic off ----------------------- -Stop to trace the topic:: +Stop tracing the topic:: $ ./bin/emqttd_ctl trace topic topic off - stop to trace topic topic successfully. + stop tracing topic topic successfully. trace list ---------- diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index b5fc505b7..5e4b8af16 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -11,7 +11,7 @@ Overview emqttd(Erlang MQTT Broker) is an open source MQTT broker written in Erlang/OTP. Erlang/OTP is a concurrent, fault-tolerant, soft-realtime and distributed programming platform. MQTT is an extremely lightweight publish/subscribe messaging protocol powering IoT, M2M and Mobile applications. -The emqttd project is aimed to implement a scalable, distributed, extensible open-source MQTT broker for IoT, M2M and Mobile applications that hope to handle ten millions of concurrent MQTT clients. +The emqttd project is aimed to implement a scalable, distributed, extensible open-source MQTT broker for IoT, M2M and Mobile applications that hope to handle millions of concurrent MQTT clients. Highlights of the emqttd broker: @@ -93,7 +93,7 @@ Web Dashboard A Web Dashboard will be loaded automatically when the emqttd broker is started successfully. -The Dashboard helps to check running status of the broker, monitor statistics and metrics of MQTT packets, query clients, sessions, topics and subscriptions. +The Dashboard helps check running status of the broker, monitor statistics and metrics of MQTT packets, query clients, sessions, topics and subscriptions. +------------------+---------------------------+ | Default Address | http://localhost:18083 | @@ -109,7 +109,7 @@ The Dashboard helps to check running status of the broker, monitor statistics an Modules and Plugins ------------------- -The Authentication and ACL mechanism is usually implemented by a Module or Plugin. +The Authentication and Authorization(ACL) are usually implemented by a Module or Plugin. Modules ------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 753bfb4af..fbfc37e6c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,7 +7,7 @@ emqttd - Erlang MQTT Broker =========================== -emqttd(Erlang MQTT Broker) is a massively scalable, clusterable MQTT V3.1/V3.1.1 broker written in Erlang/OTP. +emqttd(Erlang MQTT Broker) is a massively scalable and clusterable MQTT V3.1/V3.1.1 broker written in Erlang/OTP. emqttd is fully open source and licensed under the Apache Version 2.0. emqttd implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports WebSocket, STOMP, SockJS, CoAP and MQTT-SN in the same time. diff --git a/docs/source/install.rst b/docs/source/install.rst index cd5ac17b2..9e09ee1f4 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -120,7 +120,7 @@ We could install the broker on Mac OS X to develop and debug MQTT applications. Download Mac Package from: http://emqtt.io/downloads/macosx -Configure 'lager' log level in 'etc/emqttd.config', all MQTT messages recevied/sent will be printd on console: +Configure 'lager' log level in 'etc/emqttd.config', all MQTT messages recevied/sent will be printed on console: .. code:: erlang @@ -132,7 +132,7 @@ Configure 'lager' log level in 'etc/emqttd.config', all MQTT messages recevied/s ]} ]}, -The install and boot process on Mac is same to Linux. +The install and boot process on Mac are same to Linux. --------------------- Installing on Windows diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index 645556044..c1d1f68bf 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -405,9 +405,9 @@ trace(["topic", Topic, LogFile]) -> trace(_) -> ?USAGE([{"trace list", "query all traces"}, {"trace client ","trace client with ClientId"}, - {"trace client off", "stop to trace client"}, + {"trace client off", "stop tracing client"}, {"trace topic ", "trace topic with Topic"}, - {"trace topic off", "stop to trace Topic"}]). + {"trace topic off", "stop tracing Topic"}]). trace_on(Who, Name, LogFile) -> case emqttd_trace:start_trace({Who, iolist_to_binary(Name)}, LogFile) of @@ -420,9 +420,9 @@ trace_on(Who, Name, LogFile) -> trace_off(Who, Name) -> case emqttd_trace:stop_trace({Who, iolist_to_binary(Name)}) of ok -> - ?PRINT("stop to trace ~s ~s successfully.~n", [Who, Name]); + ?PRINT("stop tracing ~s ~s successfully.~n", [Who, Name]); {error, Error} -> - ?PRINT("stop to trace ~s ~s error: ~p.~n", [Who, Name, Error]) + ?PRINT("stop tracing ~s ~s error: ~p.~n", [Who, Name, Error]) end. %%-------------------------------------------------------------------- From 633237eae51bcf612e5e30df05ce849529604ddf Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 28 Feb 2016 23:53:35 +0800 Subject: [PATCH 40/69] million --- docs/source/cluster.rst | 2 +- docs/source/tune.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst index 1403fb0aa..5369e9381 100644 --- a/docs/source/cluster.rst +++ b/docs/source/cluster.rst @@ -247,6 +247,6 @@ The emqttd cluster does not support deployment across IDC, and the cluster will Consistent Hash and DHT ----------------------- -Consistent Hash and DHT are popular in the design of NoSQL databases. Cluster of emqttd broker could support 10 millions size of global routing table now. We could use the Consistent Hash or DHT to partition the routing table, and evolve the cluster to larger size. +Consistent Hash and DHT are popular in the design of NoSQL databases. Cluster of emqttd broker could support 10 million size of global routing table now. We could use the Consistent Hash or DHT to partition the routing table, and evolve the cluster to larger size. diff --git a/docs/source/tune.rst b/docs/source/tune.rst index c4152a532..78766fd63 100644 --- a/docs/source/tune.rst +++ b/docs/source/tune.rst @@ -13,7 +13,7 @@ Linux Kernel Tuning The system-wide limit on max opened file handles:: - # 2 millions system-wide + # 2 million system-wide sysctl -w fs.file-max=2097152 sysctl -w fs.nr_open=2097152 echo 2097152 > /proc/sys/fs/nr_open From aaedb167bf2fb2a2caf0c43a86cff2f32ea94eca Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 29 Feb 2016 10:54:24 +0800 Subject: [PATCH 41/69] fix README --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c4267105b..15211f621 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ ## Overview [![Build Status](https://travis-ci.org/emqtt/emqttd.svg?branch=master)](https://travis-ci.org/emqtt/emqttd) [![Join the chat at https://gitter.im/emqtt/emqttd](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/emqtt/emqttd?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -emqttd is a massively scalable and clusterable MQTT V3.1/V3.1.1 broker written in Erlang/OTP. emqttd support both MQTT V3.1/V3.1.1 protocol specification with extended features. +emqttd is a massively scalable and clusterable MQTT V3.1/V3.1.1 broker written in Erlang/OTP. + +emqttd is fully open source and licensed under the Apache Version 2.0. emqttd implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports WebSocket, STOMP, SockJS, CoAP and MQTT-SN in the same time. + +Latest release of the emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. emqttd requires Erlang R17+ to build. @@ -11,7 +15,7 @@ Follow us on Twitter: [@emqtt](https://twitter.com/emqtt) ## Goals -emqttd is aimed to provide a solid, enterprise grade, extensible open-source MQTT broker for IoT, M2M and Mobile applications that need to support ten millions of concurrent MQTT clients. +The emqttd project is aimed to implement a scalable, distributed, extensible open-source MQTT broker for IoT, M2M and Mobile applications that hope to handle millions of concurrent MQTT clients. * Easy to install * Massively scalable From d5b3d2d45e3a40bf210b0d98558917b456d39beb Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 29 Feb 2016 21:53:08 +0800 Subject: [PATCH 42/69] catalog --- docs/source/guide.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/guide.rst b/docs/source/guide.rst index 052821bd9..7f7667509 100644 --- a/docs/source/guide.rst +++ b/docs/source/guide.rst @@ -729,9 +729,9 @@ Topic path started with: '$SYS/brokers/${node}/sysmon/' +------------------+--------------------+ ---------------------- -Trace Topic or Client ---------------------- +----- +Trace +----- The emqttd broker supports to trace MQTT packets received/sent from/to a client, or trace MQTT messages published to a topic. From aca96279cbee7c840b27e6b44b015ca166475a65 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 29 Feb 2016 22:23:35 +0800 Subject: [PATCH 43/69] 0.17.0 - update README --- README.md | 90 +++++++++++++++++++++----------------- docs/source/getstarted.rst | 2 +- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 15211f621..a750bef17 100644 --- a/README.md +++ b/README.md @@ -27,50 +27,60 @@ The emqttd project is aimed to implement a scalable, distributed, extensible ope * Full MQTT V3.1/V3.1.1 protocol specification support * QoS0, QoS1, QoS2 Publish and Subscribe * Session Management and Offline Messages -* Retained Messages Support -* Last Will Message Support -* TCP/SSL Connection Support -* MQTT Over Websocket(SSL) Support -* HTTP Publish API Support -* [$SYS/brokers/#](https://github.com/emqtt/emqtt/wiki/$SYS-Topics-of-Broker) Support -* Client Authentication with clientId, ipaddress -* Client Authentication with username, password. -* Client ACL control with ipaddress, clientid, username. -* Cluster brokers on several servers. -* [Bridge](https://github.com/emqtt/emqttd/wiki/Bridge) brokers locally or remotely -* 500K+ concurrent clients connections per server +* Retained Message +* Last Will Message +* TCP/SSL Connection +* MQTT Over WebSocket(SSL) +* HTTP Publish API +* STOMP protocol +* STOMP over SockJS +* $SYS/# Topics +* ClientID Authentication +* IpAddress Authentication +* Username and Password Authentication +* Access control based on IpAddress, ClientID, Username +* Authentication with LDAP, Redis, MySQL, PostgreSQL +* Cluster brokers on several servers +* Bridge brokers locally or remotely +* mosquitto, RSMB bridge * Extensible architecture with Hooks, Modules and Plugins * Passed eclipse paho interoperability tests ## Modules -* [emqttd_auth_clientid](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with ClientIds -* [emqttd_auth_username](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with Username and Password -* [emqttd_auth_ldap](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with LDAP -* [emqttd_mod_presence](https://github.com/emqtt/emqttd/wiki/Presence) - Publish presence message to $SYS topics when client connected or disconnected -* emqttd_mod_autosub - Subscribe topics when client connected -* [emqttd_mod_rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) - Topics rewrite like HTTP rewrite module +Module | Description +-------------------------|------------------------------ +emqttd_auth_clientid | Authentication with ClientIds +emqttd_auth_username | Authentication with Username and Password +emqttd_auth_ldap | Authentication with LDAP +emqttd_mod_presence | Publish presence message to $SYS topics when client connected or disconnected +emqttd_mod_subscription | Subscribe topics when client connected +emqttd_mod_rewrite | Topic path rewrite like HTTP rewrite module ## Plugins -* [emqttd_plugin_template](https://github.com/emqtt/emqttd_plugin_template) - Plugin template and demo -* [emqttd_dashboard](https://github.com/emqtt/emqttd_dashboard) - Web Dashboard -* [emqttd_plugin_mysql](https://github.com/emqtt/emqttd_plugin_mysql) - Authentication with MySQL -* [emqttd_plugin_pgsql](https://github.com/emqtt/emqttd_plugin_pgsql) - Authentication with PostgreSQL -* [emqttd_plugin_kafka](https://github.com/emqtt/emqtt_kafka) - Publish MQTT Messages to Kafka -* [emqttd_plugin_redis](https://github.com/emqtt/emqttd_plugin_redis) - Redis Plugin -* [emqttd_plugin_mongo](https://github.com/emqtt/emqttd_plugin_mongo) - MongoDB Plugin -* [emqttd_stomp](https://github.com/emqtt/emqttd_stomp) - Stomp Protocol Plugin -* [emqttd_sockjs](https://github.com/emqtt/emqttd_sockjs) - SockJS(Stomp) Plugin -* [emqttd_recon](https://github.com/emqtt/emqttd_recon) - Recon Plugin +Plugin | Description +--------------------------------------------------------------------------|-------------------------------------- +[emqttd_plugin_template](https://github.com/emqtt/emqttd_plugin_template) | Plugin template and demo +[emqttd_dashboard](https://github.com/emqtt/emqttd_dashboard) | Web Dashboard +[emqttd_plugin_mysql](https://github.com/emqtt/emqttd_plugin_mysql) | MySQL Authentication/ACL Plugin +[emqttd_plugin_pgsql](https://github.com/emqtt/emqttd_plugin_pgsql) | PostgreSQL Authentication/ACL Plugin +[emqttd_plugin_redis](https://github.com/emqtt/emqttd_plugin_redis) | Redis Authentication/ACL Plugin +[emqttd_plugin_mongo](https://github.com/emqtt/emqttd_plugin_mongo) | MongoDB Authentication/ACL Plugin +[emqttd_stomp](https://github.com/emqtt/emqttd_stomp) | Stomp Protocol Plugin +[emqttd_sockjs](https://github.com/emqtt/emqttd_sockjs) | SockJS(Stomp) Plugin +[emqttd_recon](https://github.com/emqtt/emqttd_recon) | Recon Plugin ## Dashboard -The broker released a simple web dashboard in 0.10.0 version. +A Web Dashboard will be loaded when the emqttd broker started successfully. The Dashboard helps monitor broker's running status, statistics and metrics of MQTT packets. -Address: http://localhost:18083 -Username: admin -Password: public +Default Address | http://localhost:18083 +-------------------|------------------------ + Default Login | admin +-------------------|------------------------ + Default Password | public +-------------------|------------------------ ## Design @@ -78,12 +88,12 @@ Password: public ## QuickStart -Download binary packeges for linux, mac and freebsd from [http://emqtt.io/downloads](http://emqtt.io/downloads). +Download binary package for Linux, Mac and Freebsd from [http://emqtt.io/downloads](http://emqtt.io/downloads). -For example: +Installing on Ubuntu64, for example: ```sh -unzip emqttd-ubuntu64-0.12.0-beta-20151008.zip && cd emqttd +unzip emqttd-macosx-0.16.0-beta-20160216.zip && cd emqttd # start console ./bin/emqttd console @@ -98,21 +108,23 @@ unzip emqttd-ubuntu64-0.12.0-beta-20151008.zip && cd emqttd ./bin/emqttd stop ``` -Build from source: +Installing from source: ``` git clone https://github.com/emqtt/emqttd.git cd emqttd && make && make dist + +cd rel/emqttd && ./bin/emqttd console ``` -## GetStarted +## Documents -Read [emqtt wiki](https://github.com/emqtt/emqttd/wiki) for detailed installation and configuration guide. +Read Documents on [emqttd-docs.rtfd.org](http://emqttd-docs.rtfd.org) for installation and configuration guide. ## Benchmark -Benchmark 0.12.0-beta on a centos6 server with 8 Core, 32G memory from QingCloud: +Benchmark 0.12.0-beta on a CentOS6 server with 8 Core, 32G memory from QingCloud: 250K Connections, 250K Topics, 250K Subscriptions, 4K Qos1 Messages/Sec In, 20K Qos1 Messages/Sec Out, 8M+(bps) In, 40M+(bps) Out Traffic diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index 5e4b8af16..9acdfaf57 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -91,7 +91,7 @@ Installing from Source Web Dashboard ------------- -A Web Dashboard will be loaded automatically when the emqttd broker is started successfully. +A Web Dashboard will be loaded when the emqttd broker is started successfully. The Dashboard helps check running status of the broker, monitor statistics and metrics of MQTT packets, query clients, sessions, topics and subscriptions. From 46b2aa6bae23b1c98efc48b797c85b7304b6c567 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 29 Feb 2016 22:27:24 +0800 Subject: [PATCH 44/69] Dashboard --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a750bef17..3059c49c6 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ emqttd is a massively scalable and clusterable MQTT V3.1/V3.1.1 broker written i emqttd is fully open source and licensed under the Apache Version 2.0. emqttd implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports WebSocket, STOMP, SockJS, CoAP and MQTT-SN in the same time. -Latest release of the emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. - emqttd requires Erlang R17+ to build. Demo Server: tcp://t.emqtt.io:1883 @@ -73,14 +71,13 @@ Plugin | Desc ## Dashboard -A Web Dashboard will be loaded when the emqttd broker started successfully. The Dashboard helps monitor broker's running status, statistics and metrics of MQTT packets. +A Web Dashboard will be loaded when the emqttd broker started successfully. + +The Dashboard helps monitor broker's running status, statistics and metrics of MQTT packets. Default Address | http://localhost:18083 --------------------|------------------------ - Default Login | admin --------------------|------------------------ - Default Password | public --------------------|------------------------ +Default Login | admin +Default Password | public ## Design @@ -124,6 +121,8 @@ Read Documents on [emqttd-docs.rtfd.org](http://emqttd-docs.rtfd.org) for instal ## Benchmark +Latest release of the emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. + Benchmark 0.12.0-beta on a CentOS6 server with 8 Core, 32G memory from QingCloud: 250K Connections, 250K Topics, 250K Subscriptions, 4K Qos1 Messages/Sec In, 20K Qos1 Messages/Sec Out, 8M+(bps) In, 40M+(bps) Out Traffic From e8cfff66189072c113cadd96795802a070526e84 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 29 Feb 2016 22:30:19 +0800 Subject: [PATCH 45/69] fix format --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3059c49c6..fc088296f 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,8 @@ A Web Dashboard will be loaded when the emqttd broker started successfully. The Dashboard helps monitor broker's running status, statistics and metrics of MQTT packets. -Default Address | http://localhost:18083 -Default Login | admin -Default Password | public +Default Address: http://localhost:18083 +Default Login/Password: admin/public ## Design From 99e82ba3add17bd0a31fd819a1b5da3775efa8db Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 29 Feb 2016 22:36:39 +0800 Subject: [PATCH 46/69] fix format --- README.md | 5 +++-- docs/source/getstarted.rst | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fc088296f..ef607ae8d 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,8 @@ A Web Dashboard will be loaded when the emqttd broker started successfully. The Dashboard helps monitor broker's running status, statistics and metrics of MQTT packets. -Default Address: http://localhost:18083 +Default Address: http://localhost:18083 + Default Login/Password: admin/public ## Design @@ -120,7 +121,7 @@ Read Documents on [emqttd-docs.rtfd.org](http://emqttd-docs.rtfd.org) for instal ## Benchmark -Latest release of the emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. +Latest release of emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. Benchmark 0.12.0-beta on a CentOS6 server with 8 Core, 32G memory from QingCloud: diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index 9acdfaf57..a9c9c5e5c 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -184,7 +184,7 @@ For example, enable 'emqttd_plugin_pgsql' plugin:: One Million Connections ----------------------- -Latest release of the emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. +Latest release of emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. .. NOTE:: From 70d772a41d445260caff84f093d3de26d05a73d2 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 29 Feb 2016 23:37:15 +0800 Subject: [PATCH 47/69] at the same time --- README.md | 4 ++-- docs/source/config.rst | 2 +- docs/source/guide.rst | 2 +- docs/source/index.rst | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ef607ae8d..c38e8ec88 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ emqttd is a massively scalable and clusterable MQTT V3.1/V3.1.1 broker written in Erlang/OTP. -emqttd is fully open source and licensed under the Apache Version 2.0. emqttd implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports WebSocket, STOMP, SockJS, CoAP and MQTT-SN in the same time. +emqttd is fully open source and licensed under the Apache Version 2.0. emqttd implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports WebSocket, STOMP, SockJS, CoAP and MQTT-SN at the same time. emqttd requires Erlang R17+ to build. @@ -51,7 +51,7 @@ Module | Description emqttd_auth_clientid | Authentication with ClientIds emqttd_auth_username | Authentication with Username and Password emqttd_auth_ldap | Authentication with LDAP -emqttd_mod_presence | Publish presence message to $SYS topics when client connected or disconnected +emqttd_mod_presence | Publish presence message when client connected or disconnected emqttd_mod_subscription | Subscribe topics when client connected emqttd_mod_rewrite | Topic path rewrite like HTTP rewrite module diff --git a/docs/source/config.rst b/docs/source/config.rst index e2d6b070f..19d1bbf53 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -235,7 +235,7 @@ The broker provides Username, ClientId, LDAP and anonymous authentication module {anonymous, []} ]}, -The modules enabled in the same time compose an authentication chain: +The modules enabled at the same time compose an authentication chain: ---------------- ---------------- ------------- Client --> | Username | -ignore-> | ClientID | -ignore-> | Anonymous | diff --git a/docs/source/guide.rst b/docs/source/guide.rst index 7f7667509..26d550ae1 100644 --- a/docs/source/guide.rst +++ b/docs/source/guide.rst @@ -43,7 +43,7 @@ Enable an authentication module in etc/emqttd.config:: .. NOTE:: "%" comments the line. -If we enable several modules in the same time, the authentication process:: +If we enable several modules at the same time, the authentication process:: ---------------- ---------------- ------------- Client --> | Username | -ignore-> | ClientID | -ignore-> | Anonymous | diff --git a/docs/source/index.rst b/docs/source/index.rst index fbfc37e6c..f5dd4a15b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,7 +9,7 @@ emqttd - Erlang MQTT Broker emqttd(Erlang MQTT Broker) is a massively scalable and clusterable MQTT V3.1/V3.1.1 broker written in Erlang/OTP. -emqttd is fully open source and licensed under the Apache Version 2.0. emqttd implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports WebSocket, STOMP, SockJS, CoAP and MQTT-SN in the same time. +emqttd is fully open source and licensed under the Apache Version 2.0. emqttd implements both MQTT V3.1 and V3.1.1 protocol specifications, and supports WebSocket, STOMP, SockJS, CoAP and MQTT-SN at the same time. Latest release of the emqttd broker is scaling to 1.3 million MQTT connections on a 12 Core, 32G CentOS server. From 616b6df0205f9e69264997815cb49f57cdc3573c Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 1 Mar 2016 13:40:01 +0800 Subject: [PATCH 48/69] OTP --- docs/source/cluster.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst index 5369e9381..72a4a1bac 100644 --- a/docs/source/cluster.rst +++ b/docs/source/cluster.rst @@ -6,7 +6,7 @@ Clustering ========== ---------------------- -Distributed Erlang/OPT +Distributed Erlang/OTP ---------------------- Erlang/OTP is a concurrent, fault-tolerant, distributed programming platform. A distributed Erlang/OTP system consists of a number of Erlang runtime systems called 'node'. Nodes connect to each other with TCP/IP sockets and communite by Message Passing. From cf4dfe632fb6fec7f6637208cc65b2fc9bc6eb96 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 1 Mar 2016 18:59:12 +0800 Subject: [PATCH 49/69] Issue #460 - Timer Tick at interval of (Keepalive * 1.5)/2 --- src/emqttd_protocol.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index 85c9fb0bc..536474ce4 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -307,7 +307,7 @@ send_willmsg(ClientId, WillMsg) -> start_keepalive(0) -> ignore; start_keepalive(Sec) when Sec > 0 -> - self() ! {keepalive, start, round(Sec * 1.2)}. + self() ! {keepalive, start, round(Sec * 0.75)}. %%-------------------------------------------------------------------- %% Validate Packets From 652f5f57678acc16d3f4aebf113d20b533825a0a Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 3 Mar 2016 11:12:08 +0800 Subject: [PATCH 50/69] fix issue #450. Print a hint if the auth_username module is not enabled --- src/emqttd_auth_username.erl | 25 +++++++++++++++++++++---- src/emqttd_cli.erl | 6 +++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/emqttd_auth_username.erl b/src/emqttd_auth_username.erl index 395233a19..a898f6ea8 100644 --- a/src/emqttd_auth_username.erl +++ b/src/emqttd_auth_username.erl @@ -26,8 +26,9 @@ -behaviour(emqttd_auth_mod). --export([add_user/2, remove_user/1, - lookup_user/1, all_users/0]). +-export([is_enabled/0]). + +-export([add_user/2, remove_user/1, lookup_user/1, all_users/0]). %% emqttd_auth callbacks -export([init/1, check/3, description/0]). @@ -41,19 +42,35 @@ %%-------------------------------------------------------------------- cli(["add", Username, Password]) -> - ?PRINT("~p~n", [add_user(iolist_to_binary(Username), iolist_to_binary(Password))]); + if_enabled(fun() -> + ?PRINT("~p~n", [add_user(iolist_to_binary(Username), iolist_to_binary(Password))]) + end); cli(["del", Username]) -> - ?PRINT("~p~n", [remove_user(iolist_to_binary(Username))]); + if_enabled(fun() -> + ?PRINT("~p~n", [remove_user(iolist_to_binary(Username))]) + end); cli(_) -> ?USAGE([{"users add ", "Add User"}, {"users del ", "Delete User"}]). +if_enabled(Fun) -> + case is_enabled() of + true -> Fun(); + false -> hint() + end. + +hint() -> + ?PRINT_MSG("Please enable '{username, []}' authentication in etc/emqttd.config first.~n"). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- +is_enabled() -> + lists:member(?AUTH_USERNAME_TAB, mnesia:system_info(tables)). + %% @doc Add User -spec add_user(binary(), binary()) -> ok | {error, any()}. add_user(Username, Password) -> diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index c1d1f68bf..11a0b23fd 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -28,7 +28,7 @@ -export([load/0]). --export([status/1, broker/1, cluster/1, bridges/1, +-export([status/1, broker/1, cluster/1, users/1, bridges/1, clients/1, sessions/1, topics/1, subscriptions/1, plugins/1, listeners/1, vm/1, mnesia/1, trace/1]). @@ -141,6 +141,10 @@ cluster(_) -> {"cluster remove ","Remove the node from cluster"}, {"cluster status", "Cluster status"}]). +%%-------------------------------------------------------------------- +%% @doc Users usage +users(Args) -> emqttd_auth_username:cli(Args). + %%-------------------------------------------------------------------- %% @doc Query clients clients(["list"]) -> From 52adc66bda2cac00b828921824cbb0be0d4238a5 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:15:50 +0800 Subject: [PATCH 51/69] move start_listeners/0, stop_listeners/0 to emqttd_app.erl, add publish/subscribe APIs --- src/emqttd.erl | 99 +++++++++++++++++++++----------------------------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/src/emqttd.erl b/src/emqttd.erl index 69cf92482..9a740e1cd 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -16,19 +16,20 @@ -module(emqttd). --export([start/0, env/1, env/2, start_listeners/0, stop_listeners/0, - load_all_mods/0, is_mod_enabled/1, is_running/1]). +-include("emqttd.hrl"). --define(MQTT_SOCKOPTS, [ - binary, - {packet, raw}, - {reuseaddr, true}, - {backlog, 512}, - {nodelay, true}]). +-include("emqttd_protocol.hrl"). + +-export([start/0, env/1, env/2, is_running/1]). + +-export([create/2, publish/1, subscribe/1, subscribe/3, + unsubscribe/1, unsubscribe/3]). -define(APP, ?MODULE). --type listener() :: {atom(), inet:port_number(), [esockd:option()]}. +%%-------------------------------------------------------------------- +%% Bootstrap, environment, is_running... +%%-------------------------------------------------------------------- %% @doc Start emqttd application. -spec start() -> ok | {error, any()}. @@ -42,55 +43,6 @@ env(Group) -> application:get_env(?APP, Group, []). -spec env(Group :: atom(), Name :: atom()) -> undefined | any(). env(Group, Name) -> proplists:get_value(Name, env(Group)). -%% @doc Start Listeners of the broker. --spec start_listeners() -> any(). -start_listeners() -> lists:foreach(fun start_listener/1, env(listeners)). - -%% Start mqtt listener --spec start_listener(listener()) -> any(). -start_listener({mqtt, Port, Opts}) -> start_listener(mqtt, Port, Opts); - -%% Start mqtt(SSL) listener -start_listener({mqtts, Port, Opts}) -> start_listener(mqtts, Port, Opts); - -%% Start http listener -start_listener({http, Port, Opts}) -> - mochiweb:start_http(Port, Opts, {emqttd_http, handle_request, []}); - -%% Start https listener -start_listener({https, Port, Opts}) -> - mochiweb:start_http(Port, Opts, {emqttd_http, handle_request, []}). - -start_listener(Protocol, Port, Opts) -> - MFArgs = {emqttd_client, start_link, [env(mqtt)]}, - esockd:open(Protocol, Port, merge_sockopts(Opts), MFArgs). - -merge_sockopts(Options) -> - SockOpts = emqttd_opts:merge(?MQTT_SOCKOPTS, - proplists:get_value(sockopts, Options, [])), - emqttd_opts:merge(Options, [{sockopts, SockOpts}]). - -%% @doc Stop Listeners -stop_listeners() -> lists:foreach(fun stop_listener/1, env(listeners)). - -stop_listener({Protocol, Port, _Opts}) -> esockd:close({Protocol, Port}). - -%% @doc load all modules -load_all_mods() -> - lists:foreach(fun load_mod/1, env(modules)). - -load_mod({Name, Opts}) -> - Mod = list_to_atom("emqttd_mod_" ++ atom_to_list(Name)), - case catch Mod:load(Opts) of - ok -> lager:info("Load module ~s successfully", [Name]); - {error, Error} -> lager:error("Load module ~s error: ~p", [Name, Error]); - {'EXIT', Reason} -> lager:error("Load module ~s error: ~p", [Name, Reason]) - end. - -%% @doc Is module enabled? --spec is_mod_enabled(Name :: atom()) -> boolean(). -is_mod_enabled(Name) -> env(modules, Name) =/= undefined. - %% @doc Is running? -spec is_running(node()) -> boolean(). is_running(Node) -> @@ -100,3 +52,34 @@ is_running(Node) -> Pid when is_pid(Pid) -> true end. +%%-------------------------------------------------------------------- +%% PubSub APIs that wrap emqttd_server, emqttd_pubsub +%%-------------------------------------------------------------------- + +%% @doc Create a Topic +create(topic, Topic) when is_binary(Topic) -> + emqttd_pubsub:create_topic(Topic). + +%% @doc Publish MQTT Message +-spec publish(mqtt_message()) -> ok. +publish(Msg) when is_record(Msg, mqtt_message) -> + emqttd_server:publish(Msg). + +%% @doc Subscribe +-spec subscribe(binary()) -> ok. +subscribe(Topic) when is_binary(Topic) -> + emqttd_server:subscribe(Topic). + +-spec subscribe(binary(), binary(), mqtt_qos()) -> {ok, mqtt_qos()}. +subscribe(ClientId, Topic, Qos) -> + emqttd_server:subscribe(ClientId, Topic, Qos). + +%% @doc Unsubscribe +-spec unsubscribe(binary()) -> ok. +unsubscribe(Topic) when is_binary(Topic) -> + emqttd_server:unsubscribe(Topic). + +-spec unsubscribe(binary(), binary(), mqtt_qos()) -> ok. +unsubscribe(ClientId, Topic, Qos) -> + emqttd_server:unsubscribe(ClientId, Topic, Qos). + From e7c1d85d9110ef1549dedc2deee9368b42c2a5e0 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:16:35 +0800 Subject: [PATCH 52/69] add mqtt_route record --- include/emqttd.hrl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index e0349d57f..b68b583f0 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -47,7 +47,7 @@ %%-------------------------------------------------------------------- -record(mqtt_topic, { topic :: binary(), - node :: node() + flags :: [retained | static] }). -type mqtt_topic() :: #mqtt_topic{}. @@ -63,6 +63,16 @@ -type mqtt_subscription() :: #mqtt_subscription{}. +%%-------------------------------------------------------------------- +%% MQTT Route +%%-------------------------------------------------------------------- +-record(mqtt_route, { + topic :: binary(), + node :: node() +}). + +-type mqtt_route() :: #mqtt_route{}. + %%-------------------------------------------------------------------- %% MQTT Client %%-------------------------------------------------------------------- From 34493b6fec4995e04580e0032d35ec6b70a626c6 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:17:04 +0800 Subject: [PATCH 53/69] update PRINT_CMD --- include/emqttd_cli.hrl | 2 +- include/emqttd_internal.hrl | 2 +- src/emqttd_pubsub_helper.erl | 76 ----- ...mqttd_pubsub.erl => emqttd_pubsub_old.erl} | 0 src/emqttd_router.erl | 281 ------------------ 5 files changed, 2 insertions(+), 359 deletions(-) delete mode 100644 src/emqttd_pubsub_helper.erl rename src/{emqttd_pubsub.erl => emqttd_pubsub_old.erl} (100%) delete mode 100644 src/emqttd_router.erl diff --git a/include/emqttd_cli.hrl b/include/emqttd_cli.hrl index 620a771e2..f8245f9a8 100644 --- a/include/emqttd_cli.hrl +++ b/include/emqttd_cli.hrl @@ -18,7 +18,7 @@ -define(PRINT(Format, Args), io:format(Format, Args)). --define(PRINT_CMD(Cmd, Descr), io:format("~-40s#~s~n", [Cmd, Descr])). +-define(PRINT_CMD(Cmd, Descr), io:format("~-48s# ~s~n", [Cmd, Descr])). -define(USAGE(CmdList), [?PRINT_CMD(Cmd, Descr) || {Cmd, Descr} <- CmdList]). diff --git a/include/emqttd_internal.hrl b/include/emqttd_internal.hrl index 5b4f6189d..56522a62f 100644 --- a/include/emqttd_internal.hrl +++ b/include/emqttd_internal.hrl @@ -52,7 +52,7 @@ {noreply, State} end)). --define(IF(Cond, TrueFun,FalseFun), +-define(IF(Cond, TrueFun, FalseFun), (case (Cond) of true -> (TrueFun); false-> (FalseFun) diff --git a/src/emqttd_pubsub_helper.erl b/src/emqttd_pubsub_helper.erl deleted file mode 100644 index 6e697b060..000000000 --- a/src/emqttd_pubsub_helper.erl +++ /dev/null @@ -1,76 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - -%%% @doc PubSub Helper. --module(emqttd_pubsub_helper). - --behaviour(gen_server). - --include("emqttd.hrl"). - --include("emqttd_internal.hrl"). - -%% API Function Exports --export([start_link/1]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {statsfun}). - --define(SERVER, ?MODULE). - -%% @doc Start PubSub Helper. --spec start_link(fun()) -> {ok, pid()} | ignore | {error, any()}. -start_link(StatsFun) -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [StatsFun], []). - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([StatsFun]) -> - mnesia:subscribe(system), - {ok, #state{statsfun = StatsFun}}. - -handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). - -handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). - -handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> - %% TODO: mnesia master? - Pattern = #mqtt_topic{_ = '_', node = Node}, - F = fun() -> - [mnesia:delete_object(topic, R, write) || - R <- mnesia:match_object(topic, Pattern, write)] - end, - mnesia:transaction(F), noreply(State); - -handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). - -terminate(_Reason, _State) -> - mnesia:unsubscribe(system). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -noreply(State = #state{statsfun = StatsFun}) -> - StatsFun(topic), {noreply, State}. - diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub_old.erl similarity index 100% rename from src/emqttd_pubsub.erl rename to src/emqttd_pubsub_old.erl diff --git a/src/emqttd_router.erl b/src/emqttd_router.erl deleted file mode 100644 index 5fc025d8d..000000000 --- a/src/emqttd_router.erl +++ /dev/null @@ -1,281 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - -%% @doc -%% The Message Router on Local Node. -%% @end --module(emqttd_router). - --behaviour(gen_server2). - --include("emqttd.hrl"). - --include("emqttd_protocol.hrl"). - --include("emqttd_internal.hrl"). - --export([start_link/4]). - -%% Route API --export([route/2]). - -%% Route Admin API --export([add_route/2, lookup_routes/1, has_route/1, delete_route/2]). - -%% Batch API --export([add_routes/2, delete_routes/2]). - -%% For Test --export([stop/1]). - -%% gen_server Callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(aging, {topics, time, tref}). - --record(state, {pool, id, aging :: #aging{}, statsfun}). - -%% @doc Start a router. --spec start_link(atom(), pos_integer(), fun((atom()) -> ok), list()) -> {ok, pid()} | {error, any()}. -start_link(Pool, Id, StatsFun, Env) -> - gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, - ?MODULE, [Pool, Id, StatsFun, Env], []). - -%% @doc Route Message on this node. --spec route(emqttd_topic:topic(), mqtt_message()) -> any(). -route(Queue = <<"$Q/", _Q>>, Msg) -> - case lookup_routes(Queue) of - [] -> - dropped(Queue); - [SubPid] -> - SubPid ! {dispatch, Queue, Msg}; - Routes -> - Idx = crypto:rand_uniform(1, length(Routes) + 1), - SubPid = lists:nth(Idx, Routes), - SubPid ! {dispatch, Queue, Msg} - end; - -route(Topic, Msg) -> - case lookup_routes(Topic) of - [] -> - dropped(Topic); - [SubPid] -> - SubPid ! {dispatch, Topic, Msg}; - Routes -> - lists:foreach(fun(SubPid) -> - SubPid ! {dispatch, Topic, Msg} - end, Routes) - end. - -%% @private -%% @doc Ingore $SYS Messages. -dropped(<<"$SYS/", _/binary>>) -> - ok; -dropped(_Topic) -> - emqttd_metrics:inc('messages/dropped'). - -%% @doc Has Route? --spec has_route(emqttd_topic:topic()) -> boolean(). -has_route(Topic) when is_binary(Topic) -> - ets:member(route, Topic). - -%% @doc Lookup Routes --spec lookup_routes(emqttd_topic:topic()) -> list(pid()). -lookup_routes(Topic) when is_binary(Topic) -> - case ets:member(route, Topic) of - true -> - try ets:lookup_element(route, Topic, 2) catch error:badarg -> [] end; - false -> - [] - end. - -%% @doc Add Route --spec add_route(emqttd_topic:topic(), pid()) -> ok. -add_route(Topic, Pid) when is_pid(Pid) -> - call(pick(Topic), {add_route, Topic, Pid}). - -%% @doc Add Routes --spec add_routes(list(emqttd_topic:topic()), pid()) -> ok. -add_routes([], _Pid) -> - ok; -add_routes([Topic], Pid) -> - add_route(Topic, Pid); - -add_routes(Topics, Pid) -> - lists:foreach(fun({Router, Slice}) -> - call(Router, {add_routes, Slice, Pid}) - end, slice(Topics)). - -%% @doc Delete Route --spec delete_route(emqttd_topic:topic(), pid()) -> ok. -delete_route(Topic, Pid) -> - cast(pick(Topic), {delete_route, Topic, Pid}). - -%% @doc Delete Routes --spec delete_routes(list(emqttd_topic:topic()), pid()) -> ok. -delete_routes([Topic], Pid) -> - delete_route(Topic, Pid); - -delete_routes(Topics, Pid) -> - lists:foreach(fun({Router, Slice}) -> - cast(Router, {delete_routes, Slice, Pid}) - end, slice(Topics)). - -%% @private Slice topics. -slice(Topics) -> - dict:to_list(lists:foldl(fun(Topic, Dict) -> - dict:append(pick(Topic), Topic, Dict) - end, dict:new(), Topics)). - -%% @private Pick a router. -pick(Topic) -> - gproc_pool:pick_worker(router, Topic). - -%% @doc For unit test. -stop(Id) when is_integer(Id) -> - gen_server2:call(?PROC_NAME(?MODULE, Id), stop); -stop(Pid) when is_pid(Pid) -> - gen_server2:call(Pid, stop). - -call(Router, Request) -> - gen_server2:call(Router, Request, infinity). - -cast(Router, Msg) -> - gen_server2:cast(Router, Msg). - -init([Pool, Id, StatsFun, Opts]) -> - - emqttd_time:seed(), - - ?GPROC_POOL(join, Pool, Id), - - Aging = init_aging(Opts), - - {ok, #state{pool = Pool, id = Id, aging = Aging, statsfun = StatsFun}}. - -%% Init Aging -init_aging(Opts) -> - AgingSecs = proplists:get_value(route_aging, Opts, 5), - {ok, AgingTref} = start_tick(AgingSecs + random:uniform(AgingSecs)), - #aging{topics = dict:new(), time = AgingSecs, tref = AgingTref}. - -start_tick(Secs) -> - timer:send_interval(timer:seconds(Secs), {clean, aged}). - -handle_call(stop, _From, State) -> - {stop, normal, ok, State}; - -handle_call({add_route, Topic, Pid}, _From, State) -> - ets:insert(route, {Topic, Pid}), - {reply, ok, setstats(State)}; - -handle_call({add_routes, Topics, Pid}, _From, State) -> - ets:insert(route, [{Topic, Pid} || Topic <- Topics]), - {reply, ok, setstats(State)}; - -handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). - -handle_cast({delete_route, Topic, Pid}, State = #state{aging = Aging}) -> - Aging1 = delete_route_(Topic, Pid, Aging), - {noreply, setstats(State#state{aging = Aging1})}; - -handle_cast({delete_routes, Topics, Pid}, State) -> - Aging1 = - lists:foldl(fun(Topic, Aging) -> - delete_route_(Topic, Pid, Aging) - end, State#state.aging, Topics), - {noreply, setstats(State#state{aging = Aging1})}; - -handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). - -handle_info({clean, aged}, State = #state{aging = Aging}) -> - - #aging{topics = Dict, time = Time} = Aging, - - ByTime = emqttd_time:now_to_secs() - Time, - - Dict1 = try_clean(ByTime, dict:to_list(Dict)), - - Aging1 = Aging#aging{topics = dict:from_list(Dict1)}, - - {noreply, State#state{aging = Aging1}, hibernate}; - -handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). - -terminate(_Reason, #state{pool = Pool, id = Id, aging = #aging{tref = TRef}}) -> - timer:cancel(TRef), - ?GPROC_POOL(leave, Pool, Id). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -delete_route_(Topic, Pid, Aging) -> - ets:delete_object(route, {Topic, Pid}), - case has_route(Topic) of - false -> store_aged(Topic, Aging); - true -> Aging - end. - -try_clean(ByTime, List) -> - try_clean(ByTime, List, []). - -try_clean(_ByTime, [], Acc) -> - Acc; - -try_clean(ByTime, [{Topic, TS} | Left], Acc) -> - case has_route(Topic) of - false -> - try_clean2(ByTime, {Topic, TS}, Left, Acc); - true -> - try_clean(ByTime, Left, Acc) - end. - -try_clean2(ByTime, {Topic, TS}, Left, Acc) when TS > ByTime -> - try_clean(ByTime, Left, [{Topic, TS} | Acc]); - -try_clean2(ByTime, {Topic, _TS}, Left, Acc) -> - TopicR = #mqtt_topic{topic = Topic, node = node()}, - case mnesia:transaction(fun try_remove_topic/1, [TopicR]) of - {atomic, _} -> ok; - {aborted, Error} -> lager:error("Clean Topic '~s' Error: ~p", [Topic, Error]) - end, - try_clean(ByTime, Left, Acc). - -try_remove_topic(TopicR = #mqtt_topic{topic = Topic}) -> - %% Lock topic first - case mnesia:wread({topic, Topic}) of - [] -> ok; - [TopicR] -> %% Remove topic and trie - delete_topic(TopicR), - emqttd_trie:delete(Topic); - _More -> %% Remove topic only - delete_topic(TopicR) - end. - -delete_topic(TopicR) -> - mnesia:delete_object(topic, TopicR, write). - -store_aged(Topic, Aging = #aging{topics = Dict}) -> - Now = emqttd_time:now_to_secs(), - Aging#aging{topics = dict:store(Topic, Now, Dict)}. - -setstats(State = #state{statsfun = StatsFun}) -> - StatsFun(route), State. - From ef8cff4cac4f92127f4599f181103fa5c8e4259c Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:19:03 +0800 Subject: [PATCH 54/69] emqttd:publish/1 --- src/emqttd_alarm.erl | 4 +- src/emqttd_metrics.erl | 4 +- src/emqttd_session.erl | 91 +++++++++++++++++++----------------------- src/emqttd_stats.erl | 19 +++++---- src/emqttd_sysmon.erl | 2 +- 5 files changed, 56 insertions(+), 64 deletions(-) diff --git a/src/emqttd_alarm.erl b/src/emqttd_alarm.erl index 8ef08dec0..1b4924463 100644 --- a/src/emqttd_alarm.erl +++ b/src/emqttd_alarm.erl @@ -91,12 +91,12 @@ handle_event({set_alarm, Alarm = #mqtt_alarm{id = AlarmId, {title, iolist_to_binary(Title)}, {summary, iolist_to_binary(Summary)}, {ts, emqttd_time:now_to_secs(Timestamp)}]), - emqttd_pubsub:publish(alarm_msg(alert, AlarmId, Json)), + emqttd:publish(alarm_msg(alert, AlarmId, Json)), {ok, [Alarm#mqtt_alarm{timestamp = Timestamp} | Alarms]}; handle_event({clear_alarm, AlarmId}, Alarms) -> Json = mochijson2:encode([{id, AlarmId}, {ts, emqttd_time:now_to_secs()}]), - emqttd_pubsub:publish(alarm_msg(clear, AlarmId, Json)), + emqttd:publish(alarm_msg(clear, AlarmId, Json)), {ok, lists:keydelete(AlarmId, 2, Alarms), hibernate}; handle_event(_, Alarms)-> diff --git a/src/emqttd_metrics.erl b/src/emqttd_metrics.erl index 6b6c6b7a8..c2e57ee8b 100644 --- a/src/emqttd_metrics.erl +++ b/src/emqttd_metrics.erl @@ -243,7 +243,7 @@ init([]) -> % Init metrics [create_metric(Metric) || Metric <- Metrics], % $SYS Topics for metrics - [ok = emqttd_pubsub:create(topic, metric_topic(Topic)) || {_, Topic} <- Metrics], + [ok = emqttd:create(topic, metric_topic(Topic)) || {_, Topic} <- Metrics], % Tick to publish metrics {ok, #state{tick_tref = emqttd_broker:start_tick(tick)}, hibernate}. @@ -273,7 +273,7 @@ code_change(_OldVsn, State, _Extra) -> publish(Metric, Val) -> Msg = emqttd_message:make(metrics, metric_topic(Metric), bin(Val)), - emqttd_pubsub:publish(emqttd_message:set_flag(sys, Msg)). + emqttd:publish(emqttd_message:set_flag(sys, Msg)). create_metric({gauge, Name}) -> ets:insert(?METRIC_TAB, {{Name, 0}, 0}); diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index f9779509f..12b3e680d 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -154,6 +154,10 @@ info(SessPid) -> destroy(SessPid, ClientId) -> gen_server2:cast(SessPid, {destroy, ClientId}). +%%-------------------------------------------------------------------- +%% PubSub +%%-------------------------------------------------------------------- + %% @doc Subscribe Topics -spec subscribe(pid(), [{binary(), mqtt_qos()}]) -> ok. subscribe(SessPid, TopicTable) -> @@ -171,11 +175,11 @@ subscribe(SessPid, PacketId, TopicTable) -> -spec publish(pid(), mqtt_message()) -> ok | {error, any()}. publish(_SessPid, Msg = #mqtt_message{qos = ?QOS_0}) -> %% publish qos0 directly - emqttd_pubsub:publish(Msg); + emqttd:publish(Msg); publish(_SessPid, Msg = #mqtt_message{qos = ?QOS_1}) -> %% publish qos1 directly, and client will puback automatically - emqttd_pubsub:publish(Msg); + emqttd:publish(Msg); publish(SessPid, Msg = #mqtt_message{qos = ?QOS_2}) -> %% publish qos2 by session @@ -281,62 +285,51 @@ handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, pktid = PktId}}, handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). -handle_cast({subscribe, TopicTable0, AckFun}, Session = #session{client_id = ClientId, +handle_cast({subscribe, TopicTable0, AckFun}, Session = #session{client_id = ClientId, subscriptions = Subscriptions}) -> TopicTable = emqttd_broker:foldl_hooks('client.subscribe', [ClientId], TopicTable0), + ?LOG(info, "Subscribe ~p", [TopicTable], Session), + Subscriptions1 = lists:foldl( + fun({Topic, Qos}, SubDict) -> + case dict:find(Topic, SubDict) of + {ok, Qos} -> + ?LOG(warning, "duplicated subscribe: ~s, qos = ~w", [Topic, Qos], Session), + SubDict; + {ok, OldQos} -> + emqttd_server:update_subscription(ClientId, Topic, OldQos, Qos), + ?LOG(warning, "duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, Qos], Session), + dict:store(Topic, Qos, SubDict); + error -> + emqttd:subscribe(ClientId, Topic, Qos), + %%TODO: the design is ugly... + %% : 3.8.4 + %% Where the Topic Filter is not identical to any existing Subscription’s filter, + %% a new Subscription is created and all matching retained messages are sent. + emqttd_retainer:dispatch(Topic, self()), - case TopicTable -- dict:to_list(Subscriptions) of - [] -> - AckFun([Qos || {_, Qos} <- TopicTable]), - hibernate(Session); - _ -> - %% subscribe first and don't care if the subscriptions have been existed - {ok, GrantedQos} = emqttd_pubsub:subscribe(ClientId, TopicTable), - - AckFun(GrantedQos), - - emqttd_broker:foreach_hooks('client.subscribe.after', [ClientId, TopicTable]), - - ?LOG(info, "Subscribe ~p, Granted QoS: ~p", [TopicTable, GrantedQos], Session), - - Subscriptions1 = - lists:foldl(fun({Topic, Qos}, Dict) -> - case dict:find(Topic, Dict) of - {ok, Qos} -> - ?LOG(warning, "resubscribe ~s, qos = ~w", [Topic, Qos], Session), - Dict; - {ok, OldQos} -> - ?LOG(warning, "resubscribe ~s, old qos=~w, new qos=~w", [Topic, OldQos, Qos], Session), - dict:store(Topic, Qos, Dict); - error -> - %%TODO: the design is ugly, rewrite later...:( - %% : 3.8.4 - %% Where the Topic Filter is not identical to any existing Subscription’s filter, - %% a new Subscription is created and all matching retained messages are sent. - emqttd_retainer:dispatch(Topic, self()), - - dict:store(Topic, Qos, Dict) - end - end, Subscriptions, TopicTable), - hibernate(Session#session{subscriptions = Subscriptions1}) - end; + dict:store(Topic, Qos, SubDict) + end + end, Subscriptions, TopicTable), + AckFun([Qos || {_, Qos} <- TopicTable]), + emqttd_broker:foreach_hooks('client.subscribe.after', [ClientId, TopicTable]), + hibernate(Session#session{subscriptions = Subscriptions1}); handle_cast({unsubscribe, Topics0}, Session = #session{client_id = ClientId, subscriptions = Subscriptions}) -> Topics = emqttd_broker:foldl_hooks('client.unsubscribe', [ClientId], Topics0), - - %% unsubscribe from topic tree - ok = emqttd_pubsub:unsubscribe(Topics), - ?LOG(info, "unsubscribe ~p", [Topics], Session), - - Subscriptions1 = - lists:foldl(fun(Topic, Dict) -> - dict:erase(Topic, Dict) - end, Subscriptions, Topics), - + Subscriptions1 = lists:foldl( + fun(Topic, SubDict) -> + case dict:find(Topic, SubDict) of + {ok, Qos} -> + emqttd:unsubscribe(ClientId, Topic, Qos), + dict:erase(Topic, SubDict); + error -> + SubDict + end + end, Subscriptions, Topics), hibernate(Session#session{subscriptions = Subscriptions1}); handle_cast({destroy, ClientId}, Session = #session{client_id = ClientId}) -> @@ -430,7 +423,7 @@ handle_cast({pubrel, PktId}, Session = #session{awaiting_rel = AwaitingRel}) -> case maps:find(PktId, AwaitingRel) of {ok, {Msg, TRef}} -> cancel_timer(TRef), - emqttd_pubsub:publish(Msg), + emqttd:publish(Msg), hibernate(Session#session{awaiting_rel = maps:remove(PktId, AwaitingRel)}); error -> ?LOG(error, "Cannot find PUBREL: ~p", [PktId], Session), diff --git a/src/emqttd_stats.erl b/src/emqttd_stats.erl index c6a156d81..755cdb515 100644 --- a/src/emqttd_stats.erl +++ b/src/emqttd_stats.erl @@ -52,14 +52,14 @@ %% $SYS Topics for Subscribers -define(SYSTOP_PUBSUB, [ - 'routes/count', % ... - 'routes/reverse', % ... - 'topics/count', % ... - 'topics/max', % ... + 'routes/count', % ... + 'routes/max', % ... + 'topics/count', % ... + 'topics/max', % ... + 'subscribers/count', % ... + 'subscribers/max', % ... 'subscriptions/count', % ... - 'subscriptions/max', % ... - 'queues/count', % ... - 'queues/max' % ... + 'subscriptions/max' % ... ]). %% $SYS Topic for retained @@ -122,7 +122,7 @@ init([]) -> Topics = ?SYSTOP_CLIENTS ++ ?SYSTOP_SESSIONS ++ ?SYSTOP_PUBSUB ++ ?SYSTOP_RETAINED, ets:insert(?STATS_TAB, [{Topic, 0} || Topic <- Topics]), % Create $SYS Topics - [ok = emqttd_pubsub:create(topic, stats_topic(Topic)) || Topic <- Topics], + [ok = emqttd:create(topic, stats_topic(Topic)) || Topic <- Topics], % Tick to publish stats {ok, #state{tick_tref = emqttd_broker:start_tick(tick)}, hibernate}. @@ -165,8 +165,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- publish(Stat, Val) -> - Msg = emqttd_message:make(stats, stats_topic(Stat), bin(Val)), - emqttd_pubsub:publish(Msg). + emqttd:publish(emqttd_message:make(stats, stats_topic(Stat), bin(Val))). stats_topic(Stat) -> emqttd_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))). diff --git a/src/emqttd_sysmon.erl b/src/emqttd_sysmon.erl index 5fa68bcec..7d6799592 100644 --- a/src/emqttd_sysmon.erl +++ b/src/emqttd_sysmon.erl @@ -157,7 +157,7 @@ procinfo(Pid) -> publish(Sysmon, WarnMsg) -> Msg = emqttd_message:make(sysmon, topic(Sysmon), iolist_to_binary(WarnMsg)), - emqttd_pubsub:publish(emqttd_message:set_flag(sys, Msg)). + emqttd:publish(emqttd_message:set_flag(sys, Msg)). topic(Sysmon) -> emqttd_topic:systop(list_to_binary(lists:concat(['sysmon/', Sysmon]))). From 67f1e15d4ad2b893e9f4063a9602cb59e1e69ea7 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:20:50 +0800 Subject: [PATCH 55/69] start_listeners, stop_listeners --- src/emqttd_app.erl | 99 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index 0f8158c58..385ca134e 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -16,18 +16,30 @@ -module(emqttd_app). --include("emqttd_cli.hrl"). - -behaviour(application). +-include("emqttd_cli.hrl"). + %% Application callbacks -export([start/2, stop/1]). +-export([start_listener/1, stop_listener/1, is_mod_enabled/1]). + +%% MQTT SockOpts +-define(MQTT_SOCKOPTS, [ + binary, + {packet, raw}, + {reuseaddr, true}, + {backlog, 512}, + {nodelay, true}]). + +-type listener() :: {atom(), inet:port_number(), [esockd:option()]}. + %%-------------------------------------------------------------------- %% Application callbacks %%-------------------------------------------------------------------- --spec start(StartType, StartArgs) -> {ok, pid()} | {ok, pid(), State} | {error, Reason} when +-spec start(StartType, StartArgs) -> {ok, pid()} | {ok, pid(), State} | {error, Reason} when StartType :: normal | {takeover, node()} | {failover, node()}, StartArgs :: term(), State :: term(), @@ -38,13 +50,21 @@ start(_StartType, _StartArgs) -> {ok, Sup} = emqttd_sup:start_link(), start_servers(Sup), emqttd_cli:load(), - emqttd:load_all_mods(), + load_all_mods(), emqttd_plugins:load(), - emqttd:start_listeners(), + start_listeners(), register(emqttd, self()), print_vsn(), {ok, Sup}. +-spec stop(State :: term()) -> term(). +stop(_State) -> + catch stop_listeners(). + +%%-------------------------------------------------------------------- +%% Print Banner +%%-------------------------------------------------------------------- + print_banner() -> ?PRINT("starting emqttd on node '~s'~n", [node()]). @@ -53,14 +73,18 @@ print_vsn() -> {ok, Desc} = application:get_key(description), ?PRINT("~s ~s is running now~n", [Desc, Vsn]). +%%-------------------------------------------------------------------- +%% Start Servers +%%-------------------------------------------------------------------- + start_servers(Sup) -> Servers = [{"emqttd ctl", emqttd_ctl}, - {"emqttd trace", {supervisor, emqttd_trace_sup}}, {"emqttd pubsub", {supervisor, emqttd_pubsub_sup}}, {"emqttd stats", emqttd_stats}, {"emqttd metrics", emqttd_metrics}, {"emqttd retainer", emqttd_retainer}, {"emqttd pooler", {supervisor, emqttd_pooler}}, + {"emqttd trace", {supervisor, emqttd_trace_sup}}, {"emqttd client manager", {supervisor, emqttd_cm_sup}}, {"emqttd session manager", {supervisor, emqttd_sm_sup}}, {"emqttd session supervisor", {supervisor, emqttd_session_sup}}, @@ -117,7 +141,64 @@ worker_spec(Module, Opts) when is_atom(Module) -> worker_spec(M, F, A) -> {M, {M, F, A}, permanent, 10000, worker, [M]}. --spec stop(State :: term()) -> term(). -stop(_State) -> - catch emqttd:stop_listeners(). +%%-------------------------------------------------------------------- +%% Load Modules +%%-------------------------------------------------------------------- + +%% @doc load all modules +load_all_mods() -> + lists:foreach(fun load_mod/1, emqttd:env(modules)). + +load_mod({Name, Opts}) -> + Mod = list_to_atom("emqttd_mod_" ++ atom_to_list(Name)), + case catch Mod:load(Opts) of + ok -> lager:info("Load module ~s successfully", [Name]); + {error, Error} -> lager:error("Load module ~s error: ~p", [Name, Error]); + {'EXIT', Reason} -> lager:error("Load module ~s error: ~p", [Name, Reason]) + end. + +%% @doc Is module enabled? +-spec is_mod_enabled(Name :: atom()) -> boolean(). +is_mod_enabled(Name) -> emqttd:env(modules, Name) =/= undefined. + +%%-------------------------------------------------------------------- +%% Start Listeners +%%-------------------------------------------------------------------- + +%% @doc Start Listeners of the broker. +-spec start_listeners() -> any(). +start_listeners() -> lists:foreach(fun start_listener/1, emqttd:env(listeners)). + +%% Start mqtt listener +-spec start_listener(listener()) -> any(). +start_listener({mqtt, Port, Opts}) -> start_listener(mqtt, Port, Opts); + +%% Start mqtt(SSL) listener +start_listener({mqtts, Port, Opts}) -> start_listener(mqtts, Port, Opts); + +%% Start http listener +start_listener({http, Port, Opts}) -> + mochiweb:start_http(Port, Opts, {emqttd_http, handle_request, []}); + +%% Start https listener +start_listener({https, Port, Opts}) -> + mochiweb:start_http(Port, Opts, {emqttd_http, handle_request, []}). + +start_listener(Protocol, Port, Opts) -> + MFArgs = {emqttd_client, start_link, [emqttd:env(mqtt)]}, + esockd:open(Protocol, Port, merge_sockopts(Opts), MFArgs). + +merge_sockopts(Options) -> + SockOpts = emqttd_opts:merge(?MQTT_SOCKOPTS, + proplists:get_value(sockopts, Options, [])), + emqttd_opts:merge(Options, [{sockopts, SockOpts}]). + +%%-------------------------------------------------------------------- +%% Stop Listeners +%%-------------------------------------------------------------------- + +%% @doc Stop Listeners +stop_listeners() -> lists:foreach(fun stop_listener/1, emqttd:env(listeners)). + +stop_listener({Protocol, Port, _Opts}) -> esockd:close({Protocol, Port}). From 73b7de7cb07300d52ae52bccadfafa51b92a170a Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:21:17 +0800 Subject: [PATCH 56/69] emqttd:publish --- src/emqttd_broker.erl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/emqttd_broker.erl b/src/emqttd_broker.erl index 5a26fd291..50b72720a 100644 --- a/src/emqttd_broker.erl +++ b/src/emqttd_broker.erl @@ -157,7 +157,7 @@ init([]) -> emqttd_time:seed(), ets:new(?BROKER_TAB, [set, public, named_table]), % Create $SYS Topics - emqttd_pubsub:create(topic, <<"$SYS/brokers">>), + emqttd:create(topic, <<"$SYS/brokers">>), [ok = create_topic(Topic) || Topic <- ?SYSTOP_BROKERS], % Tick {ok, #state{started_at = os:timestamp(), @@ -228,21 +228,21 @@ insert_hooks(Key, Hooks) -> ets:insert(?BROKER_TAB, {Key, Hooks}), ok. create_topic(Topic) -> - emqttd_pubsub:create(topic, emqttd_topic:systop(Topic)). + emqttd:create(topic, emqttd_topic:systop(Topic)). retain(brokers) -> Payload = list_to_binary(string:join([atom_to_list(N) || N <- emqttd_mnesia:running_nodes()], ",")), Msg = emqttd_message:make(broker, <<"$SYS/brokers">>, Payload), - emqttd_pubsub:publish(emqttd_message:set_flag(sys, Msg)). + emqttd:publish(emqttd_message:set_flag(sys, Msg)). retain(Topic, Payload) when is_binary(Payload) -> Msg = emqttd_message:make(broker, emqttd_topic:systop(Topic), Payload), - emqttd_pubsub:publish(emqttd_message:set_flag(retain, Msg)). + emqttd:publish(emqttd_message:set_flag(retain, Msg)). publish(Topic, Payload) when is_binary(Payload) -> Msg = emqttd_message:make(broker, emqttd_topic:systop(Topic), Payload), - emqttd_pubsub:publish(Msg). + emqttd:publish(Msg). uptime(#state{started_at = Ts}) -> Secs = timer:now_diff(os:timestamp(), Ts) div 1000000, From 26655f1ee3c3f89cf15ef70bd0d270215337b5c7 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:25:19 +0800 Subject: [PATCH 57/69] emqttd:publish/1 --- src/emqttd_backend.erl | 90 ++++++++ src/emqttd_http.erl | 2 +- src/emqttd_pubsub.erl | 265 +++++++++++++++++++++++ src/emqttd_pubsub_old.erl | 414 ------------------------------------ src/emqttd_pubsub_sup.erl | 67 ++---- src/emqttd_router.erl | 225 ++++++++++++++++++++ src/emqttd_server.erl | 254 ++++++++++++++++++++++ src/lager_emqtt_backend.erl | 4 +- 8 files changed, 859 insertions(+), 462 deletions(-) create mode 100644 src/emqttd_backend.erl create mode 100644 src/emqttd_pubsub.erl delete mode 100644 src/emqttd_pubsub_old.erl create mode 100644 src/emqttd_router.erl create mode 100644 src/emqttd_server.erl diff --git a/src/emqttd_backend.erl b/src/emqttd_backend.erl new file mode 100644 index 000000000..39c1a8c2d --- /dev/null +++ b/src/emqttd_backend.erl @@ -0,0 +1,90 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_backend). + +-include("emqttd.hrl"). + +%% Mnesia Callbacks +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +%% API. +-export([add_static_subscription/1, lookup_static_subscriptions/1, + del_static_subscriptions/1, del_static_subscription/2]). + +%%-------------------------------------------------------------------- +%% Mnesia callbacks +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = emqttd_mnesia:create_table(static_subscription, [ + {type, bag}, + {disc_copies, [node()]}, + {record_name, mqtt_subscription}, + {attributes, record_info(fields, mqtt_subscription)}, + {storage_properties, [{ets, [compressed]}, + {dets, [{auto_save, 5000}]}]}]); + +mnesia(copy) -> + ok = emqttd_mnesia:copy_table(static_subscription). + +%%-------------------------------------------------------------------- +%% Static Subscriptions +%%-------------------------------------------------------------------- + +%% @doc Add a static subscription manually. +-spec add_static_subscription(mqtt_subscription()) -> {atom, ok}. +add_static_subscription(Subscription = #mqtt_subscription{subid = SubId, topic = Topic}) -> + Pattern = match_pattern(SubId, Topic), + mnesia:transaction( + fun() -> + case mnesia:match_object(static_subscription, Pattern, write) of + [] -> + mnesia:write(static_subscription, Subscription, write); + [Subscription] -> + mnesia:abort({error, existed}); + [Subscription1] -> %% QoS is different + mnesia:delete_object(static_subscription, Subscription1, write), + mnesia:write(static_subscription, Subscription, write) + end + end). + +%% @doc Lookup static subscriptions. +-spec lookup_static_subscriptions(binary()) -> list(mqtt_subscription()). +lookup_static_subscriptions(ClientId) when is_binary(ClientId) -> + mnesia:dirty_read(static_subscription, ClientId). + +%% @doc Delete static subscriptions by ClientId manually. +-spec del_static_subscriptions(binary()) -> ok. +del_static_subscriptions(ClientId) when is_binary(ClientId) -> + mnesia:transaction(fun mnesia:delete/1, [{static_subscription, ClientId}]). + +%% @doc Delete a static subscription manually. +-spec del_static_subscription(binary(), binary()) -> ok. +del_static_subscription(ClientId, Topic) when is_binary(ClientId) andalso is_binary(Topic) -> + mnesia:transaction(fun del_static_subscription_/1, [match_pattern(ClientId, Topic)]). + +del_static_subscription_(Pattern) -> + lists:foreach(fun(Subscription) -> + mnesia:delete_object(static_subscription, Subscription, write) + end, mnesia:match_object(static_subscription, Pattern, write)). + +match_pattern(SubId, Topic) -> + #mqtt_subscription{subid = SubId, topic = Topic, qos = '_'}. + diff --git a/src/emqttd_http.erl b/src/emqttd_http.erl index d673b14d5..36dc778d9 100644 --- a/src/emqttd_http.erl +++ b/src/emqttd_http.erl @@ -56,7 +56,7 @@ handle_request('POST', "/mqtt/publish", Req) -> case {validate(qos, Qos), validate(topic, Topic)} of {true, true} -> Msg = emqttd_message:make(ClientId, Qos, Topic, Payload), - emqttd_pubsub:publish(Msg#mqtt_message{retain = Retain}), + emqttd:publish(Msg#mqtt_message{retain = Retain}), Req:ok({"text/plain", <<"ok">>}); {false, _} -> Req:respond({400, [], <<"Bad QoS">>}); diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl new file mode 100644 index 000000000..de1cf245c --- /dev/null +++ b/src/emqttd_pubsub.erl @@ -0,0 +1,265 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_pubsub). + +-behaviour(gen_server2). + +-include("emqttd.hrl"). + +-include("emqttd_protocol.hrl"). + +-include("emqttd_internal.hrl"). + +%% Mnesia Callbacks +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +%% API Exports +-export([start_link/3, create_topic/1, lookup_topic/1]). + +-export([subscribe/2, unsubscribe/2, publish/2, dispatch/2, + async_subscribe/2, async_unsubscribe/2]). + +%% gen_server. +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {pool, id, env}). + +%%-------------------------------------------------------------------- +%% Mnesia callbacks +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = emqttd_mnesia:create_table(topic, [ + {ram_copies, [node()]}, + {record_name, mqtt_topic}, + {attributes, record_info(fields, mqtt_topic)}]); + +mnesia(copy) -> + ok = emqttd_mnesia:copy_table(topic). + +%%-------------------------------------------------------------------- +%% Start PubSub +%%-------------------------------------------------------------------- + +%% @doc Start one pubsub +-spec start_link(Pool, Id, Env) -> {ok, pid()} | ignore | {error, any()} when + Pool :: atom(), + Id :: pos_integer(), + Env :: list(tuple()). +start_link(Pool, Id, Env) -> + gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). + +%% @doc Create a Topic. +-spec create_topic(emqttd_topic:topic()) -> ok | {error, any()}. +create_topic(Topic) when is_binary(Topic) -> + case mnesia:transaction(fun add_topic_/2, [Topic, [static]]) of + {atomic, ok} -> ok; + {aborted, Error} -> {error, Error} + end. + +%% @doc Lookup a Topic. +-spec lookup_topic(emqttd_topic:topic()) -> list(mqtt_topic()). +lookup_topic(Topic) when is_binary(Topic) -> + mnesia:dirty_read(topic, Topic). + +%%-------------------------------------------------------------------- +%% PubSub API +%%-------------------------------------------------------------------- + +%% @doc Subscribe a Topic +-spec subscribe(binary(), pid()) -> ok. +subscribe(Topic, SubPid) when is_binary(Topic) -> + call(pick(Topic), {subscribe, Topic, SubPid}). + +%% @doc Asynchronous Subscribe +-spec async_subscribe(binary(), pid()) -> ok. +async_subscribe(Topic, SubPid) when is_binary(Topic) -> + cast(pick(Topic), {subscribe, Topic, SubPid}). + +%% @doc Publish message to Topic. +-spec publish(binary(), any()) -> ok. +publish(Topic, Msg) -> + lists:foreach( + fun(#mqtt_route{topic = To, node = Node}) when Node =:= node() -> + ?MODULE:dispatch(To, Msg); + (#mqtt_route{topic = To, node = Node}) -> + rpc:cast(Node, ?MODULE, dispatch, [To, Msg]) + end, emqttd_router:lookup(Topic)). + +%% @doc Dispatch Message to Subscribers +-spec dispatch(binary(), mqtt_message()) -> ok. +dispatch(Queue = <<"$Q/", _Q>>, Msg) -> + case subscribers(Queue) of + [] -> + dropped(Queue); + [SubPid] -> + SubPid ! {dispatch, Queue, Msg}; + SubPids -> + Idx = crypto:rand_uniform(1, length(SubPids) + 1), + SubPid = lists:nth(Idx, SubPids), + SubPid ! {dispatch, Queue, Msg} + end; + +dispatch(Topic, Msg) -> + case subscribers(Topic) of + [] -> + dropped(Topic); + [SubPid] -> + SubPid ! {dispatch, Topic, Msg}; + SubPids -> + lists:foreach(fun(SubPid) -> + SubPid ! {dispatch, Topic, Msg} + end, SubPids) + end. + +%% @private +%% @doc Find all subscribers +subscribers(Topic) -> + case ets:member(subscriber, Topic) of + true -> %% faster then lookup? + try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end; + false -> + [] + end. + +%% @private +%% @doc Ingore $SYS Messages. +dropped(<<"$SYS/", _/binary>>) -> + ok; +dropped(_Topic) -> + emqttd_metrics:inc('messages/dropped'). + +%% @doc Unsubscribe +-spec unsubscribe(binary(), pid()) -> ok. +unsubscribe(Topic, SubPid) when is_binary(Topic) -> + call(pick(Topic), {unsubscribe, Topic, SubPid}). + +%% @doc Asynchronous Unsubscribe +-spec async_unsubscribe(binary(), pid()) -> ok. +async_unsubscribe(Topic, SubPid) when is_binary(Topic) -> + cast(pick(Topic), {unsubscribe, Topic, SubPid}). + +call(PubSub, Req) when is_pid(PubSub) -> + gen_server2:call(PubSub, Req, infinity). + +cast(PubSub, Msg) when is_pid(PubSub) -> + gen_server2:cast(PubSub, Msg). + +pick(Topic) -> gproc_pool:pick_worker(pubsub, Topic). + +%%-------------------------------------------------------------------- +%% gen_server Callbacks +%%-------------------------------------------------------------------- + +init([Pool, Id, Env]) -> + ?GPROC_POOL(join, Pool, Id), + {ok, #state{pool = Pool, id = Id, env = Env}}. + +handle_call({subscribe, Topic, SubPid}, _From, State) -> + add_subscriber_(Topic, SubPid), + {reply, ok, setstats(State)}; + +handle_call({unsubscribe, Topic, SubPid}, _From, State) -> + del_subscriber_(Topic, SubPid), + {reply, ok, setstats(State)}; + +handle_call(Req, _From, State) -> + ?UNEXPECTED_REQ(Req, State). + +handle_cast({subscribe, Topic, SubPid}, State) -> + add_subscriber_(Topic, SubPid), + {noreply, setstats(State)}; + +handle_cast({unsubscribe, Topic, SubPid}, State) -> + del_subscriber_(Topic, SubPid), + {noreply, setstats(State)}; + +handle_cast(Msg, State) -> + ?UNEXPECTED_MSG(Msg, State). + +handle_info(Info, State) -> + ?UNEXPECTED_INFO(Info, State). + +terminate(_Reason, #state{pool = Pool, id = Id}) -> + ?GPROC_POOL(leave, Pool, Id). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal Functions +%%-------------------------------------------------------------------- + +add_subscriber_(Topic, SubPid) -> + case ets:member(subscriber, Topic) of + false -> + mnesia:transaction(fun add_topic_/1, [Topic]), + emqttd_router:add_route(Topic, node()), + setstats(topic); + true -> + ok + end, + ets:insert(subscriber, {Topic, SubPid}). + +del_subscriber_(Topic, SubPid) -> + ets:delete_object(subscriber, {Topic, SubPid}), + case ets:lookup(subscriber, Topic) of + [] -> + emqttd_router:del_route(Topic, node()), + mnesia:transaction(fun del_topic_/1, [Topic]), + setstats(topic); + [_|_] -> + ok + end. + +add_topic_(Topic) -> + add_topic_(Topic, []). + +add_topic_(Topic, Flags) -> + Record = #mqtt_topic{topic = Topic, flags = Flags}, + case mnesia:wread({topic, Topic}) of + [] -> mnesia:write(topic, Record, write); + [_] -> ok + end. + +del_topic_(Topic) -> + case emqttd_router:has_route(Topic) of + true -> ok; + false -> do_del_topic_(Topic) + end. + +do_del_topic_(Topic) -> + case mnesia:wread({topic, Topic}) of + [#mqtt_topic{flags = []}] -> + mnesia:delete(topic, Topic, write); + _ -> + ok + end. + +setstats(State) when is_record(State, state) -> + setstats(subscriber), State; + +setstats(topic) -> + emqttd_stats:setstats('topics/count', 'topics/max', mnesia:table_info(topic, size)); + +setstats(subscriber) -> + emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(subscriber, size)). + diff --git a/src/emqttd_pubsub_old.erl b/src/emqttd_pubsub_old.erl deleted file mode 100644 index eb60fdb1a..000000000 --- a/src/emqttd_pubsub_old.erl +++ /dev/null @@ -1,414 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqttd_pubsub). - --behaviour(gen_server2). - --include("emqttd.hrl"). - --include("emqttd_protocol.hrl"). - --include("emqttd_internal.hrl"). - -%% Mnesia Callbacks --export([mnesia/1]). - --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). - -%% API Exports --export([start_link/4]). - --export([create/2, lookup/2, subscribe/1, subscribe/2, - publish/1, unsubscribe/1, unsubscribe/2, delete/2]). - -%% Local node --export([match/1]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {pool, id, statsfun}). - --define(ROUTER, emqttd_router). - -%%-------------------------------------------------------------------- -%% Mnesia callbacks -%%-------------------------------------------------------------------- -mnesia(boot) -> - ok = create_table(topic, ram_copies), - if_subscription(fun(RamOrDisc) -> - ok = create_table(subscription, RamOrDisc) - end); - -mnesia(copy) -> - ok = emqttd_mnesia:copy_table(topic), - %% Only one disc_copy??? - if_subscription(fun(_RamOrDisc) -> - ok = emqttd_mnesia:copy_table(subscription) - end). - -%% Topic Table -create_table(topic, RamOrDisc) -> - emqttd_mnesia:create_table(topic, [ - {type, bag}, - {RamOrDisc, [node()]}, - {record_name, mqtt_topic}, - {attributes, record_info(fields, mqtt_topic)}]); - -%% Subscription Table -create_table(subscription, RamOrDisc) -> - emqttd_mnesia:create_table(subscription, [ - {type, bag}, - {RamOrDisc, [node()]}, - {record_name, mqtt_subscription}, - {attributes, record_info(fields, mqtt_subscription)}, - {storage_properties, [{ets, [compressed]}, - {dets, [{auto_save, 5000}]}]}]). - -if_subscription(Fun) -> - case env(subscription) of - disc -> Fun(disc_copies); - ram -> Fun(ram_copies); - false -> ok; - undefined -> ok - end. - -env(Key) -> - case get({pubsub, Key}) of - undefined -> - cache_env(Key); - Val -> - Val - end. - -cache_env(Key) -> - Val = proplists:get_value(Key, emqttd_broker:env(pubsub)), - put({pubsub, Key}, Val), - Val. - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -%% @doc Start one pubsub server --spec start_link(Pool, Id, StatsFun, Opts) -> {ok, pid()} | ignore | {error, any()} when - Pool :: atom(), - Id :: pos_integer(), - StatsFun :: fun((atom()) -> any()), - Opts :: list(tuple()). -start_link(Pool, Id, StatsFun, Opts) -> - gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, - ?MODULE, [Pool, Id, StatsFun, Opts], []). - -%% @doc Create Topic or Subscription. --spec create(topic, emqttd_topic:topic()) -> ok | {error, any()}; - (subscription, {binary(), binary(), mqtt_qos()}) -> ok | {error, any()}. -create(topic, Topic) when is_binary(Topic) -> - Record = #mqtt_topic{topic = Topic, node = node()}, - case mnesia:transaction(fun add_topic/1, [Record]) of - {atomic, ok} -> ok; - {aborted, Error} -> {error, Error} - end; - -create(subscription, {SubId, Topic, Qos}) when is_binary(SubId) andalso is_binary(Topic) -> - case mnesia:transaction(fun add_subscription/2, [SubId, {Topic, ?QOS_I(Qos)}]) of - {atomic, ok} -> ok; - {aborted, Error} -> {error, Error} - end. - -%% @doc Lookup Topic or Subscription. --spec lookup(topic, emqttd_topic:topic()) -> list(mqtt_topic()); - (subscription, binary()) -> list(mqtt_subscription()). -lookup(topic, Topic) when is_binary(Topic) -> - mnesia:dirty_read(topic, Topic); - -lookup(subscription, SubId) when is_binary(SubId) -> - mnesia:dirty_read(subscription, SubId). - -%% @doc Delete Topic or Subscription. --spec delete(topic, emqttd_topic:topic()) -> ok | {error, any()}; - (subscription, binary() | {binary(), emqttd_topic:topic()}) -> ok. -delete(topic, _Topic) -> - {error, unsupported}; - -delete(subscription, SubId) when is_binary(SubId) -> - mnesia:dirty_delete({subscription, SubId}); - -delete(subscription, {SubId, Topic}) when is_binary(SubId) andalso is_binary(Topic) -> - mnesia:async_dirty(fun remove_subscriptions/2, [SubId, [Topic]]). - -%% @doc Subscribe Topics --spec subscribe({Topic, Qos} | list({Topic, Qos})) -> - {ok, Qos | list(Qos)} | {error, any()} when - Topic :: binary(), - Qos :: mqtt_qos() | mqtt_qos_name(). -subscribe({Topic, Qos}) -> - subscribe([{Topic, Qos}]); -subscribe(TopicTable) when is_list(TopicTable) -> - call({subscribe, {undefined, self()}, fixqos(TopicTable)}). - --spec subscribe(ClientId, {Topic, Qos} | list({Topic, Qos})) -> - {ok, Qos | list(Qos)} | {error, any()} when - ClientId :: binary(), - Topic :: binary(), - Qos :: mqtt_qos() | mqtt_qos_name(). -subscribe(ClientId, {Topic, Qos}) when is_binary(ClientId) -> - subscribe(ClientId, [{Topic, Qos}]); -subscribe(ClientId, TopicTable) when is_binary(ClientId) andalso is_list(TopicTable) -> - call({subscribe, {ClientId, self()}, fixqos(TopicTable)}). - -fixqos(TopicTable) -> - [{Topic, ?QOS_I(Qos)} || {Topic, Qos} <- TopicTable]. - -%% @doc Unsubscribe Topic or Topics --spec unsubscribe(emqttd_topic:topic() | list(emqttd_topic:topic())) -> ok. -unsubscribe(Topic) when is_binary(Topic) -> - unsubscribe([Topic]); -unsubscribe(Topics = [Topic|_]) when is_binary(Topic) -> - cast({unsubscribe, {undefined, self()}, Topics}). - --spec unsubscribe(binary(), emqttd_topic:topic() | list(emqttd_topic:topic())) -> ok. -unsubscribe(ClientId, Topic) when is_binary(ClientId) andalso is_binary(Topic) -> - unsubscribe(ClientId, [Topic]); -unsubscribe(ClientId, Topics = [Topic|_]) when is_binary(Topic) -> - cast({unsubscribe, {ClientId, self()}, Topics}). - -call(Request) -> - gen_server2:call(pick(self()), Request, infinity). - -cast(Msg) -> - gen_server2:cast(pick(self()), Msg). - -pick(Self) -> gproc_pool:pick_worker(pubsub, Self). - -%% @doc Publish to cluster nodes --spec publish(Msg :: mqtt_message()) -> ok. -publish(Msg = #mqtt_message{from = From}) -> - trace(publish, From, Msg), - Msg1 = #mqtt_message{topic = To} - = emqttd_broker:foldl_hooks('message.publish', [], Msg), - - %% Retain message first. Don't create retained topic. - case emqttd_retainer:retain(Msg1) of - ok -> - %% TODO: why unset 'retain' flag? - publish(To, emqttd_message:unset_flag(Msg1)); - ignore -> - publish(To, Msg1) - end. - -publish(To, Msg) -> - lists:foreach(fun(#mqtt_topic{topic = Topic, node = Node}) -> - case Node =:= node() of - true -> ?ROUTER:route(Topic, Msg); - false -> rpc:cast(Node, ?ROUTER, route, [Topic, Msg]) - end - end, match(To)). - -%% @doc Match Topic Name with Topic Filters --spec match(emqttd_topic:topic()) -> [mqtt_topic()]. -match(To) -> - Matched = mnesia:async_dirty(fun emqttd_trie:match/1, [To]), - %% ets:lookup for topic table will be replicated to all nodes. - lists:append([ets:lookup(topic, Topic) || Topic <- [To | Matched]]). - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([Pool, Id, StatsFun, _Opts]) -> - ?GPROC_POOL(join, Pool, Id), - {ok, #state{pool = Pool, id = Id, statsfun = StatsFun}}. - -handle_call({subscribe, {SubId, SubPid}, TopicTable}, _From, - State = #state{statsfun = StatsFun}) -> - - %% Monitor SubPid first - try_monitor(SubPid), - - %% Topics - Topics = [Topic || {Topic, _Qos} <- TopicTable], - - NewTopics = Topics -- reverse_routes(SubPid), - - %% Add routes - ?ROUTER:add_routes(NewTopics, SubPid), - - insert_reverse_routes(SubPid, NewTopics), - - StatsFun(reverse_route), - - %% Insert topic records to mnesia - Records = [#mqtt_topic{topic = Topic, node = node()} || Topic <- NewTopics], - - case mnesia:transaction(fun add_topics/1, [Records]) of - {atomic, _} -> - StatsFun(topic), - if_subscription( - fun(_) -> - %% Add subscriptions - Args = [fun add_subscriptions/2, [SubId, TopicTable]], - emqttd_pooler:async_submit({mnesia, async_dirty, Args}), - StatsFun(subscription) - end), - %% Grant all qos... - {reply, {ok, [Qos || {_Topic, Qos} <- TopicTable]}, State}; - {aborted, Error} -> - {reply, {error, Error}, State} - end; - -handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). - -handle_cast({unsubscribe, {SubId, SubPid}, Topics}, State = #state{statsfun = StatsFun}) -> - - %% Delete routes first - ?ROUTER:delete_routes(Topics, SubPid), - - delete_reverse_routes(SubPid, Topics), - - StatsFun(reverse_route), - - %% Remove subscriptions - if_subscription( - fun(_) -> - Args = [fun remove_subscriptions/2, [SubId, Topics]], - emqttd_pooler:async_submit({mnesia, async_dirty, Args}), - StatsFun(subscription) - end), - - {noreply, State}; - -handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). - -handle_info({'DOWN', _Mon, _Type, DownPid, _Info}, State = #state{statsfun = StatsFun}) -> - - Topics = reverse_routes(DownPid), - - ?ROUTER:delete_routes(Topics, DownPid), - - delete_reverse_routes(DownPid), - - StatsFun(reverse_route), - - {noreply, State, hibernate}; - -handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). - -terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -add_topics(Records) -> - lists:foreach(fun add_topic/1, Records). - -add_topic(TopicR = #mqtt_topic{topic = Topic}) -> - case mnesia:wread({topic, Topic}) of - [] -> - case emqttd_topic:wildcard(Topic) of - true -> emqttd_trie:insert(Topic); - false -> ok - end, - mnesia:write(topic, TopicR, write); - Records -> - case lists:member(TopicR, Records) of - true -> ok; - false -> mnesia:write(topic, TopicR, write) - end - end. - -add_subscriptions(undefined, _TopicTable) -> - ok; -add_subscriptions(SubId, TopicTable) -> - lists:foreach(fun({Topic, Qos}) -> - add_subscription(SubId, {Topic, Qos}) - end,TopicTable). - -add_subscription(SubId, {Topic, Qos}) -> - Subscription = #mqtt_subscription{subid = SubId, topic = Topic, qos = Qos}, - Pattern = #mqtt_subscription{subid = SubId, topic = Topic, qos = '_'}, - Records = mnesia:match_object(subscription, Pattern, write), - case lists:member(Subscription, Records) of - true -> - ok; - false -> - [delete_subscription(Record) || Record <- Records], - insert_subscription(Subscription) - end. - -insert_subscription(Record) -> - mnesia:write(subscription, Record, write). - -remove_subscriptions(undefined, _Topics) -> - ok; -remove_subscriptions(SubId, Topics) -> - lists:foreach(fun(Topic) -> - Pattern = #mqtt_subscription{subid = SubId, topic = Topic, qos = '_'}, - Records = mnesia:match_object(subscription, Pattern, write), - lists:foreach(fun delete_subscription/1, Records) - end, Topics). - -delete_subscription(Record) -> - mnesia:delete_object(subscription, Record, write). - -reverse_routes(SubPid) -> - case ets:member(reverse_route, SubPid) of - true -> - try ets:lookup_element(reverse_route, SubPid, 2) catch error:badarg -> [] end; - false -> - [] - end. - -insert_reverse_routes(SubPid, Topics) -> - ets:insert(reverse_route, [{SubPid, Topic} || Topic <- Topics]). - -delete_reverse_routes(SubPid, Topics) -> - lists:foreach(fun(Topic) -> - ets:delete_object(reverse_route, {SubPid, Topic}) - end, Topics). - -delete_reverse_routes(SubPid) -> - ets:delete(reverse_route, SubPid). - -try_monitor(SubPid) -> - case ets:member(reverse_route, SubPid) of - true -> ignore; - false -> erlang:monitor(process, SubPid) - end. - -%%-------------------------------------------------------------------- -%% Trace Functions -%%-------------------------------------------------------------------- - -trace(publish, From, _Msg) when is_atom(From) -> - %% Dont' trace '$SYS' publish - ignore; - -trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) -> - lager:info([{client, From}, {topic, Topic}], - "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). - diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index c04b486f6..28ff8033b 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -21,8 +21,6 @@ -include("emqttd.hrl"). --define(HELPER, emqttd_pubsub_helper). - -define(CONCURRENCY_OPTS, [{read_concurrency, true}, {write_concurrency, true}]). %% API @@ -38,33 +36,37 @@ pubsub_pool() -> hd([Pid|| {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]). init([Env]) -> - %% Create tabs - create_tab(route), create_tab(reverse_route), - %% PubSub Helper - Helper = {helper, {?HELPER, start_link, [fun setstats/1]}, - permanent, infinity, worker, [?HELPER]}, + %% Create ETS Tabs + create_tab(subscriber), create_tab(subscribed), - %% Router Pool Sup - RouterMFA = {emqttd_router, start_link, [fun setstats/1, Env]}, - - %% Pool_size / 2 - RouterSup = emqttd_pool_sup:spec(router_pool, [router, hash, router_pool(Env), RouterMFA]), + %% Router + Router = {router, {emqttd_router, start_link, []}, + permanent, 5000, worker, [emqttd_router]}, %% PubSub Pool Sup - PubSubMFA = {emqttd_pubsub, start_link, [fun setstats/1, Env]}, - PubSubSup = emqttd_pool_sup:spec(pubsub_pool, [pubsub, hash, pool_size(Env), PubSubMFA]), + PubSubMFA = {emqttd_pubsub, start_link, [Env]}, + PubSubPoolSup = emqttd_pool_sup:spec(pubsub_pool, [pubsub, hash, pool_size(Env), PubSubMFA]), - {ok, {{one_for_all, 10, 60}, [Helper, RouterSup, PubSubSup]}}. + %% Server Pool Sup + ServerMFA = {emqttd_server, start_link, [Env]}, + ServerPoolSup = emqttd_pool_sup:spec(server_pool, [server, hash, pool_size(Env), ServerMFA]), -create_tab(route) -> - %% Route Table: Topic -> Pid1, Pid2, ..., PidN + {ok, {{one_for_all, 5, 60}, [Router, PubSubPoolSup, ServerPoolSup]}}. + +pool_size(Env) -> + Schedulers = erlang:system_info(schedulers), + proplists:get_value(pool_size, Env, Schedulers). + +create_tab(subscriber) -> + %% subscriber: Topic -> Pid1, Pid2, ..., PidN %% duplicate_bag: o(1) insert - ensure_tab(route, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]); + ensure_tab(subscriber, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]); -create_tab(reverse_route) -> - %% Reverse Route Table: Pid -> Topic1, Topic2, ..., TopicN - ensure_tab(reverse_route, [public, named_table, bag | ?CONCURRENCY_OPTS]). +create_tab(subscribed) -> + %% subscribed: Pid -> Topic1, Topic2, ..., TopicN + %% bag: o(n) insert + ensure_tab(subscribed, [public, named_table, bag | ?CONCURRENCY_OPTS]). ensure_tab(Tab, Opts) -> case ets:info(Tab, name) of @@ -72,26 +74,3 @@ ensure_tab(Tab, Opts) -> _ -> ok end. -router_pool(Env) -> - case pool_size(Env) div 2 of - 0 -> 1; - I -> I - end. - -pool_size(Env) -> - Schedulers = erlang:system_info(schedulers), - proplists:get_value(pool_size, Env, Schedulers). - -setstats(route) -> - emqttd_stats:setstat('routes/count', ets:info(route, size)); - -setstats(reverse_route) -> - emqttd_stats:setstat('routes/reverse', ets:info(reverse_route, size)); - -setstats(topic) -> - emqttd_stats:setstats('topics/count', 'topics/max', mnesia:table_info(topic, size)); - -setstats(subscription) -> - emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', - mnesia:table_info(subscription, size)). - diff --git a/src/emqttd_router.erl b/src/emqttd_router.erl new file mode 100644 index 000000000..ac744e94a --- /dev/null +++ b/src/emqttd_router.erl @@ -0,0 +1,225 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_router). + +-behaviour(gen_server). + +-include("emqttd.hrl"). + +%% Mnesia Bootstrap +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +-export([start_link/0, stop/0]). + +-export([add_route/1, add_route/2, add_routes/1, lookup/1, print/1, + del_route/1, del_route/2, del_routes/1, has_route/1]). + +%% gen_server Function Exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {}). + +%%-------------------------------------------------------------------- +%% Mnesia Bootstrap +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = emqttd_mnesia:create_table(route, [ + {type, bag}, + {ram_copies, [node()]}, + {record_name, mqtt_route}, + {attributes, record_info(fields, mqtt_route)}]); + +mnesia(copy) -> + ok = emqttd_mnesia:copy_table(route, ram_copies). + +%%-------------------------------------------------------------------- +%% Start the Router +%%-------------------------------------------------------------------- + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +%% @doc Lookup Routes. +-spec lookup(Topic:: binary()) -> [mqtt_route()]. +lookup(Topic) when is_binary(Topic) -> + Matched = mnesia:async_dirty(fun emqttd_trie:match/1, [Topic]), + %% Optimize: route table will be replicated to all nodes. + lists:append([ets:lookup(route, To) || To <- [Topic | Matched]]). + +%% @doc Print Routes. +-spec print(Topic :: binary()) -> [ok]. +print(Topic) -> + [io:format("~s -> ~s~n", [To, Node]) || + #mqtt_route{topic = To, node = Node} <- lookup(Topic)]. + +%% @doc Add Route +-spec add_route(binary() | mqtt_route()) -> ok | {error, Reason :: any()}. +add_route(Topic) when is_binary(Topic) -> + add_route(#mqtt_route{topic = Topic, node = node()}); +add_route(Route) when is_record(Route, mqtt_route) -> + add_routes([Route]). + +-spec add_route(Topic :: binary(), Node :: node()) -> ok | {error, Reason :: any()}. +add_route(Topic, Node) when is_binary(Topic), is_atom(Node) -> + add_route(#mqtt_route{topic = Topic, node = Node}). + +%% @doc Add Routes +-spec add_routes([mqtt_route()]) -> ok | {errory, Reason :: any()}. +add_routes(Routes) -> + Add = fun() -> [add_route_(Route) || Route <- Routes] end, + case mnesia:transaction(Add) of + {atomic, _} -> update_stats_(), ok; + {aborted, Error} -> {error, Error} + end. + +%% @private +add_route_(Route = #mqtt_route{topic = Topic}) -> + case mnesia:wread({route, Topic}) of + [] -> + case emqttd_topic:wildcard(Topic) of + true -> emqttd_trie:insert(Topic); + false -> ok + end, + mnesia:write(route, Route, write); + Records -> + case lists:member(Route, Records) of + true -> ok; + false -> mnesia:write(route, Route, write) + end + end. + +%% @doc Delete Route +-spec del_route(binary() | mqtt_route()) -> ok | {error, Reason :: any()}. +del_route(Topic) when is_binary(Topic) -> + del_route(#mqtt_route{topic = Topic, node = node()}); +del_route(Route) when is_record(Route, mqtt_route) -> + del_routes([Route]). + +-spec del_route(Topic :: binary(), Node :: node()) -> ok | {error, Reason :: any()}. +del_route(Topic, Node) when is_binary(Topic), is_atom(Node) -> + del_route(#mqtt_route{topic = Topic, node = Node}). + +%% @doc Delete Routes +-spec del_routes([mqtt_route()]) -> ok | {error, any()}. +del_routes(Routes) -> + Del = fun() -> [del_route_(Route) || Route <- Routes] end, + case mnesia:transaction(Del) of + {atomic, _} -> update_stats_(), ok; + {aborted, Error} -> {error, Error} + end. + +del_route_(Route = #mqtt_route{topic = Topic}) -> + case mnesia:wread({route, Topic}) of + [] -> + ok; + [Route] -> + %% Remove route and trie + mnesia:delete_object(route, Route, write), + case emqttd_topic:wildcard(Topic) of + true -> emqttd_trie:delete(Topic); + false -> ok + end; + _More -> + %% Remove route only + mnesia:delete_object(route, Route, write) + end. + +%% @doc Has Route? +-spec has_route(binary()) -> boolean(). +has_route(Topic) -> + Routes = case mnesia:is_transaction() of + true -> mnesia:read(route, Topic); + false -> mnesia:dirty_read(route, Topic) + end, + length(Routes) > 0. + +stop() -> gen_server:call(?MODULE, stop). + +%%-------------------------------------------------------------------- +%% gen_server Callbacks +%%-------------------------------------------------------------------- + +init([]) -> + mnesia:subscribe(system), + {ok, #state{}}. + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; + +handle_call(_Req, _From, State) -> + {reply, ignore, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({mnesia_system_event, {mnesia_up, Node}}, State) -> + lager:error("Mnesia up: ~p~n", [Node]), + {noreply, State}; + +handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> + lager:error("Mnesia down: ~p~n", [Node]), + clean_routes_(Node), + update_stats_(), + {noreply, State}; + +handle_info({mnesia_system_event, {inconsistent_database, Context, Node}}, State) -> + %% 1. Backup and restart + %% 2. Set master nodes + lager:critical("Mnesia inconsistent_database event: ~p, ~p~n", [Context, Node]), + {noreply, State}; + +handle_info({mnesia_system_event, {mnesia_overload, Details}}, State) -> + lager:critical("Mnesia overload: ~p~n", [Details]), + {noreply, State}; + +handle_info({mnesia_system_event, _Event}, State) -> + {noreply, State}; + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + mnesia:unsubscribe(system). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal Functions +%%-------------------------------------------------------------------- + +%% Clean Routes on Node +clean_routes_(Node) -> + Pattern = #mqtt_route{_ = '_', node = Node}, + Clean = fun() -> + [mnesia:delete_object(route, R, write) || + R <- mnesia:match_object(route, Pattern, write)] + end, + mnesia:transaction(Clean). + +update_stats_() -> + emqttd_stats:setstats('routes/count', 'routes/max', mnesia:table_info(route, size)). + diff --git a/src/emqttd_server.erl b/src/emqttd_server.erl new file mode 100644 index 000000000..e119a5029 --- /dev/null +++ b/src/emqttd_server.erl @@ -0,0 +1,254 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_server). + +-behaviour(gen_server2). + +-include("emqttd.hrl"). + +-include("emqttd_protocol.hrl"). + +-include("emqttd_internal.hrl"). + +%% Mnesia Callbacks +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +%% API Exports +-export([start_link/3]). + +%% PubSub API +-export([subscribe/1, subscribe/3, publish/1, unsubscribe/1, unsubscribe/3, + update_subscription/4]). + +%% gen_server Function Exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {pool, id, env, monitors}). + +%%-------------------------------------------------------------------- +%% Mnesia callbacks +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = emqttd_mnesia:create_table(subscription, [ + {type, bag}, + {ram_copies, [node()]}, + {local_content, true}, %% subscription table is local + {record_name, mqtt_subscription}, + {attributes, record_info(fields, mqtt_subscription)}]); + +mnesia(copy) -> + ok = emqttd_mnesia:copy_table(subscription). + +%%-------------------------------------------------------------------- +%% Start server +%%-------------------------------------------------------------------- + +%% @doc Start a Server +-spec start_link(Pool, Id, Env) -> {ok, pid()} | ignore | {error, any()} when + Pool :: atom(), + Id :: pos_integer(), + Env :: list(tuple()). +start_link(Pool, Id, Env) -> + gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). + +%%-------------------------------------------------------------------- +%% PubSub API +%%-------------------------------------------------------------------- + +%% @doc Subscribe a Topic +-spec subscribe(binary()) -> ok. +subscribe(Topic) when is_binary(Topic) -> + From = self(), call(server(From), {subscribe, From, Topic}). + +%% @doc Subscribe from a MQTT session. +-spec subscribe(binary(), binary(), mqtt_qos()) -> ok. +subscribe(ClientId, Topic, Qos) -> + From = self(), call(server(From), {subscribe, From, ClientId, Topic, ?QOS_I(Qos)}). + +%% @doc Update a subscription. +-spec update_subscription(binary(), binary(), mqtt_qos(), mqtt_qos()) -> ok. +update_subscription(ClientId, Topic, OldQos, NewQos) -> + call(server(self()), {update_subscription, ClientId, Topic, ?QOS_I(OldQos), ?QOS_I(NewQos)}). + +%% @doc Publish a Message +-spec publish(Msg :: mqtt_message()) -> ok. +publish(Msg = #mqtt_message{from = From}) -> + trace(publish, From, Msg), + Msg1 = #mqtt_message{topic = Topic} + = emqttd_broker:foldl_hooks('message.publish', [], Msg), + %% Retain message first. Don't create retained topic. + Msg2 = case emqttd_retainer:retain(Msg1) of + ok -> emqttd_message:unset_flag(Msg1); + ignore -> Msg1 + end, + emqttd_pubsub:publish(Topic, Msg2). + +%% @doc Unsubscribe a Topic +-spec unsubscribe(binary()) -> ok. +unsubscribe(Topic) when is_binary(Topic) -> + From = self(), call(server(From), {unsubscribe, From, Topic}). + +%% @doc Unsubscribe a Topic from a MQTT session +-spec unsubscribe(binary(), binary(), mqtt_qos()) -> ok. +unsubscribe(ClientId, Topic, Qos) -> + From = self(), call(server(From), {unsubscribe, From, ClientId, Topic, Qos}). + +call(Server, Req) -> + gen_server2:call(Server, Req, infinity). + +server(From) -> + gproc_pool:pick_worker(server, From). + +%%-------------------------------------------------------------------- +%% gen_server Callbacks +%%-------------------------------------------------------------------- + +init([Pool, Id, Env]) -> + ?GPROC_POOL(join, Pool, Id), + {ok, #state{pool = Pool, id = Id, env = Env, monitors = dict:new()}}. + +handle_call({subscribe, SubPid, ClientId, Topic, Qos}, _From, State) -> + add_subscription_(ClientId, Topic, Qos), + set_subscription_stats(), + do_subscribe_(SubPid, Topic), + ok(monitor_subscriber_(ClientId, SubPid, State)); + +handle_call({subscribe, SubPid, Topic}, _From, State) -> + do_subscribe_(SubPid, Topic), + ok(monitor_subscriber_(undefined, SubPid, State)); + +handle_call({update_subscription, ClientId, Topic, OldQos, NewQos}, _From, State) -> + OldSub = #mqtt_subscription{subid = ClientId, topic = Topic, qos = OldQos}, + NewSub = #mqtt_subscription{subid = ClientId, topic = Topic, qos = NewQos}, + mnesia:transaction(fun update_subscription_/2, [OldSub, NewSub]), + set_subscription_stats(), ok(State); + +handle_call({unsubscribe, SubPid, ClientId, Topic, Qos}, From, State) -> + del_subscription_(ClientId, Topic, Qos), + set_subscription_stats(), + handle_call({unsubscribe, SubPid, Topic}, From, State); + +handle_call({unsubscribe, SubPid, Topic}, _From, State) -> + emqttd_pubsub:unsubscribe(Topic, SubPid), + ets:delete_object(subscribed, {SubPid, Topic}), + ok(State); + +handle_call(Req, _From, State) -> + ?UNEXPECTED_REQ(Req, State). + +handle_cast(Msg, State) -> + ?UNEXPECTED_MSG(Msg, State). + +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{monitors = Monitors}) -> + %% unsubscribe + lists:foreach(fun({_, Topic}) -> + emqttd_pubsub:async_unsubscribe(Topic, DownPid) + end, ets:lookup(subscribed, DownPid)), + ets:delete(subscribed, DownPid), + + %% clean subscriptions + case dict:find(DownPid, Monitors) of + {ok, {undefined, _}} -> ok; + {ok, {ClientId, _}} -> mnesia:dirty_delete(subscription, ClientId); + error -> ok + end, + {noreply, State#state{monitors = dict:erase(DownPid, Monitors)}}; + +handle_info(Info, State) -> + ?UNEXPECTED_INFO(Info, State). + +terminate(_Reason, #state{pool = Pool, id = Id}) -> + ?GPROC_POOL(leave, Pool, Id). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal Functions +%%-------------------------------------------------------------------- + +%% @private +%% @doc Add a subscription. +-spec add_subscription_(binary(), binary(), mqtt_qos()) -> ok. +add_subscription_(ClientId, Topic, Qos) -> + add_subscription_(#mqtt_subscription{subid = ClientId, topic = Topic, qos = Qos}). + +-spec add_subscription_(mqtt_subscription()) -> ok. +add_subscription_(Subscription) when is_record(Subscription, mqtt_subscription) -> + mnesia:dirty_write(subscription, Subscription). + +update_subscription_(OldSub, NewSub) -> + mnesia:delete_object(subscription, OldSub, write), + mnesia:write(subscription, NewSub, write). + +%% @private +%% @doc Delete a subscription +-spec del_subscription_(binary(), binary(), mqtt_qos()) -> ok. +del_subscription_(ClientId, Topic, Qos) -> + del_subscription_(#mqtt_subscription{subid = ClientId, topic = Topic, qos = Qos}). + +del_subscription_(Subscription) when is_record(Subscription, mqtt_subscription) -> + mnesia:dirty_delete_object(subscription, Subscription). + +%% @private +%% @doc Call pubsub to subscribe +do_subscribe_(SubPid, Topic) -> + case ets:match(subscribed, {SubPid, Topic}) of + [] -> + emqttd_pubsub:subscribe(Topic, SubPid), + ets:insert(subscribed, {SubPid, Topic}); + [_] -> + false + end. + +monitor_subscriber_(ClientId, SubPid, State = #state{monitors = Monitors}) -> + case dict:find(SubPid, Monitors) of + {ok, _} -> + State; + error -> + MRef = erlang:monitor(process, SubPid), + State#state{monitors = dict:store(SubPid, {ClientId, MRef}, Monitors)} + end. + +%%-------------------------------------------------------------------- +%% Trace Functions +%%-------------------------------------------------------------------- + +trace(publish, From, _Msg) when is_atom(From) -> + %% Dont' trace '$SYS' publish + ignore; + +trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) -> + lager:info([{client, From}, {topic, Topic}], + "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + +%%-------------------------------------------------------------------- +%% Subscription Statistics +%%-------------------------------------------------------------------- + +set_subscription_stats() -> + emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', mnesia:table_info(subscription, size)). + +%%-------------------------------------------------------------------- + +ok(State) -> {reply, ok, State}. + diff --git a/src/lager_emqtt_backend.erl b/src/lager_emqtt_backend.erl index 9c28090bf..9deccac52 100644 --- a/src/lager_emqtt_backend.erl +++ b/src/lager_emqtt_backend.erl @@ -77,9 +77,7 @@ publish_log(Message, State = #state{formatter = Formatter, format_config = FormatConfig}) -> Severity = lager_msg:severity(Message), Payload = Formatter:format(Message, FormatConfig), - emqttd_pubsub:publish( - emqttd_message:make( - log, topic(Severity), iolist_to_binary(Payload))), + emqttd:publish(emqttd_message:make(log, topic(Severity), iolist_to_binary(Payload))), {ok, State}. topic(Severity) -> From 16d488adca813daa2025dab231944aff83cb3604 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:26:46 +0800 Subject: [PATCH 58/69] emqttd:publish/1 --- src/emqttd_mod_presence.erl | 4 ++-- src/emqttd_protocol.erl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqttd_mod_presence.erl b/src/emqttd_mod_presence.erl index 57a02c1bf..1aa382fa2 100644 --- a/src/emqttd_mod_presence.erl +++ b/src/emqttd_mod_presence.erl @@ -52,7 +52,7 @@ client_connected(ConnAck, #mqtt_client{client_id = ClientId, proplists:get_value(qos, Opts, 0), topic(connected, ClientId), iolist_to_binary(Json)), - emqttd_pubsub:publish(Msg). + emqttd:publish(Msg). client_disconnected(Reason, ClientId, Opts) -> Json = mochijson2:encode([{clientid, ClientId}, @@ -62,7 +62,7 @@ client_disconnected(Reason, ClientId, Opts) -> proplists:get_value(qos, Opts, 0), topic(disconnected, ClientId), iolist_to_binary(Json)), - emqttd_pubsub:publish(Msg). + emqttd:publish(Msg). unload(_Opts) -> emqttd_broker:unhook('client.connected', {?MODULE, client_connected}), diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index 536474ce4..6764d201c 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -302,7 +302,7 @@ maybe_set_clientid(State) -> send_willmsg(_ClientId, undefined) -> ignore; send_willmsg(ClientId, WillMsg) -> - emqttd_pubsub:publish(WillMsg#mqtt_message{from = ClientId}). + emqttd:publish(WillMsg#mqtt_message{from = ClientId}). start_keepalive(0) -> ignore; From fb273f0ecae5bad08b421a0fccb270e06b7393ff Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:27:17 +0800 Subject: [PATCH 59/69] disc_copies --- src/emqttd_retainer.erl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/emqttd_retainer.erl b/src/emqttd_retainer.erl index f18050b28..f90223a22 100644 --- a/src/emqttd_retainer.erl +++ b/src/emqttd_retainer.erl @@ -14,8 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% TODO: should match topic tree -%% @doc MQTT retained message storage. +%% @doc MQTT retained message. -module(emqttd_retainer). -behaviour(gen_server). @@ -53,7 +52,7 @@ mnesia(boot) -> ok = emqttd_mnesia:create_table(retained, [ {type, ordered_set}, - {ram_copies, [node()]}, + {disc_copies, [node()]}, {record_name, mqtt_retained}, {attributes, record_info(fields, mqtt_retained)}]); mnesia(copy) -> From 0f1347a495b91004f983aa184b302a74f7a7eb9a Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:28:11 +0800 Subject: [PATCH 60/69] add 'routes' command, improve usage --- src/emqttd_cli.erl | 165 ++++++++++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 70 deletions(-) diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index 11a0b23fd..6bfaf104b 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -28,9 +28,9 @@ -export([load/0]). --export([status/1, broker/1, cluster/1, users/1, bridges/1, - clients/1, sessions/1, topics/1, subscriptions/1, - plugins/1, listeners/1, vm/1, mnesia/1, trace/1]). +-export([status/1, broker/1, cluster/1, users/1, clients/1, sessions/1, + routes/1, topics/1, subscriptions/1, plugins/1, bridges/1, + listeners/1, vm/1, mnesia/1, trace/1]). -define(PROC_INFOKEYS, [status, memory, @@ -67,7 +67,7 @@ status([]) -> ?PRINT("emqttd ~s is running~n", [Vsn]) end; status(_) -> - ?PRINT_CMD("status", "query broker status"). + ?PRINT_CMD("status", "Show broker status"). %%-------------------------------------------------------------------- %% @doc Query broker @@ -98,10 +98,10 @@ broker(["pubsub"]) -> end, lists:reverse(Pubsubs)); broker(_) -> - ?USAGE([{"broker", "query broker version, uptime and description"}, - {"broker pubsub", "query process_info of pubsub"}, - {"broker stats", "query broker statistics of clients, topics, subscribers"}, - {"broker metrics", "query broker metrics"}]). + ?USAGE([{"broker", "Show broker version, uptime and description"}, + {"broker pubsub", "Show process_info of pubsub"}, + {"broker stats", "Show broker statistics of clients, topics, subscribers"}, + {"broker metrics", "Show broker metrics"}]). %%-------------------------------------------------------------------- %% @doc Cluster with other nodes @@ -157,9 +157,9 @@ clients(["kick", ClientId]) -> if_client(ClientId, fun(#mqtt_client{client_pid = Pid}) -> emqttd_client:kick(Pid) end); clients(_) -> - ?USAGE([{"clients list", "list all clients"}, - {"clients show ", "show a client"}, - {"clients kick ", "kick a client"}]). + ?USAGE([{"clients list", "List all clients"}, + {"clients show ", "Show a client"}, + {"clients kick ", "Kick out a client"}]). if_client(ClientId, Fun) -> case emqttd_cm:lookup(bin(ClientId)) of @@ -191,10 +191,23 @@ sessions(["show", ClientId]) -> end; sessions(_) -> - ?USAGE([{"sessions list", "list all sessions"}, - {"sessions list persistent", "list all persistent sessions"}, - {"sessions list transient", "list all transient sessions"}, - {"sessions show ", "show a session"}]). + ?USAGE([{"sessions list", "List all sessions"}, + {"sessions list persistent", "List all persistent sessions"}, + {"sessions list transient", "List all transient sessions"}, + {"sessions show ", "Show a session"}]). + +%%-------------------------------------------------------------------- +%% @doc Routes Command +routes(["list"]) -> + Print = fun(Topic, Records) -> print(route, Topic, Records) end, + if_could_print(route, Print); + +routes(["show", Topic]) -> + print(route, Topic, mnesia:dirty_read(route, bin(Topic))); + +routes(_) -> + ?USAGE([{"routes list", "List all routes"}, + {"routes show ", "Show a route"}]). %%-------------------------------------------------------------------- %% @doc Topics Command @@ -206,48 +219,54 @@ topics(["show", Topic]) -> print(topic, Topic, ets:lookup(topic, bin(Topic))); topics(_) -> - ?USAGE([{"topics list", "list all topics"}, - {"topics show ", "show a topic"}]). + ?USAGE([{"topics list", "List all topics"}, + {"topics show ", "Show a topic"}]). subscriptions(["list"]) -> Print = fun(ClientId, Records) -> print(subscription, ClientId, Records) end, - if_subscription(fun() -> if_could_print(subscription, Print) end); + if_could_print(subscription, Print); + +subscriptions(["list", "static"]) -> + Print = fun(ClientId, Records) -> print(subscription, ClientId, Records) end, + if_could_print(static_subscription, Print); subscriptions(["show", ClientId]) -> - if_subscription(fun() -> - case emqttd_pubsub:lookup(subscription, bin(ClientId)) of - [] -> ?PRINT_MSG("Not Found.~n"); - Records -> print(subscription, ClientId, Records) - end - end); + case mnesia:dirty_read(subscription, bin(ClientId)) of + [] -> ?PRINT_MSG("Not Found.~n"); + Records -> print(subscription, ClientId, Records) + end; subscriptions(["add", ClientId, Topic, QoS]) -> - Create = fun(IntQos) -> - Subscription = {bin(ClientId), bin(Topic), IntQos}, - case emqttd_pubsub:create(subscription, Subscription) of - ok -> ?PRINT_MSG("ok~n"); - {error, Error} -> ?PRINT("Error: ~p~n", [Error]) - end - end, - if_subscription(fun() -> if_valid_qos(QoS, Create) end); + Add = fun(IntQos) -> + Subscription = #mqtt_subscription{subid = bin(ClientId), + topic = bin(Topic), + qos = IntQos}, + case emqttd_backend:add_static_subscription(Subscription) of + {atomic, ok} -> + ?PRINT_MSG("ok~n"); + {aborted, {error, existed}} -> + ?PRINT_MSG("Error: already existed~n"); + {aborted, Reason} -> + ?PRINT("Error: ~p~n", [Reason]) + end + end, + if_valid_qos(QoS, Add); + +subscriptions(["del", ClientId]) -> + Ok = emqttd_backend:del_static_subscriptions(bin(ClientId)), + ?PRINT("~p~n", [Ok]); subscriptions(["del", ClientId, Topic]) -> - if_subscription(fun() -> - Ok = emqttd_pubsub:delete(subscription, {bin(ClientId), bin(Topic)}), - ?PRINT("~p~n", [Ok]) - end); + Ok = emqttd_backend:del_static_subscription(bin(ClientId), bin(Topic)), + ?PRINT("~p~n", [Ok]); subscriptions(_) -> - ?USAGE([{"subscriptions list", "list all subscriptions"}, - {"subscriptions show ", "show subscriptions of a client"}, - {"subscriptions add ", "add subscription"}, - {"subscriptions del ", "delete subscription"}]). - -if_subscription(Fun) -> - case ets:info(subscription, name) of - undefined -> ?PRINT_MSG("Error: subscription table not found!~n"); - _ -> Fun() - end. + ?USAGE([{"subscriptions list", "List all subscriptions"}, + {"subscriptions list static", "List all static subscriptions"}, + {"subscriptions show ", "Show subscriptions of a client"}, + {"subscriptions add ", "Add a static subscription manually"}, + {"subscriptions del ", "Delete static subscriptions manually"}, + {"subscriptions del ", "Delete a static subscription manually"}]). if_could_print(Tab, Fun) -> case mnesia:table_info(Tab, size) of @@ -286,9 +305,9 @@ plugins(["unload", Name]) -> end; plugins(_) -> - ?USAGE([{"plugins list", "show loaded plugins"}, - {"plugins load ", "load plugin"}, - {"plugins unload ", "unload plugin"}]). + ?USAGE([{"plugins list", "Show loaded plugins"}, + {"plugins load ", "Load plugin"}, + {"plugins unload ", "Unload plugin"}]). %%-------------------------------------------------------------------- %% @doc Bridges command @@ -326,11 +345,11 @@ bridges(["stop", SNode, Topic]) -> end; bridges(_) -> - ?USAGE([{"bridges list", "query bridges"}, - {"bridges options", "bridge options"}, - {"bridges start ", "start bridge"}, - {"bridges start ", "start bridge with options"}, - {"bridges stop ", "stop bridge"}]). + ?USAGE([{"bridges list", "List bridges"}, + {"bridges options", "Bridge options"}, + {"bridges start ", "Start a bridge"}, + {"bridges start ", "Start a bridge with options"}, + {"bridges stop ", "Stop a bridge"}]). parse_opts(Cmd, OptStr) -> Tokens = string:tokens(OptStr, ","), @@ -373,11 +392,11 @@ vm(["io"]) -> end, [max_fds, active_fds]); vm(_) -> - ?USAGE([{"vm all", "query info of erlang vm"}, - {"vm load", "query load of erlang vm"}, - {"vm memory", "query memory of erlang vm"}, - {"vm process", "query process of erlang vm"}, - {"vm io", "queue io of erlang vm"}]). + ?USAGE([{"vm all", "Show info of erlang vm"}, + {"vm load", "Show load of erlang vm"}, + {"vm memory", "Show memory of erlang vm"}, + {"vm process", "Show process of erlang vm"}, + {"vm io", "Show IO of erlang vm"}]). %%-------------------------------------------------------------------- %% @doc mnesia Command @@ -385,7 +404,7 @@ mnesia([]) -> mnesia:system_info(); mnesia(_) -> - ?PRINT_CMD("mnesia", "mnesia system info"). + ?PRINT_CMD("mnesia", "Mnesia system info"). %%-------------------------------------------------------------------- %% @doc Trace Command @@ -407,11 +426,11 @@ trace(["topic", Topic, LogFile]) -> trace_on(topic, Topic, LogFile); trace(_) -> - ?USAGE([{"trace list", "query all traces"}, - {"trace client ","trace client with ClientId"}, - {"trace client off", "stop tracing client"}, - {"trace topic ", "trace topic with Topic"}, - {"trace topic off", "stop tracing Topic"}]). + ?USAGE([{"trace list", "List all traces"}, + {"trace client ","Trace a client"}, + {"trace client off", "Stop tracing a client"}, + {"trace topic ", "Trace a topic"}, + {"trace topic off", "Stop tracing a Topic"}]). trace_on(Who, Name, LogFile) -> case emqttd_trace:start_trace({Who, iolist_to_binary(Name)}, LogFile) of @@ -444,7 +463,7 @@ listeners([]) -> end, esockd:listeners()); listeners(_) -> - ?PRINT_CMD("listeners", "query broker listeners"). + ?PRINT_CMD("listeners", "List listeners"). print(#mqtt_plugin{name = Name, version = Ver, descr = Descr, active = Active}) -> ?PRINT("Plugin(~s, version=~s, description=~s, active=~s)~n", @@ -458,8 +477,11 @@ print(#mqtt_client{client_id = ClientId, clean_sess = CleanSess, emqttd_net:format(Peername), emqttd_time:now_to_secs(ConnectedAt)]); -print(#mqtt_topic{topic = Topic, node = Node}) -> - ?PRINT("~s on ~s~n", [Topic, Node]); +print(#mqtt_topic{topic = Topic, flags = Flags}) -> + ?PRINT("~s: ~p~n", [Topic, Flags]); + +print(#mqtt_route{topic = Topic, node = Node}) -> + ?PRINT("~s: ~s~n", [Topic, Node]); print({{ClientId, _ClientPid}, SessInfo}) -> InfoKeys = [clean_sess, @@ -477,10 +499,13 @@ print({{ClientId, _ClientPid}, SessInfo}) -> "created_at=~w)~n", [ClientId | [format(Key, proplists:get_value(Key, SessInfo)) || Key <- InfoKeys]]). -print(topic, Topic, Records) -> - Nodes = [Node || #mqtt_topic{node = Node} <- Records], +print(route, Topic, Routes) -> + Nodes = [Node || #mqtt_route{node = Node} <- Routes], ?PRINT("~s: ~p~n", [Topic, Nodes]); +print(topic, _Topic, Records) -> + [print(R) || R <- Records]; + print(subscription, ClientId, Subscriptions) -> TopicTable = [{Topic, Qos} || #mqtt_subscription{topic = Topic, qos = Qos} <- Subscriptions], ?PRINT("~s: ~p~n", [ClientId, TopicTable]). From f1f58818d5261171d9e8a6b641df52df4e0abbdd Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:50:41 +0800 Subject: [PATCH 61/69] backend_subscription --- src/emqttd_backend.erl | 44 ++++++++++++++++----------------- src/emqttd_cli.erl | 8 +++--- src/emqttd_mod_subscription.erl | 14 +++++------ 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/emqttd_backend.erl b/src/emqttd_backend.erl index 39c1a8c2d..338a6097f 100644 --- a/src/emqttd_backend.erl +++ b/src/emqttd_backend.erl @@ -25,15 +25,15 @@ -copy_mnesia({mnesia, [copy]}). %% API. --export([add_static_subscription/1, lookup_static_subscriptions/1, - del_static_subscriptions/1, del_static_subscription/2]). +-export([add_subscription/1, lookup_subscriptions/1, del_subscriptions/1, + del_subscription/2]). %%-------------------------------------------------------------------- %% Mnesia callbacks %%-------------------------------------------------------------------- mnesia(boot) -> - ok = emqttd_mnesia:create_table(static_subscription, [ + ok = emqttd_mnesia:create_table(backend_subscription, [ {type, bag}, {disc_copies, [node()]}, {record_name, mqtt_subscription}, @@ -42,48 +42,48 @@ mnesia(boot) -> {dets, [{auto_save, 5000}]}]}]); mnesia(copy) -> - ok = emqttd_mnesia:copy_table(static_subscription). + ok = emqttd_mnesia:copy_table(backend_subscription). %%-------------------------------------------------------------------- %% Static Subscriptions %%-------------------------------------------------------------------- %% @doc Add a static subscription manually. --spec add_static_subscription(mqtt_subscription()) -> {atom, ok}. -add_static_subscription(Subscription = #mqtt_subscription{subid = SubId, topic = Topic}) -> +-spec add_subscription(mqtt_subscription()) -> {atom, ok}. +add_subscription(Subscription = #mqtt_subscription{subid = SubId, topic = Topic}) -> Pattern = match_pattern(SubId, Topic), mnesia:transaction( fun() -> - case mnesia:match_object(static_subscription, Pattern, write) of + case mnesia:match_object(backend_subscription, Pattern, write) of [] -> - mnesia:write(static_subscription, Subscription, write); + mnesia:write(backend_subscription, Subscription, write); [Subscription] -> mnesia:abort({error, existed}); [Subscription1] -> %% QoS is different - mnesia:delete_object(static_subscription, Subscription1, write), - mnesia:write(static_subscription, Subscription, write) + mnesia:delete_object(backend_subscription, Subscription1, write), + mnesia:write(backend_subscription, Subscription, write) end end). %% @doc Lookup static subscriptions. --spec lookup_static_subscriptions(binary()) -> list(mqtt_subscription()). -lookup_static_subscriptions(ClientId) when is_binary(ClientId) -> - mnesia:dirty_read(static_subscription, ClientId). +-spec lookup_subscriptions(binary()) -> list(mqtt_subscription()). +lookup_subscriptions(ClientId) when is_binary(ClientId) -> + mnesia:dirty_read(backend_subscription, ClientId). %% @doc Delete static subscriptions by ClientId manually. --spec del_static_subscriptions(binary()) -> ok. -del_static_subscriptions(ClientId) when is_binary(ClientId) -> - mnesia:transaction(fun mnesia:delete/1, [{static_subscription, ClientId}]). +-spec del_subscriptions(binary()) -> ok. +del_subscriptions(ClientId) when is_binary(ClientId) -> + mnesia:transaction(fun mnesia:delete/1, [{backend_subscription, ClientId}]). %% @doc Delete a static subscription manually. --spec del_static_subscription(binary(), binary()) -> ok. -del_static_subscription(ClientId, Topic) when is_binary(ClientId) andalso is_binary(Topic) -> - mnesia:transaction(fun del_static_subscription_/1, [match_pattern(ClientId, Topic)]). +-spec del_subscription(binary(), binary()) -> ok. +del_subscription(ClientId, Topic) when is_binary(ClientId) andalso is_binary(Topic) -> + mnesia:transaction(fun del_subscription_/1, [match_pattern(ClientId, Topic)]). -del_static_subscription_(Pattern) -> +del_subscription_(Pattern) -> lists:foreach(fun(Subscription) -> - mnesia:delete_object(static_subscription, Subscription, write) - end, mnesia:match_object(static_subscription, Pattern, write)). + mnesia:delete_object(backend_subscription, Subscription, write) + end, mnesia:match_object(backend_subscription, Pattern, write)). match_pattern(SubId, Topic) -> #mqtt_subscription{subid = SubId, topic = Topic, qos = '_'}. diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index 6bfaf104b..17225ac4a 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -228,7 +228,7 @@ subscriptions(["list"]) -> subscriptions(["list", "static"]) -> Print = fun(ClientId, Records) -> print(subscription, ClientId, Records) end, - if_could_print(static_subscription, Print); + if_could_print(backend_subscription, Print); subscriptions(["show", ClientId]) -> case mnesia:dirty_read(subscription, bin(ClientId)) of @@ -241,7 +241,7 @@ subscriptions(["add", ClientId, Topic, QoS]) -> Subscription = #mqtt_subscription{subid = bin(ClientId), topic = bin(Topic), qos = IntQos}, - case emqttd_backend:add_static_subscription(Subscription) of + case emqttd_backend:add_subscription(Subscription) of {atomic, ok} -> ?PRINT_MSG("ok~n"); {aborted, {error, existed}} -> @@ -253,11 +253,11 @@ subscriptions(["add", ClientId, Topic, QoS]) -> if_valid_qos(QoS, Add); subscriptions(["del", ClientId]) -> - Ok = emqttd_backend:del_static_subscriptions(bin(ClientId)), + Ok = emqttd_backend:del_subscriptions(bin(ClientId)), ?PRINT("~p~n", [Ok]); subscriptions(["del", ClientId, Topic]) -> - Ok = emqttd_backend:del_static_subscription(bin(ClientId), bin(Topic)), + Ok = emqttd_backend:del_subscription(bin(ClientId), bin(Topic)), ?PRINT("~p~n", [Ok]); subscriptions(_) -> diff --git a/src/emqttd_mod_subscription.erl b/src/emqttd_mod_subscription.erl index 098fc592d..b8d31b436 100644 --- a/src/emqttd_mod_subscription.erl +++ b/src/emqttd_mod_subscription.erl @@ -25,11 +25,11 @@ -export([load/1, client_connected/3, unload/1]). --record(state, {topics, stored = false}). +-record(state, {topics, backend = false}). load(Opts) -> Topics = [{iolist_to_binary(Topic), QoS} || {Topic, QoS} <- Opts, ?IS_QOS(QoS)], - State = #state{topics = Topics, stored = lists:member(stored, Opts)}, + State = #state{topics = Topics, backend = lists:member(backend, Opts)}, emqttd_broker:hook('client.connected', {?MODULE, client_connected}, {?MODULE, client_connected, [State]}), ok. @@ -37,18 +37,18 @@ load(Opts) -> client_connected(?CONNACK_ACCEPT, #mqtt_client{client_id = ClientId, client_pid = ClientPid, username = Username}, - #state{topics = Topics, stored = Stored}) -> + #state{topics = Topics, backend = Backend}) -> Replace = fun(Topic) -> rep(<<"$u">>, Username, rep(<<"$c">>, ClientId, Topic)) end, - TopicTable = with_stored(Stored, ClientId, [{Replace(Topic), Qos} || {Topic, Qos} <- Topics]), + TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- with_backend(Backend, ClientId, Topics)], emqttd_client:subscribe(ClientPid, TopicTable); client_connected(_ConnAck, _Client, _State) -> ok. -with_stored(false, _ClientId, TopicTable) -> +with_backend(false, _ClientId, TopicTable) -> TopicTable; -with_stored(true, ClientId, TopicTable) -> +with_backend(true, ClientId, TopicTable) -> Fun = fun(#mqtt_subscription{topic = Topic, qos = Qos}) -> {Topic, Qos} end, - emqttd_opts:merge([Fun(Sub) || Sub <- emqttd_pubsub:lookup(subscription, ClientId)], TopicTable). + emqttd_opts:merge([Fun(Sub) || Sub <- emqttd_backend:lookup_subscriptions(ClientId)], TopicTable). unload(_Opts) -> emqttd_broker:unhook('client.connected', {?MODULE, client_connected}). From 32635af0845d8a58009be3d9e75accdf7ea5172a Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 8 Mar 2016 13:51:21 +0800 Subject: [PATCH 62/69] backend subscription --- rel/files/emqttd.config.development | 13 ++----------- rel/files/emqttd.config.production | 13 ++----------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/rel/files/emqttd.config.development b/rel/files/emqttd.config.development index 93bacd972..9fce5b427 100644 --- a/rel/files/emqttd.config.development +++ b/rel/files/emqttd.config.development @@ -151,15 +151,6 @@ {pubsub, [ %% Default should be scheduler numbers {pool_size, 8}, - - %% Subscription: disc | ram | false - {subscription, ram}, - - %% Route shard - {route_shard, false}, - - %% Route delay, false | integer - {route_delay, false}, %% Route aging time(seconds) {route_aging, 5} @@ -182,8 +173,8 @@ %% Subscribe topics automatically when client connected {subscription, [ - %% Subscription from stored table - stored, + %% Static subscriptions from backend + backend, %% $u will be replaced with username {"$Q/username/$u", 1}, diff --git a/rel/files/emqttd.config.production b/rel/files/emqttd.config.production index 755a6b1dc..095c66550 100644 --- a/rel/files/emqttd.config.production +++ b/rel/files/emqttd.config.production @@ -146,15 +146,6 @@ {pubsub, [ %% Default should be scheduler numbers {pool_size, 8}, - - %% Subscription: disc | ram | false - {subscription, ram}, - - %% Route shard - {route_shard, false}, - - %% Route delay, false | integer - {route_delay, false}, %% Route aging time(seconds) {route_aging, 5} @@ -177,8 +168,8 @@ %% Subscribe topics automatically when client connected %% {subscription, [ - %% %% Subscription from stored table - %% stored, + %% %% Static subscriptions from backend + %% backend, %% %% %% $u will be replaced with username %% {"$Q/username/$u", 1}, From faf05eb85a9d5718a908ba67cb6d4de2975d87ea Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 11 Mar 2016 23:19:17 +0800 Subject: [PATCH 63/69] test cases for server, pubsub and router --- rel/files/{test.config => emqttd.test.config} | 0 src/emqttd_pubsub.erl | 9 +- src/emqttd_topic.erl | 6 +- test/emqttd_SUITE.erl | 221 +++++++++++------- test/emqttd_access_SUITE.erl | 1 - test/emqttd_protocol_SUITE.erl | 3 +- test/emqttd_topic_SUITE.erl | 5 +- 7 files changed, 152 insertions(+), 93 deletions(-) rename rel/files/{test.config => emqttd.test.config} (100%) diff --git a/rel/files/test.config b/rel/files/emqttd.test.config similarity index 100% rename from rel/files/test.config rename to rel/files/emqttd.test.config diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index de1cf245c..03ca4907f 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -68,7 +68,7 @@ start_link(Pool, Id, Env) -> gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). %% @doc Create a Topic. --spec create_topic(emqttd_topic:topic()) -> ok | {error, any()}. +-spec create_topic(binary()) -> ok | {error, any()}. create_topic(Topic) when is_binary(Topic) -> case mnesia:transaction(fun add_topic_/2, [Topic, [static]]) of {atomic, ok} -> ok; @@ -76,7 +76,7 @@ create_topic(Topic) when is_binary(Topic) -> end. %% @doc Lookup a Topic. --spec lookup_topic(emqttd_topic:topic()) -> list(mqtt_topic()). +-spec lookup_topic(binary()) -> list(mqtt_topic()). lookup_topic(Topic) when is_binary(Topic) -> mnesia:dirty_read(topic, Topic). @@ -106,7 +106,7 @@ publish(Topic, Msg) -> %% @doc Dispatch Message to Subscribers -spec dispatch(binary(), mqtt_message()) -> ok. -dispatch(Queue = <<"$Q/", _Q>>, Msg) -> +dispatch(Queue = <<"$queue/", _T>>, Msg) -> case subscribers(Queue) of [] -> dropped(Queue); @@ -163,7 +163,8 @@ call(PubSub, Req) when is_pid(PubSub) -> cast(PubSub, Msg) when is_pid(PubSub) -> gen_server2:cast(PubSub, Msg). -pick(Topic) -> gproc_pool:pick_worker(pubsub, Topic). +pick(Topic) -> + gproc_pool:pick_worker(pubsub, Topic). %%-------------------------------------------------------------------- %% gen_server Callbacks diff --git a/src/emqttd_topic.erl b/src/emqttd_topic.erl index f3c1f1a33..7e0d0b6ac 100644 --- a/src/emqttd_topic.erl +++ b/src/emqttd_topic.erl @@ -139,11 +139,9 @@ word(<<"+">>) -> '+'; word(<<"#">>) -> '#'; word(Bin) -> Bin. -%% @doc Queue is a special topic name that starts with "$Q/" +%% @doc Queue is a special topic name that starts with "$queue/" -spec is_queue(topic()) -> boolean(). -is_queue(<<"$Q/", _Queue/binary>>) -> - true; -is_queue(<<"$q/", _Queue/binary>>) -> +is_queue(<<"$queue/", _Queue/binary>>) -> true; is_queue(_) -> false. diff --git a/test/emqttd_SUITE.erl b/test/emqttd_SUITE.erl index 6eafdd2dd..bd6913a69 100644 --- a/test/emqttd_SUITE.erl +++ b/test/emqttd_SUITE.erl @@ -28,6 +28,7 @@ all() -> {group, broker}, {group, metrics}, {group, stats}, + {group, hook}, {group, cli}]. groups() -> @@ -35,11 +36,12 @@ groups() -> [create_topic, create_subscription, subscribe_unsubscribe, - publish_message]}, + publish, pubsub, + 'pubsub#', 'pubsub+']}, {router, [sequence], - [add_delete_routes, - add_delete_route, - route_message]}, + [router_add_del, + router_print, + router_unused]}, {session, [sequence], [start_session]}, {retainer, [sequence], @@ -50,12 +52,16 @@ groups() -> [inc_dec_metric]}, {stats, [sequence], [set_get_stat]}, + {hook, [sequence], + [add_delete_hook, + run_hooks]}, {cli, [sequence], [ctl_register_cmd, cli_status, cli_broker, cli_clients, cli_sessions, + cli_routes, cli_topics, cli_subscriptions, cli_bridges, @@ -74,98 +80,107 @@ end_per_suite(_Config) -> emqttd_mnesia:ensure_stopped(). %%-------------------------------------------------------------------- -%% PubSub Group +%% PubSub Test %%-------------------------------------------------------------------- create_topic(_) -> - Node = node(), - ok = emqttd_pubsub:create(topic, <<"topic/create">>), - ok = emqttd_pubsub:create(topic, <<"topic/create2">>), - [#mqtt_topic{topic = <<"topic/create">>, node = Node}] - = emqttd_pubsub:lookup(topic, <<"topic/create">>). + ok = emqttd:create(topic, <<"topic/create">>), + ok = emqttd:create(topic, <<"topic/create2">>), + [#mqtt_topic{topic = <<"topic/create">>, flags = [static]}] + = emqttd:lookup(topic, <<"topic/create">>). create_subscription(_) -> - ok = emqttd_pubsub:create(subscription, {<<"clientId">>, <<"topic/sub">>, qos2}), + ok = emqttd:create(subscription, {<<"clientId">>, <<"topic/sub">>, qos2}), [#mqtt_subscription{subid = <<"clientId">>, topic = <<"topic/sub">>, qos = 2}] - = emqttd_pubsub:lookup(subscription, <<"clientId">>), - ok = emqttd_pubsub:delete(subscription, <<"clientId">>), - [] = emqttd_pubsub:lookup(subscription, <<"clientId">>). + = emqttd_backend:lookup_subscriptions(<<"clientId">>), + ok = emqttd_backend:del_subscriptions(<<"clientId">>), + [] = emqttd_backend:lookup_subscriptions(<<"clientId">>). subscribe_unsubscribe(_) -> - {ok, [1]} = emqttd_pubsub:subscribe({<<"topic/subunsub">>, 1}), - {ok, [1, 2]} = emqttd_pubsub:subscribe([{<<"topic/subunsub1">>, 1}, {<<"topic/subunsub2">>, 2}]), - ok = emqttd_pubsub:unsubscribe(<<"topic/subunsub">>), - ok = emqttd_pubsub:unsubscribe([<<"topic/subunsub1">>, <<"topic/subunsub2">>]), + ok = emqttd:subscribe(<<"topic/subunsub">>), + ok = emqttd:subscribe(<<"clientId">>, <<"topic/subunsub1">>, 1), + ok = emqttd:subscribe(<<"clientId">>, <<"topic/subunsub2">>, 2), + ok = emqttd:unsubscribe(<<"topic/subunsub">>), + ok = emqttd:unsubscribe(<<"clientId">>, <<"topic/subunsub1">>, 1), + ok = emqttd:unsubscribe(<<"clientId">>, <<"topic/subunsub2">>, 2). - {ok, [1]} = emqttd_pubsub:subscribe(<<"client_subunsub">>, {<<"topic/subunsub">>, 1}), - {ok, [1,2]} = emqttd_pubsub:subscribe(<<"client_subunsub">>, [{<<"topic/subunsub1">>, 1}, - {<<"topic/subunsub2">>, 2}]), - ok = emqttd_pubsub:unsubscribe(<<"client_subunsub">>, <<"topic/subunsub">>), - ok = emqttd_pubsub:unsubscribe(<<"client_subunsub">>, [<<"topic/subunsub1">>, - <<"topic/subunsub2">>]). - -publish_message(_) -> +publish(_) -> Msg = emqttd_message:make(ct, <<"test/pubsub">>, <<"hello">>), - {ok, [1]} = emqttd_pubsub:subscribe({<<"test/+">>, qos1}), - emqttd_pubsub:publish(Msg), + ok = emqttd:subscribe(<<"test/+">>), + emqttd:publish(Msg), true = receive {dispatch, <<"test/+">>, Msg} -> true after 5 -> false end. +pubsub(_) -> + Self = self(), + emqttd:subscribe({<<"clientId">>, <<"a/b/c">>, 1}), + emqttd:subscribe({<<"clientId">>, <<"a/b/c">>, 2}), + [{Self, <<"a/b/c">>}] = ets:lookup(subscribed, Self), + [{<<"a/b/c">>, Self}] = ets:lookup(subscriber, <<"a/b/c">>), + emqttd:publish(emqttd_message:make(ct, <<"a/b/c">>, <<"hello">>)), + true = receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end, + spawn(fun() -> + emqttd:subscribe(<<"a/b/c">>), + emqttd:subscribe(<<"c/d/e">>), + timer:sleep(10), + emqttd:unsubscribe(<<"a/b/c">>) + end), + timer:sleep(20), + emqttd:unsubscribe(<<"a/b/c">>). + +'pubsub#'(_) -> + emqttd:subscribe(<<"a/#">>), + emqttd:publish(emqttd_message:make(ct, <<"a/b/c">>, <<"hello">>)), + true = receive {dispatch, <<"a/#">>, _} -> true after 2 -> false end, + emqttd:unsubscribe(<<"a/#">>). + +'pubsub+'(_) -> + emqttd:subscribe(<<"a/+/+">>), + emqttd:publish(emqttd_message:make(ct, <<"a/b/c">>, <<"hello">>)), + true = receive {dispatch, <<"a/+/+">>, _} -> true after 1 -> false end, + emqttd:unsubscribe(<<"a/+/+">>). + %%-------------------------------------------------------------------- -%% Route Group +%% Router Test %%-------------------------------------------------------------------- -add_delete_route(_) -> - Self = self(), - emqttd_router:add_route(<<"topic1">>, Self), - true = emqttd_router:has_route(<<"topic1">>), - emqttd_router:add_route(<<"topic2">>, Self), - true = emqttd_router:has_route(<<"topic2">>), - [Self] = emqttd_router:lookup_routes(<<"topic1">>), - [Self] = emqttd_router:lookup_routes(<<"topic2">>), - %% Del topic1 - emqttd_router:delete_route(<<"topic1">>, Self), - erlang:yield(), - timer:sleep(10), - false = emqttd_router:has_route(<<"topic1">>), - %% Del topic2 - emqttd_router:delete_route(<<"topic2">>, Self), - erlang:yield(), - timer:sleep(10), - false = emqttd_router:has_route(<<"topic2">>). +router_add_del(_) -> + %% Add + emqttd_router:add_route(<<"#">>), + emqttd_router:add_route(<<"a/b/c">>), + emqttd_router:add_route(<<"+/#">>, node()), + Routes = [R1, R2 | _] = [ + #mqtt_route{topic = <<"#">>, node = node()}, + #mqtt_route{topic = <<"+/#">>, node = node()}, + #mqtt_route{topic = <<"a/b/c">>, node = node()}], + Routes = lists:sort(emqttd_router:lookup(<<"a/b/c">>)), -add_delete_routes(_) -> - Self = self(), - emqttd_router:add_routes([], Self), - emqttd_router:add_routes([<<"t0">>], Self), - emqttd_router:add_routes([<<"t1">>,<<"t2">>,<<"t3">>], Self), - true = emqttd_router:has_route(<<"t1">>), - [Self] = emqttd_router:lookup_routes(<<"t1">>), - [Self] = emqttd_router:lookup_routes(<<"t2">>), - [Self] = emqttd_router:lookup_routes(<<"t3">>), + %% Batch Add + emqttd_router:add_routes(Routes), + Routes = lists:sort(emqttd_router:lookup(<<"a/b/c">>)), - emqttd_router:delete_routes([<<"t3">>], Self), - emqttd_router:delete_routes([<<"t0">>, <<"t1">>], Self), - erlang:yield(), - timer:sleep(10), - false = emqttd_router:has_route(<<"t0">>), - false = emqttd_router:has_route(<<"t1">>), - true = emqttd_router:has_route(<<"t2">>), - false = emqttd_router:has_route(<<"t3">>). + %% Del + emqttd_router:del_route(<<"a/b/c">>), + [R1, R2] = lists:sort(emqttd_router:lookup(<<"a/b/c">>)), + {atomic, []} = mnesia:transaction(fun emqttd_trie:lookup/1, [<<"a/b/c">>]), -route_message(_) -> - Self = self(), - Pid = spawn_link(fun() -> timer:sleep(1000) end), - emqttd_router:add_routes([<<"$Q/1">>,<<"t/2">>,<<"t/3">>], Self), - emqttd_router:add_routes([<<"t/2">>], Pid), - Msg1 = #mqtt_message{topic = <<"$Q/1">>, payload = <<"q">>}, - Msg2 = #mqtt_message{topic = <<"t/2">>, payload = <<"t2">>}, - Msg3 = #mqtt_message{topic = <<"t/3">>, payload = <<"t3">>}, - emqttd_router:route(<<"$Q/1">>, Msg1), - emqttd_router:route(<<"t/2">>, Msg2), - emqttd_router:route(<<"t/3">>, Msg3), - [Msg1, Msg2, Msg3] = recv_loop([]), - emqttd_router:add_route(<<"$Q/1">>, Self), - emqttd_router:route(<<"$Q/1">>, Msg1). + %% Batch Del + R3 = #mqtt_route{topic = <<"#">>, node = 'a@127.0.0.1'}, + emqttd_router:add_route(R3), + emqttd_router:del_routes([R1, R2]), + emqttd_router:del_route(R3), + [] = lists:sort(emqttd_router:lookup(<<"a/b/c">>)). + +router_print(_) -> + Routes = [#mqtt_route{topic = <<"a/b/c">>, node = node()}, + #mqtt_route{topic = <<"#">>, node = node()}, + #mqtt_route{topic = <<"+/#">>, node = node()}], + emqttd_router:add_routes(Routes), + emqttd_router:print(<<"a/b/c">>). + +router_unused(_) -> + gen_server:call(emqttd_router, bad_call), + gen_server:cast(emqttd_router, bad_msg), + emqttd_router ! bad_info. recv_loop(Msgs) -> receive @@ -225,6 +240,46 @@ set_get_stat(_) -> emqttd_stats:setstat('retained/max', 99), 99 = emqttd_stats:getstat('retained/max'). +%%-------------------------------------------------------------------- +%% Hook Test +%%-------------------------------------------------------------------- + +add_delete_hook(_) -> + emqttd:hook(test_hook, fun ?MODULE:hook_fun1/1, []), + emqttd:hook(test_hook, fun ?MODULE:hook_fun2/1, []), + {error, already_hooked} = emqttd:hook(test_hook, fun ?MODULE:hook_fun2/1, []), + Callbacks = [{callback, fun ?MODULE:hook_fun1/1, [], 0}, + {callback, fun ?MODULE:hook_fun2/1, [], 0}], + Callbacks = emqttd_hook:lookup(test_hook), + emqttd:unhook(test_hook, fun ?MODULE:hook_fun1/1), + emqttd:unhook(test_hook, fun ?MODULE:hook_fun2/1), + ok = emqttd:unhook(test_hook, fun ?MODULE:hook_fun2/1), + {error, not_found} = emqttd:unhook(test_hook1, fun ?MODULE:hook_fun2/1), + [] = emqttd_hook:lookup(test_hook), + + emqttd:hook(emqttd_hook, fun ?MODULE:hook_fun1/1, [], 9), + emqttd:hook(emqttd_hook, fun ?MODULE:hook_fun2/1, [], 8), + Callbacks2 = [{callback, fun ?MODULE:hook_fun2/1, [], 8}, + {callback, fun ?MODULE:hook_fun1/1, [], 9}], + Callbacks2 = emqttd_hook:lookup(emqttd_hook), + emqttd:unhook(emqttd_hook, fun ?MODULE:hook_fun1/1), + emqttd:unhook(emqttd_hook, fun ?MODULE:hook_fun2/1), + [] = emqttd_hook:lookup(emqttd_hook). + +run_hooks(_) -> + emqttd:hook(test_hook, fun ?MODULE:hook_fun3/4, [init]), + emqttd:hook(test_hook, fun ?MODULE:hook_fun4/4, [init]), + emqttd:hook(test_hook, fun ?MODULE:hook_fun5/4, [init]), + {stop, [r3, r2]} = emqttd:run_hooks(test_hook, [arg1, arg2], []), + {ok, []} = emqttd:run_hooks(unknown_hook, [], []). + +hook_fun1([]) -> ok. +hook_fun2([]) -> {ok, []}. + +hook_fun3(arg1, arg2, _Acc, init) -> ok. +hook_fun4(arg1, arg2, Acc, init) -> {ok, [r2 | Acc]}. +hook_fun5(arg1, arg2, Acc, init) -> {stop, [r3 | Acc]}. + %%-------------------------------------------------------------------- %% CLI Group %%-------------------------------------------------------------------- @@ -263,9 +318,17 @@ cli_sessions(_) -> emqttd_cli:sessions(["list", "transient"]), emqttd_cli:sessions(["show", "clientId"]). +cli_routes(_) -> + emqttd:subscribe(<<"topic/route">>), + emqttd_cli:routes(["list"]), + emqttd_cli:routes(["show", "topic/route"]), + emqttd:unsubscribe(<<"topic/route">>). + cli_topics(_) -> + emqttd:subscribe(<<"topic">>), emqttd_cli:topics(["list"]), - emqttd_cli:topics(["show", "topic"]). + emqttd_cli:topics(["show", "topic"]), + emqttd:unsubscribe(<<"topic">>). cli_subscriptions(_) -> emqttd_cli:subscriptions(["list"]), diff --git a/test/emqttd_access_SUITE.erl b/test/emqttd_access_SUITE.erl index 3a58d031c..c12fd00bd 100644 --- a/test/emqttd_access_SUITE.erl +++ b/test/emqttd_access_SUITE.erl @@ -74,7 +74,6 @@ end_per_testcase(_TestCase, _Config) -> %%-------------------------------------------------------------------- reload_acl(_) -> - ct:print("~p~n", [whereis(?AC)]), [ok] = ?AC:reload_acl(). register_mod(_) -> diff --git a/test/emqttd_protocol_SUITE.erl b/test/emqttd_protocol_SUITE.erl index f5f63a185..18984591d 100644 --- a/test/emqttd_protocol_SUITE.erl +++ b/test/emqttd_protocol_SUITE.erl @@ -132,9 +132,8 @@ parse_bridge(_) -> %% CONNECT(Q0, R0, D0, ClientId=C_00:0C:29:2B:77:52, ProtoName=MQIsdp, ProtoVsn=131, CleanSess=false, KeepAlive=60, %% Username=undefined, Password=undefined, Will(Q1, R1, Topic=$SYS/broker/connection/C_00:0C:29:2B:77:52/state, Msg=0)) {ok, #mqtt_packet{variable = Variable}, <<>>} = Parser(Data), - ct:print("~p", [Variable]), #mqtt_packet_connect{client_id = <<"C_00:0C:29:2B:77:52">>, - proto_ver = 16#83, + proto_ver = 16#03, proto_name = <<"MQIsdp">>, will_retain = true, will_qos = 1, diff --git a/test/emqttd_topic_SUITE.erl b/test/emqttd_topic_SUITE.erl index aa79fffa1..abcf50cf9 100644 --- a/test/emqttd_topic_SUITE.erl +++ b/test/emqttd_topic_SUITE.erl @@ -156,8 +156,7 @@ t_join(_) -> <<"ab/+/#">> = join(words(<<"ab/+/#">>)). t_is_queue(_) -> - true = is_queue(<<"$Q/queue">>), - true = is_queue(<<"$q/queue">>), + true = is_queue(<<"$queue/queue">>), false = is_queue(<<"xyz/queue">>). t_systop(_) -> @@ -167,7 +166,7 @@ t_systop(_) -> SysTop2 = systop(<<"abc">>). t_feed_var(_) -> - <<"$Q/client/clientId">> = feed_var(<<"$c">>, <<"clientId">>, <<"$Q/client/$c">>), + <<"$queue/client/clientId">> = feed_var(<<"$c">>, <<"clientId">>, <<"$queue/client/$c">>), <<"username/test/client/x">> = feed_var(<<"%u">>, <<"test">>, <<"username/%u/client/x">>), <<"username/test/client/clientId">> = feed_var(<<"%c">>, <<"clientId">>, <<"username/test/client/%c">>). From d9d7581013e9149b90f045d274fcebff58d90d30 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 11 Mar 2016 23:42:37 +0800 Subject: [PATCH 64/69] 0.17.0 - Improve the design of Hook, PubSub and Router --- src/emqttd.erl | 69 +++++++++++--- src/emqttd_app.erl | 1 + src/emqttd_broker.erl | 65 -------------- src/emqttd_cli.erl | 110 +++++++++++------------ src/emqttd_hook.erl | 155 ++++++++++++++++++++++++++++++++ src/emqttd_mod_presence.erl | 56 ++++++------ src/emqttd_mod_rewrite.erl | 36 ++++---- src/emqttd_mod_subscription.erl | 23 ++--- src/emqttd_protocol.erl | 8 +- src/emqttd_pubsub.erl | 24 ++--- src/emqttd_router.erl | 16 ++-- src/emqttd_server.erl | 55 +++++++----- src/emqttd_session.erl | 89 ++++++++++-------- 13 files changed, 425 insertions(+), 282 deletions(-) create mode 100644 src/emqttd_hook.erl diff --git a/src/emqttd.erl b/src/emqttd.erl index 9a740e1cd..9bc455fbd 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -22,9 +22,13 @@ -export([start/0, env/1, env/2, is_running/1]). --export([create/2, publish/1, subscribe/1, subscribe/3, +%% PubSub API +-export([create/2, lookup/2, publish/1, subscribe/1, subscribe/3, unsubscribe/1, unsubscribe/3]). +%% Hooks API +-export([hook/4, hook/3, unhook/2, run_hooks/3]). + -define(APP, ?MODULE). %%-------------------------------------------------------------------- @@ -32,19 +36,19 @@ %%-------------------------------------------------------------------- %% @doc Start emqttd application. --spec start() -> ok | {error, any()}. +-spec(start() -> ok | {error, any()}). start() -> application:start(?APP). %% @doc Group environment --spec env(Group :: atom()) -> list(). +-spec(env(Group :: atom()) -> list()). env(Group) -> application:get_env(?APP, Group, []). %% @doc Get environment --spec env(Group :: atom(), Name :: atom()) -> undefined | any(). +-spec(env(Group :: atom(), Name :: atom()) -> undefined | any()). env(Group, Name) -> proplists:get_value(Name, env(Group)). %% @doc Is running? --spec is_running(node()) -> boolean(). +-spec(is_running(node()) -> boolean()). is_running(Node) -> case rpc:call(Node, erlang, whereis, [?APP]) of {badrpc, _} -> false; @@ -56,30 +60,67 @@ is_running(Node) -> %% PubSub APIs that wrap emqttd_server, emqttd_pubsub %%-------------------------------------------------------------------- -%% @doc Create a Topic +%% @doc Lookup Topic or Subscription +-spec(lookup(topic, binary()) -> [mqtt_topic()]; + (subscription, binary()) -> [mqtt_subscription()]). +lookup(topic, Topic) when is_binary(Topic) -> + emqttd_pubsub:lookup_topic(Topic); + +lookup(subscription, ClientId) when is_binary(ClientId) -> + emqttd_server:lookup_subscription(ClientId). + +%% @doc Create a Topic or Subscription +-spec(create(topic | subscription, binary()) -> ok | {error, any()}). create(topic, Topic) when is_binary(Topic) -> - emqttd_pubsub:create_topic(Topic). + emqttd_pubsub:create_topic(Topic); + +create(subscription, {ClientId, Topic, Qos}) -> + Subscription = #mqtt_subscription{subid = ClientId, topic = Topic, qos = ?QOS_I(Qos)}, + emqttd_backend:add_subscription(Subscription). %% @doc Publish MQTT Message --spec publish(mqtt_message()) -> ok. +-spec(publish(mqtt_message()) -> ok). publish(Msg) when is_record(Msg, mqtt_message) -> - emqttd_server:publish(Msg). + emqttd_server:publish(Msg), ok. %% @doc Subscribe --spec subscribe(binary()) -> ok. +-spec(subscribe(binary()) -> ok; + ({binary(), binary(), mqtt_qos()}) -> ok). subscribe(Topic) when is_binary(Topic) -> - emqttd_server:subscribe(Topic). + emqttd_server:subscribe(Topic); +subscribe({ClientId, Topic, Qos}) -> + subscribe(ClientId, Topic, Qos). --spec subscribe(binary(), binary(), mqtt_qos()) -> {ok, mqtt_qos()}. +-spec(subscribe(binary(), binary(), mqtt_qos()) -> {ok, mqtt_qos()}). subscribe(ClientId, Topic, Qos) -> emqttd_server:subscribe(ClientId, Topic, Qos). %% @doc Unsubscribe --spec unsubscribe(binary()) -> ok. +-spec(unsubscribe(binary()) -> ok). unsubscribe(Topic) when is_binary(Topic) -> emqttd_server:unsubscribe(Topic). --spec unsubscribe(binary(), binary(), mqtt_qos()) -> ok. +-spec(unsubscribe(binary(), binary(), mqtt_qos()) -> ok). unsubscribe(ClientId, Topic, Qos) -> emqttd_server:unsubscribe(ClientId, Topic, Qos). +%%-------------------------------------------------------------------- +%% Hooks API +%%-------------------------------------------------------------------- + +-spec(hook(atom(), function(), list(any())) -> ok | {error, any()}). +hook(Hook, Function, InitArgs) -> + emqttd_hook:add(Hook, Function, InitArgs). + +-spec(hook(atom(), function(), list(any()), integer()) -> ok | {error, any()}). +hook(Hook, Function, InitArgs, Priority) -> + emqttd_hook:add(Hook, Function, InitArgs, Priority). + +-spec(unhook(atom(), function()) -> ok | {error, any()}). +unhook(Hook, Function) -> + emqttd_hook:delete(Hook, Function). + +-spec(run_hooks(atom(), list(any()), any()) -> {ok | stop, any()}). +run_hooks(Hook, Args, Acc) -> + emqttd_hook:run(Hook, Args, Acc). + diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index 385ca134e..8b92d496b 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -79,6 +79,7 @@ print_vsn() -> start_servers(Sup) -> Servers = [{"emqttd ctl", emqttd_ctl}, + {"emqttd hook", emqttd_hook}, {"emqttd pubsub", {supervisor, emqttd_pubsub_sup}}, {"emqttd stats", emqttd_stats}, {"emqttd metrics", emqttd_metrics}, diff --git a/src/emqttd_broker.erl b/src/emqttd_broker.erl index 50b72720a..760dba041 100644 --- a/src/emqttd_broker.erl +++ b/src/emqttd_broker.erl @@ -28,9 +28,6 @@ %% Event API -export([subscribe/1, notify/2]). -%% Hook API --export([hook/3, unhook/2, foreach_hooks/2, foldl_hooks/3]). - %% Broker API -export([env/1, version/0, uptime/0, datetime/0, sysdescr/0]). @@ -100,40 +97,6 @@ datetime() -> io_lib:format( "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])). -%% @doc Hook --spec hook(Hook :: atom(), Name :: any(), MFA :: mfa()) -> ok | {error, any()}. -hook(Hook, Name, MFA) -> - gen_server:call(?SERVER, {hook, Hook, Name, MFA}). - -%% @doc Unhook --spec unhook(Hook :: atom(), Name :: any()) -> ok | {error, any()}. -unhook(Hook, Name) -> - gen_server:call(?SERVER, {unhook, Hook, Name}). - -%% @doc Foreach hooks --spec foreach_hooks(Hook :: atom(), Args :: list()) -> any(). -foreach_hooks(Hook, Args) -> - case ets:lookup(?BROKER_TAB, {hook, Hook}) of - [{_, Hooks}] -> - lists:foreach(fun({_Name, {M, F, A}}) -> - apply(M, F, Args++A) - end, Hooks); - [] -> - ok - end. - -%% @doc Foldl hooks --spec foldl_hooks(Hook :: atom(), Args :: list(), Acc0 :: any()) -> any(). -foldl_hooks(Hook, Args, Acc0) -> - case ets:lookup(?BROKER_TAB, {hook, Hook}) of - [{_, Hooks}] -> - lists:foldl(fun({_Name, {M, F, A}}, Acc) -> - apply(M, F, lists:append([Args, [Acc], A])) - end, Acc0, Hooks); - [] -> - Acc0 - end. - %% @doc Start a tick timer start_tick(Msg) -> start_tick(timer:seconds(env(sys_interval)), Msg). @@ -167,31 +130,6 @@ init([]) -> handle_call(uptime, _From, State) -> {reply, uptime(State), State}; -handle_call({hook, Hook, Name, MFArgs}, _From, State) -> - Key = {hook, Hook}, Reply = - case ets:lookup(?BROKER_TAB, Key) of - [{Key, Hooks}] -> - case lists:keyfind(Name, 1, Hooks) of - {Name, _MFArgs} -> - {error, existed}; - false -> - insert_hooks(Key, Hooks ++ [{Name, MFArgs}]) - end; - [] -> - insert_hooks(Key, [{Name, MFArgs}]) - end, - {reply, Reply, State}; - -handle_call({unhook, Hook, Name}, _From, State) -> - Key = {hook, Hook}, Reply = - case ets:lookup(?BROKER_TAB, Key) of - [{Key, Hooks}] -> - insert_hooks(Key, lists:keydelete(Name, 1, Hooks)); - [] -> - {error, not_found} - end, - {reply, Reply, State}; - handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). @@ -224,9 +162,6 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -insert_hooks(Key, Hooks) -> - ets:insert(?BROKER_TAB, {Key, Hooks}), ok. - create_topic(Topic) -> emqttd:create(topic, emqttd_topic:systop(Topic)). diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index 17225ac4a..04f0014d6 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -40,7 +40,7 @@ stack_size, reductions]). --define(MAX_LINES, 20000). +-define(MAX_LINES, 10000). -define(APP, emqttd). @@ -148,7 +148,7 @@ users(Args) -> emqttd_auth_username:cli(Args). %%-------------------------------------------------------------------- %% @doc Query clients clients(["list"]) -> - dump(ets, mqtt_client, fun print/1); + dump(mqtt_client); clients(["show", ClientId]) -> if_client(ClientId, fun print/1); @@ -173,10 +173,10 @@ sessions(["list"]) -> [sessions(["list", Type]) || Type <- ["persistent", "transient"]]; sessions(["list", "persistent"]) -> - dump(ets, mqtt_persistent_session, fun print/1); + dump(mqtt_persistent_session); sessions(["list", "transient"]) -> - dump(ets, mqtt_transient_session, fun print/1); + dump(mqtt_transient_session); sessions(["show", ClientId]) -> MP = {{bin(ClientId), '_'}, '_'}, @@ -199,11 +199,10 @@ sessions(_) -> %%-------------------------------------------------------------------- %% @doc Routes Command routes(["list"]) -> - Print = fun(Topic, Records) -> print(route, Topic, Records) end, - if_could_print(route, Print); + if_could_print(route, fun print/1); routes(["show", Topic]) -> - print(route, Topic, mnesia:dirty_read(route, bin(Topic))); + print(mnesia:dirty_read(route, bin(Topic))); routes(_) -> ?USAGE([{"routes list", "List all routes"}, @@ -212,28 +211,25 @@ routes(_) -> %%-------------------------------------------------------------------- %% @doc Topics Command topics(["list"]) -> - Print = fun(Topic, Records) -> print(topic, Topic, Records) end, - if_could_print(topic, Print); + if_could_print(topic, fun print/1); topics(["show", Topic]) -> - print(topic, Topic, ets:lookup(topic, bin(Topic))); + print(mnesia:dirty_read(topic, bin(Topic))); topics(_) -> ?USAGE([{"topics list", "List all topics"}, {"topics show ", "Show a topic"}]). subscriptions(["list"]) -> - Print = fun(ClientId, Records) -> print(subscription, ClientId, Records) end, - if_could_print(subscription, Print); + if_could_print(subscription, fun print/1); subscriptions(["list", "static"]) -> - Print = fun(ClientId, Records) -> print(subscription, ClientId, Records) end, - if_could_print(backend_subscription, Print); + if_could_print(backend_subscription, fun print/1); subscriptions(["show", ClientId]) -> case mnesia:dirty_read(subscription, bin(ClientId)) of [] -> ?PRINT_MSG("Not Found.~n"); - Records -> print(subscription, ClientId, Records) + Records -> print(Records) end; subscriptions(["add", ClientId, Topic, QoS]) -> @@ -242,11 +238,11 @@ subscriptions(["add", ClientId, Topic, QoS]) -> topic = bin(Topic), qos = IntQos}, case emqttd_backend:add_subscription(Subscription) of - {atomic, ok} -> + ok -> ?PRINT_MSG("ok~n"); - {aborted, {error, existed}} -> + {error, already_existed} -> ?PRINT_MSG("Error: already existed~n"); - {aborted, Reason} -> + {error, Reason} -> ?PRINT("Error: ~p~n", [Reason]) end end, @@ -274,7 +270,7 @@ if_could_print(Tab, Fun) -> ?PRINT("Could not list, too many ~ss: ~p~n", [Tab, Size]); _Size -> Keys = mnesia:dirty_all_keys(Tab), - foreach(fun(Key) -> Fun(Key, ets:lookup(Tab, Key)) end, Keys) + foreach(fun(Key) -> Fun(ets:lookup(Tab, Key)) end, Keys) end. if_valid_qos(QoS, Fun) -> @@ -465,23 +461,53 @@ listeners([]) -> listeners(_) -> ?PRINT_CMD("listeners", "List listeners"). +%%-------------------------------------------------------------------- +%% Dump ETS +%%-------------------------------------------------------------------- + +dump(Table) -> + dump(Table, ets:first(Table)). + +dump(_Table, '$end_of_table') -> + ok; + +dump(Table, Key) -> + case ets:lookup(Table, Key) of + [Record] -> print(Record); + [] -> ok + end, + dump(Table, ets:next(Table, Key)). + +print([]) -> + ok; + +print(Routes = [#mqtt_route{topic = Topic} | _]) -> + Nodes = [atom_to_list(Node) || #mqtt_route{node = Node} <- Routes], + ?PRINT("~s -> ~s~n", [Topic, string:join(Nodes, ",")]); + +print(Subscriptions = [#mqtt_subscription{subid = ClientId} | _]) -> + TopicTable = [io_lib:format("~s:~w", [Topic, Qos]) + || #mqtt_subscription{topic = Topic, qos = Qos} <- Subscriptions], + ?PRINT("~s -> ~s~n", [ClientId, string:join(TopicTable, ",")]); + +print(Topics = [#mqtt_topic{}|_]) -> + foreach(fun print/1, Topics); + print(#mqtt_plugin{name = Name, version = Ver, descr = Descr, active = Active}) -> ?PRINT("Plugin(~s, version=~s, description=~s, active=~s)~n", - [Name, Ver, Descr, Active]); + [Name, Ver, Descr, Active]); -print(#mqtt_client{client_id = ClientId, clean_sess = CleanSess, - username = Username, peername = Peername, - connected_at = ConnectedAt}) -> +print(#mqtt_client{client_id = ClientId, clean_sess = CleanSess, username = Username, + peername = Peername, connected_at = ConnectedAt}) -> ?PRINT("Client(~s, clean_sess=~s, username=~s, peername=~s, connected_at=~p)~n", - [ClientId, CleanSess, Username, - emqttd_net:format(Peername), - emqttd_time:now_to_secs(ConnectedAt)]); + [ClientId, CleanSess, Username, emqttd_net:format(Peername), + emqttd_time:now_to_secs(ConnectedAt)]); print(#mqtt_topic{topic = Topic, flags = Flags}) -> - ?PRINT("~s: ~p~n", [Topic, Flags]); + ?PRINT("~s: ~s~n", [Topic, string:join([atom_to_list(F) || F <- Flags], ",")]); print(#mqtt_route{topic = Topic, node = Node}) -> - ?PRINT("~s: ~s~n", [Topic, Node]); + ?PRINT("~s -> ~s~n", [Topic, Node]); print({{ClientId, _ClientPid}, SessInfo}) -> InfoKeys = [clean_sess, @@ -499,39 +525,11 @@ print({{ClientId, _ClientPid}, SessInfo}) -> "created_at=~w)~n", [ClientId | [format(Key, proplists:get_value(Key, SessInfo)) || Key <- InfoKeys]]). -print(route, Topic, Routes) -> - Nodes = [Node || #mqtt_route{node = Node} <- Routes], - ?PRINT("~s: ~p~n", [Topic, Nodes]); - -print(topic, _Topic, Records) -> - [print(R) || R <- Records]; - -print(subscription, ClientId, Subscriptions) -> - TopicTable = [{Topic, Qos} || #mqtt_subscription{topic = Topic, qos = Qos} <- Subscriptions], - ?PRINT("~s: ~p~n", [ClientId, TopicTable]). - format(created_at, Val) -> emqttd_time:now_to_secs(Val); -format(subscriptions, List) -> - string:join([io_lib:format("~s:~w", [Topic, Qos]) || {Topic, Qos} <- List], ","); - format(_, Val) -> Val. bin(S) -> iolist_to_binary(S). -%%TODO: ... -dump(ets, Table, Fun) -> - dump(ets, Table, ets:first(Table), Fun). - -dump(ets, _Table, '$end_of_table', _Fun) -> - ok; - -dump(ets, Table, Key, Fun) -> - case ets:lookup(Table, Key) of - [Record] -> Fun(Record); - [] -> ignore - end, - dump(ets, Table, ets:next(Table, Key), Fun). - diff --git a/src/emqttd_hook.erl b/src/emqttd_hook.erl new file mode 100644 index 000000000..f183c98d3 --- /dev/null +++ b/src/emqttd_hook.erl @@ -0,0 +1,155 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_hook). + +-author("Feng Lee "). + +-behaviour(gen_server). + +%% Start +-export([start_link/0]). + +%% Hooks API +-export([add/3, add/4, delete/2, run/3, lookup/1]). + +%% gen_server Function Exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {}). + +-record(callback, {function :: function(), + init_args = [] :: list(any()), + priority = 0 :: integer()}). + +-record(hook, {name :: atom(), callbacks = [] :: list(#callback{})}). + +-define(HOOK_TAB, mqtt_hook). + +%%-------------------------------------------------------------------- +%% Start API +%%-------------------------------------------------------------------- + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +%%-------------------------------------------------------------------- +%% Hooks API +%%-------------------------------------------------------------------- + +-spec(add(atom(), function(), list(any())) -> ok). +add(HookPoint, Function, InitArgs) -> + add(HookPoint, Function, InitArgs, 0). + +-spec(add(atom(), function(), list(any()), integer()) -> ok). +add(HookPoint, Function, InitArgs, Priority) -> + gen_server:call(?MODULE, {add, HookPoint, Function, InitArgs, Priority}). + +-spec(delete(atom(), function()) -> ok). +delete(HookPoint, Function) -> + gen_server:call(?MODULE, {delete, HookPoint, Function}). + +-spec(run(atom(), list(any()), any()) -> any()). +run(HookPoint, Args, Acc) -> + run_(lookup(HookPoint), Args, Acc). + +%% @private +run_([#callback{function = Fun, init_args = InitArgs} | Callbacks], Args, Acc) -> + case apply(Fun, lists:append([Args, [Acc], InitArgs])) of + ok -> run_(Callbacks, Args, Acc); + {ok, NewAcc} -> run_(Callbacks, Args, NewAcc); + stop -> {stop, Acc}; + {stop, NewAcc} -> {stop, NewAcc} + end; + +run_([], _Args, Acc) -> + {ok, Acc}. + +-spec(lookup(atom()) -> [#callback{}]). +lookup(HookPoint) -> + case ets:lookup(?HOOK_TAB, HookPoint) of + [] -> []; + [#hook{callbacks = Callbacks}] -> Callbacks + end. + +%%-------------------------------------------------------------------- +%% gen_server Callbacks +%%-------------------------------------------------------------------- + +init([]) -> + ets:new(?HOOK_TAB, [set, protected, named_table, {keypos, #hook.name}]), + {ok, #state{}}. + +handle_call({add, HookPoint, Function, InitArgs, Priority}, _From, State) -> + Reply = + case ets:lookup(?HOOK_TAB, HookPoint) of + [#hook{callbacks = Callbacks}] -> + case lists:keyfind(Function, #callback.function, Callbacks) of + false -> + Callback = #callback{function = Function, + init_args = InitArgs, + priority = Priority}, + insert_hook_(HookPoint, add_callback_(Callback, Callbacks)); + _Callback -> + {error, already_hooked} + end; + [] -> + Callback = #callback{function = Function, + init_args = InitArgs, + priority = Priority}, + insert_hook_(HookPoint, [Callback]) + end, + {reply, Reply, State}; + +handle_call({delete, HookPoint, Function}, _From, State) -> + Reply = + case ets:lookup(?HOOK_TAB, HookPoint) of + [#hook{callbacks = Callbacks}] -> + insert_hook_(HookPoint, del_callback_(Function, Callbacks)); + [] -> + {error, not_found} + end, + {reply, Reply, State}; + +handle_call(_Req, _From, State) -> + {reply, ignore, 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 functions +%%-------------------------------------------------------------------- + +insert_hook_(HookPoint, Callbacks) -> + ets:insert(?HOOK_TAB, #hook{name = HookPoint, callbacks = Callbacks}), ok. + +add_callback_(Callback, Callbacks) -> + lists:keymerge(#callback.priority, Callbacks, [Callback]). + +del_callback_(Function, Callbacks) -> + lists:keydelete(Function, #callback.function, Callbacks). + diff --git a/src/emqttd_mod_presence.erl b/src/emqttd_mod_presence.erl index 1aa382fa2..b49511000 100644 --- a/src/emqttd_mod_presence.erl +++ b/src/emqttd_mod_presence.erl @@ -23,57 +23,51 @@ -export([load/1, unload/1]). --export([client_connected/3, client_disconnected/3]). +-export([on_client_connected/3, on_client_disconnected/3]). load(Opts) -> - emqttd_broker:hook('client.connected', {?MODULE, client_connected}, - {?MODULE, client_connected, [Opts]}), - emqttd_broker:hook('client.disconnected', {?MODULE, client_disconnected}, - {?MODULE, client_disconnected, [Opts]}), - ok. + emqttd:hook('client.connected', fun ?MODULE:on_client_connected/3, [Opts]), + emqttd:hook('client.disconnected', fun ?MODULE:on_client_disconnected/3, [Opts]). -client_connected(ConnAck, #mqtt_client{client_id = ClientId, - username = Username, - peername = {IpAddress, _}, - clean_sess = CleanSess, - proto_ver = ProtoVer}, Opts) -> - Sess = case CleanSess of - true -> false; - false -> true - end, +on_client_connected(ConnAck, Client = #mqtt_client{client_id = ClientId, + username = Username, + peername = {IpAddr, _}, + clean_sess = CleanSess, + proto_ver = ProtoVer}, Opts) -> Json = mochijson2:encode([{clientid, ClientId}, {username, Username}, - {ipaddress, list_to_binary(emqttd_net:ntoa(IpAddress))}, - {session, Sess}, + {ipaddress, list_to_binary(emqttd_net:ntoa(IpAddr))}, + {session, sess(CleanSess)}, {protocol, ProtoVer}, {connack, ConnAck}, {ts, emqttd_time:now_to_secs()}]), - Msg = emqttd_message:make(presence, - proplists:get_value(qos, Opts, 0), - topic(connected, ClientId), - iolist_to_binary(Json)), - emqttd:publish(Msg). + emqttd:publish(message(qos(Opts), topic(connected, ClientId), Json)), + {ok, Client}. -client_disconnected(Reason, ClientId, Opts) -> +on_client_disconnected(Reason, ClientId, Opts) -> Json = mochijson2:encode([{clientid, ClientId}, {reason, reason(Reason)}, {ts, emqttd_time:now_to_secs()}]), - Msg = emqttd_message:make(presence, - proplists:get_value(qos, Opts, 0), - topic(disconnected, ClientId), - iolist_to_binary(Json)), - emqttd:publish(Msg). + emqttd:publish(message(qos(Opts), topic(disconnected, ClientId), Json)). unload(_Opts) -> - emqttd_broker:unhook('client.connected', {?MODULE, client_connected}), - emqttd_broker:unhook('client.disconnected', {?MODULE, client_disconnected}). + emqttd:unhook('client.connected', fun ?MODULE:on_client_connected/3), + emqttd:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3). + +sess(false) -> true; +sess(true) -> false. + +qos(Opts) -> proplists:get_value(qos, Opts, 0). + +message(Qos, Topic, Json) -> + emqttd_message:make(presence, Qos, Topic, iolist_to_binary(Json)). topic(connected, ClientId) -> emqttd_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])); topic(disconnected, ClientId) -> emqttd_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])). -reason(Reason) when is_atom(Reason) -> Reason; +reason(Reason) when is_atom(Reason) -> Reason; reason({Error, _}) when is_atom(Error) -> Error; reason(_) -> internal_error. diff --git a/src/emqttd_mod_rewrite.erl b/src/emqttd_mod_rewrite.erl index b1bad9766..9109d2155 100644 --- a/src/emqttd_mod_rewrite.erl +++ b/src/emqttd_mod_rewrite.erl @@ -23,7 +23,7 @@ -export([load/1, reload/1, unload/1]). --export([rewrite/3, rewrite/4]). +-export([rewrite_subscribe/3, rewrite_unsubscribe/3, rewrite_publish/2]). %%-------------------------------------------------------------------- %% API @@ -33,23 +33,19 @@ load(Opts) -> File = proplists:get_value(file, Opts), {ok, Terms} = file:consult(File), Sections = compile(Terms), - emqttd_broker:hook('client.subscribe', {?MODULE, rewrite_subscribe}, - {?MODULE, rewrite, [subscribe, Sections]}), - emqttd_broker:hook('client.unsubscribe', {?MODULE, rewrite_unsubscribe}, - {?MODULE, rewrite, [unsubscribe, Sections]}), - emqttd_broker:hook('message.publish', {?MODULE, rewrite_publish}, - {?MODULE, rewrite, [publish, Sections]}), - ok. + emqttd:hook('client.subscribe', fun ?MODULE:rewrite_subscribe/3, [Sections]), + emqttd:hook('client.unsubscribe', fun ?MODULE:rewrite_unsubscribe/3, [Sections]), + emqttd:hook('message.publish', fun ?MODULE:rewrite_publish/2, [Sections]). -rewrite(_ClientId, TopicTable, subscribe, Sections) -> - lager:info("rewrite subscribe: ~p", [TopicTable]), - [{match_topic(Topic, Sections), Qos} || {Topic, Qos} <- TopicTable]; +rewrite_subscribe(_ClientId, TopicTable, Sections) -> + lager:info("Rewrite subscribe: ~p", [TopicTable]), + {ok, [{match_topic(Topic, Sections), Qos} || {Topic, Qos} <- TopicTable]}. -rewrite(_ClientId, Topics, unsubscribe, Sections) -> - lager:info("rewrite unsubscribe: ~p", [Topics]), - [match_topic(Topic, Sections) || Topic <- Topics]. +rewrite_unsubscribe(_ClientId, Topics, Sections) -> + lager:info("Rewrite unsubscribe: ~p", [Topics]), + {ok, [match_topic(Topic, Sections) || Topic <- Topics]}. -rewrite(Message=#mqtt_message{topic = Topic}, publish, Sections) -> +rewrite_publish(Message=#mqtt_message{topic = Topic}, Sections) -> %%TODO: this will not work if the client is always online. RewriteTopic = case get({rewrite, Topic}) of @@ -59,11 +55,11 @@ rewrite(Message=#mqtt_message{topic = Topic}, publish, Sections) -> DestTopic -> DestTopic end, - Message#mqtt_message{topic = RewriteTopic}. + {ok, Message#mqtt_message{topic = RewriteTopic}}. reload(File) -> %%TODO: The unload api is not right... - case emqttd:is_mod_enabled(rewrite) of + case emqttd_app:is_mod_enabled(rewrite) of true -> unload(state), load([{file, File}]); @@ -72,9 +68,9 @@ reload(File) -> end. unload(_) -> - emqttd_broker:unhook('client.subscribe', {?MODULE, rewrite_subscribe}), - emqttd_broker:unhook('client.unsubscribe',{?MODULE, rewrite_unsubscribe}), - emqttd_broker:unhook('message.publish', {?MODULE, rewrite_publish}). + emqttd:unhook('client.subscribe', fun ?MODULE:rewrite_subscribe/3), + emqttd:unhook('client.unsubscribe',fun ?MODULE:rewrite_unsubscribe/3), + emqttd:unhook('message.publish', fun ?MODULE:rewrite_publish/2). %%-------------------------------------------------------------------- %% Internal functions diff --git a/src/emqttd_mod_subscription.erl b/src/emqttd_mod_subscription.erl index b8d31b436..c23ab6848 100644 --- a/src/emqttd_mod_subscription.erl +++ b/src/emqttd_mod_subscription.erl @@ -23,26 +23,27 @@ -include("emqttd_protocol.hrl"). --export([load/1, client_connected/3, unload/1]). +-export([load/1, on_client_connected/3, unload/1]). -record(state, {topics, backend = false}). load(Opts) -> Topics = [{iolist_to_binary(Topic), QoS} || {Topic, QoS} <- Opts, ?IS_QOS(QoS)], State = #state{topics = Topics, backend = lists:member(backend, Opts)}, - emqttd_broker:hook('client.connected', {?MODULE, client_connected}, - {?MODULE, client_connected, [State]}), - ok. + emqttd:hook('client.connected', fun ?MODULE:on_client_connected/3, [State]). + +on_client_connected(?CONNACK_ACCEPT, Client = #mqtt_client{client_id = ClientId, + client_pid = ClientPid, + username = Username}, + #state{topics = Topics, backend = Backend}) -> -client_connected(?CONNACK_ACCEPT, #mqtt_client{client_id = ClientId, - client_pid = ClientPid, - username = Username}, - #state{topics = Topics, backend = Backend}) -> Replace = fun(Topic) -> rep(<<"$u">>, Username, rep(<<"$c">>, ClientId, Topic)) end, TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- with_backend(Backend, ClientId, Topics)], - emqttd_client:subscribe(ClientPid, TopicTable); + emqttd_client:subscribe(ClientPid, TopicTable), + {ok, Client}; -client_connected(_ConnAck, _Client, _State) -> ok. +on_client_connected(_ConnAck, _Client, _State) -> + ok. with_backend(false, _ClientId, TopicTable) -> TopicTable; @@ -51,7 +52,7 @@ with_backend(true, ClientId, TopicTable) -> emqttd_opts:merge([Fun(Sub) || Sub <- emqttd_backend:lookup_subscriptions(ClientId)], TopicTable). unload(_Opts) -> - emqttd_broker:unhook('client.connected', {?MODULE, client_connected}). + emqttd:unhook('client.connected', fun ?MODULE:on_client_connected/3). rep(<<"$c">>, ClientId, Topic) -> emqttd_topic:feed_var(<<"$c">>, ClientId, Topic); diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index 6764d201c..a6af27155 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -165,7 +165,7 @@ process(Packet = ?CONNECT_PACKET(Var), State0) -> {ReturnCode, false, State1} end, %% Run hooks - emqttd_broker:foreach_hooks('client.connected', [ReturnCode1, client(State3)]), + emqttd:run_hooks('client.connected', [ReturnCode1], client(State3)), %% Send connack send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3); @@ -247,7 +247,9 @@ with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), end. -spec send(mqtt_message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}. -send(Msg, State) when is_record(Msg, mqtt_message) -> +send(Msg, State = #proto_state{client_id = ClientId}) + when is_record(Msg, mqtt_message) -> + emqttd:run_hooks('message.delivered', [ClientId], Msg), send(emqttd_message:to_packet(Msg), State); send(Packet, State = #proto_state{sendfun = SendFun}) @@ -281,7 +283,7 @@ shutdown(conflict, #proto_state{client_id = _ClientId}) -> shutdown(Error, State = #proto_state{client_id = ClientId, will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Error], State), send_willmsg(ClientId, WillMsg), - emqttd_broker:foreach_hooks('client.disconnected', [Error, ClientId]), + emqttd:run_hooks('client.disconnected', [Error], ClientId), %% let it down %% emqttd_cm:unregister(ClientId). ok. diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 03ca4907f..e216a89fe 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -60,15 +60,15 @@ mnesia(copy) -> %%-------------------------------------------------------------------- %% @doc Start one pubsub --spec start_link(Pool, Id, Env) -> {ok, pid()} | ignore | {error, any()} when - Pool :: atom(), - Id :: pos_integer(), - Env :: list(tuple()). +-spec(start_link(Pool, Id, Env) -> {ok, pid()} | ignore | {error, any()} when + Pool :: atom(), + Id :: pos_integer(), + Env :: list(tuple())). start_link(Pool, Id, Env) -> gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). %% @doc Create a Topic. --spec create_topic(binary()) -> ok | {error, any()}. +-spec(create_topic(binary()) -> ok | {error, any()}). create_topic(Topic) when is_binary(Topic) -> case mnesia:transaction(fun add_topic_/2, [Topic, [static]]) of {atomic, ok} -> ok; @@ -76,7 +76,7 @@ create_topic(Topic) when is_binary(Topic) -> end. %% @doc Lookup a Topic. --spec lookup_topic(binary()) -> list(mqtt_topic()). +-spec(lookup_topic(binary()) -> list(mqtt_topic())). lookup_topic(Topic) when is_binary(Topic) -> mnesia:dirty_read(topic, Topic). @@ -85,17 +85,17 @@ lookup_topic(Topic) when is_binary(Topic) -> %%-------------------------------------------------------------------- %% @doc Subscribe a Topic --spec subscribe(binary(), pid()) -> ok. +-spec(subscribe(binary(), pid()) -> ok). subscribe(Topic, SubPid) when is_binary(Topic) -> call(pick(Topic), {subscribe, Topic, SubPid}). %% @doc Asynchronous Subscribe --spec async_subscribe(binary(), pid()) -> ok. +-spec(async_subscribe(binary(), pid()) -> ok). async_subscribe(Topic, SubPid) when is_binary(Topic) -> cast(pick(Topic), {subscribe, Topic, SubPid}). %% @doc Publish message to Topic. --spec publish(binary(), any()) -> ok. +-spec(publish(binary(), any()) -> any()). publish(Topic, Msg) -> lists:foreach( fun(#mqtt_route{topic = To, node = Node}) when Node =:= node() -> @@ -105,7 +105,7 @@ publish(Topic, Msg) -> end, emqttd_router:lookup(Topic)). %% @doc Dispatch Message to Subscribers --spec dispatch(binary(), mqtt_message()) -> ok. +-spec(dispatch(binary(), mqtt_message()) -> ok). dispatch(Queue = <<"$queue/", _T>>, Msg) -> case subscribers(Queue) of [] -> @@ -148,12 +148,12 @@ dropped(_Topic) -> emqttd_metrics:inc('messages/dropped'). %% @doc Unsubscribe --spec unsubscribe(binary(), pid()) -> ok. +-spec(unsubscribe(binary(), pid()) -> ok). unsubscribe(Topic, SubPid) when is_binary(Topic) -> call(pick(Topic), {unsubscribe, Topic, SubPid}). %% @doc Asynchronous Unsubscribe --spec async_unsubscribe(binary(), pid()) -> ok. +-spec(async_unsubscribe(binary(), pid()) -> ok). async_unsubscribe(Topic, SubPid) when is_binary(Topic) -> cast(pick(Topic), {unsubscribe, Topic, SubPid}). diff --git a/src/emqttd_router.erl b/src/emqttd_router.erl index ac744e94a..a40b9e550 100644 --- a/src/emqttd_router.erl +++ b/src/emqttd_router.erl @@ -63,31 +63,31 @@ start_link() -> %%-------------------------------------------------------------------- %% @doc Lookup Routes. --spec lookup(Topic:: binary()) -> [mqtt_route()]. +-spec(lookup(Topic:: binary()) -> [mqtt_route()]). lookup(Topic) when is_binary(Topic) -> Matched = mnesia:async_dirty(fun emqttd_trie:match/1, [Topic]), %% Optimize: route table will be replicated to all nodes. lists:append([ets:lookup(route, To) || To <- [Topic | Matched]]). %% @doc Print Routes. --spec print(Topic :: binary()) -> [ok]. +-spec(print(Topic :: binary()) -> [ok]). print(Topic) -> [io:format("~s -> ~s~n", [To, Node]) || #mqtt_route{topic = To, node = Node} <- lookup(Topic)]. %% @doc Add Route --spec add_route(binary() | mqtt_route()) -> ok | {error, Reason :: any()}. +-spec(add_route(binary() | mqtt_route()) -> ok | {error, Reason :: any()}). add_route(Topic) when is_binary(Topic) -> add_route(#mqtt_route{topic = Topic, node = node()}); add_route(Route) when is_record(Route, mqtt_route) -> add_routes([Route]). --spec add_route(Topic :: binary(), Node :: node()) -> ok | {error, Reason :: any()}. +-spec(add_route(Topic :: binary(), Node :: node()) -> ok | {error, Reason :: any()}). add_route(Topic, Node) when is_binary(Topic), is_atom(Node) -> add_route(#mqtt_route{topic = Topic, node = Node}). %% @doc Add Routes --spec add_routes([mqtt_route()]) -> ok | {errory, Reason :: any()}. +-spec(add_routes([mqtt_route()]) -> ok | {errory, Reason :: any()}). add_routes(Routes) -> Add = fun() -> [add_route_(Route) || Route <- Routes] end, case mnesia:transaction(Add) of @@ -112,18 +112,18 @@ add_route_(Route = #mqtt_route{topic = Topic}) -> end. %% @doc Delete Route --spec del_route(binary() | mqtt_route()) -> ok | {error, Reason :: any()}. +-spec(del_route(binary() | mqtt_route()) -> ok | {error, Reason :: any()}). del_route(Topic) when is_binary(Topic) -> del_route(#mqtt_route{topic = Topic, node = node()}); del_route(Route) when is_record(Route, mqtt_route) -> del_routes([Route]). --spec del_route(Topic :: binary(), Node :: node()) -> ok | {error, Reason :: any()}. +-spec(del_route(Topic :: binary(), Node :: node()) -> ok | {error, Reason :: any()}). del_route(Topic, Node) when is_binary(Topic), is_atom(Node) -> del_route(#mqtt_route{topic = Topic, node = Node}). %% @doc Delete Routes --spec del_routes([mqtt_route()]) -> ok | {error, any()}. +-spec(del_routes([mqtt_route()]) -> ok | {error, any()}). del_routes(Routes) -> Del = fun() -> [del_route_(Route) || Route <- Routes] end, case mnesia:transaction(Del) of diff --git a/src/emqttd_server.erl b/src/emqttd_server.erl index e119a5029..0fafb14a4 100644 --- a/src/emqttd_server.erl +++ b/src/emqttd_server.erl @@ -35,7 +35,7 @@ %% PubSub API -export([subscribe/1, subscribe/3, publish/1, unsubscribe/1, unsubscribe/3, - update_subscription/4]). + lookup_subscription/1, update_subscription/4]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -63,10 +63,10 @@ mnesia(copy) -> %%-------------------------------------------------------------------- %% @doc Start a Server --spec start_link(Pool, Id, Env) -> {ok, pid()} | ignore | {error, any()} when - Pool :: atom(), - Id :: pos_integer(), - Env :: list(tuple()). +-spec(start_link(Pool, Id, Env) -> {ok, pid()} | ignore | {error, any()} when + Pool :: atom(), + Id :: pos_integer(), + Env :: list(tuple())). start_link(Pool, Id, Env) -> gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). @@ -75,40 +75,48 @@ start_link(Pool, Id, Env) -> %%-------------------------------------------------------------------- %% @doc Subscribe a Topic --spec subscribe(binary()) -> ok. +-spec(subscribe(binary()) -> ok). subscribe(Topic) when is_binary(Topic) -> From = self(), call(server(From), {subscribe, From, Topic}). %% @doc Subscribe from a MQTT session. --spec subscribe(binary(), binary(), mqtt_qos()) -> ok. +-spec(subscribe(binary(), binary(), mqtt_qos()) -> ok). subscribe(ClientId, Topic, Qos) -> From = self(), call(server(From), {subscribe, From, ClientId, Topic, ?QOS_I(Qos)}). +%% @doc Lookup subscriptions. +-spec(lookup_subscription(binary()) -> [#mqtt_subscription{}]). +lookup_subscription(ClientId) -> + mnesia:dirty_read(subscription, ClientId). + %% @doc Update a subscription. --spec update_subscription(binary(), binary(), mqtt_qos(), mqtt_qos()) -> ok. +-spec(update_subscription(binary(), binary(), mqtt_qos(), mqtt_qos()) -> ok). update_subscription(ClientId, Topic, OldQos, NewQos) -> call(server(self()), {update_subscription, ClientId, Topic, ?QOS_I(OldQos), ?QOS_I(NewQos)}). %% @doc Publish a Message --spec publish(Msg :: mqtt_message()) -> ok. +-spec(publish(Msg :: mqtt_message()) -> any()). publish(Msg = #mqtt_message{from = From}) -> trace(publish, From, Msg), - Msg1 = #mqtt_message{topic = Topic} - = emqttd_broker:foldl_hooks('message.publish', [], Msg), - %% Retain message first. Don't create retained topic. - Msg2 = case emqttd_retainer:retain(Msg1) of - ok -> emqttd_message:unset_flag(Msg1); - ignore -> Msg1 - end, - emqttd_pubsub:publish(Topic, Msg2). + case emqttd:run_hooks('message.publish', [], Msg) of + {ok, Msg1 = #mqtt_message{topic = Topic}} -> + %% Retain message first. Don't create retained topic. + Msg2 = case emqttd_retainer:retain(Msg1) of + ok -> emqttd_message:unset_flag(Msg1); + ignore -> Msg1 + end, + emqttd_pubsub:publish(Topic, Msg2); + {stop, Msg1} -> + lager:warning("Stop publishing: ~s", [emqttd_message:format(Msg1)]) + end. %% @doc Unsubscribe a Topic --spec unsubscribe(binary()) -> ok. +-spec(unsubscribe(binary()) -> ok). unsubscribe(Topic) when is_binary(Topic) -> From = self(), call(server(From), {unsubscribe, From, Topic}). %% @doc Unsubscribe a Topic from a MQTT session --spec unsubscribe(binary(), binary(), mqtt_qos()) -> ok. +-spec(unsubscribe(binary(), binary(), mqtt_qos()) -> ok). unsubscribe(ClientId, Topic, Qos) -> From = self(), call(server(From), {unsubscribe, From, ClientId, Topic, Qos}). @@ -188,11 +196,11 @@ code_change(_OldVsn, State, _Extra) -> %% @private %% @doc Add a subscription. --spec add_subscription_(binary(), binary(), mqtt_qos()) -> ok. +-spec(add_subscription_(binary(), binary(), mqtt_qos()) -> ok). add_subscription_(ClientId, Topic, Qos) -> add_subscription_(#mqtt_subscription{subid = ClientId, topic = Topic, qos = Qos}). --spec add_subscription_(mqtt_subscription()) -> ok. +-spec(add_subscription_(mqtt_subscription()) -> ok). add_subscription_(Subscription) when is_record(Subscription, mqtt_subscription) -> mnesia:dirty_write(subscription, Subscription). @@ -202,7 +210,7 @@ update_subscription_(OldSub, NewSub) -> %% @private %% @doc Delete a subscription --spec del_subscription_(binary(), binary(), mqtt_qos()) -> ok. +-spec(del_subscription_(binary(), binary(), mqtt_qos()) -> ok). del_subscription_(ClientId, Topic, Qos) -> del_subscription_(#mqtt_subscription{subid = ClientId, topic = Topic, qos = Qos}). @@ -246,7 +254,8 @@ trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) -> %%-------------------------------------------------------------------- set_subscription_stats() -> - emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', mnesia:table_info(subscription, size)). + emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', + mnesia:table_info(subscription, size)). %%-------------------------------------------------------------------- diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 12b3e680d..b95494f8d 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -288,49 +288,60 @@ handle_call(Req, _From, State) -> handle_cast({subscribe, TopicTable0, AckFun}, Session = #session{client_id = ClientId, subscriptions = Subscriptions}) -> - TopicTable = emqttd_broker:foldl_hooks('client.subscribe', [ClientId], TopicTable0), - ?LOG(info, "Subscribe ~p", [TopicTable], Session), - Subscriptions1 = lists:foldl( - fun({Topic, Qos}, SubDict) -> - case dict:find(Topic, SubDict) of - {ok, Qos} -> - ?LOG(warning, "duplicated subscribe: ~s, qos = ~w", [Topic, Qos], Session), - SubDict; - {ok, OldQos} -> - emqttd_server:update_subscription(ClientId, Topic, OldQos, Qos), - ?LOG(warning, "duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, Qos], Session), - dict:store(Topic, Qos, SubDict); - error -> - emqttd:subscribe(ClientId, Topic, Qos), - %%TODO: the design is ugly... - %% : 3.8.4 - %% Where the Topic Filter is not identical to any existing Subscription’s filter, - %% a new Subscription is created and all matching retained messages are sent. - emqttd_retainer:dispatch(Topic, self()), + case emqttd:run_hooks('client.subscribe', [ClientId], TopicTable0) of + {ok, TopicTable} -> + ?LOG(info, "Subscribe ~p", [TopicTable], Session), + Subscriptions1 = lists:foldl( + fun({Topic, Qos}, SubDict) -> + case dict:find(Topic, SubDict) of + {ok, Qos} -> + ?LOG(warning, "duplicated subscribe: ~s, qos = ~w", [Topic, Qos], Session), + SubDict; + {ok, OldQos} -> + emqttd_server:update_subscription(ClientId, Topic, OldQos, Qos), + ?LOG(warning, "duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, Qos], Session), + dict:store(Topic, Qos, SubDict); + error -> + emqttd:subscribe(ClientId, Topic, Qos), + %%TODO: the design is ugly... + %% : 3.8.4 + %% Where the Topic Filter is not identical to any existing Subscription’s filter, + %% a new Subscription is created and all matching retained messages are sent. + emqttd_retainer:dispatch(Topic, self()), + + dict:store(Topic, Qos, SubDict) + end + end, Subscriptions, TopicTable), + AckFun([Qos || {_, Qos} <- TopicTable]), + emqttd:run_hooks('client.subscribe.after', [ClientId], TopicTable), + hibernate(Session#session{subscriptions = Subscriptions1}); + {stop, TopicTable} -> + ?LOG(error, "Cannot subscribe: ~p", [TopicTable], Session), + hibernate(Session) + end; - dict:store(Topic, Qos, SubDict) - end - end, Subscriptions, TopicTable), - AckFun([Qos || {_, Qos} <- TopicTable]), - emqttd_broker:foreach_hooks('client.subscribe.after', [ClientId, TopicTable]), - hibernate(Session#session{subscriptions = Subscriptions1}); handle_cast({unsubscribe, Topics0}, Session = #session{client_id = ClientId, subscriptions = Subscriptions}) -> - Topics = emqttd_broker:foldl_hooks('client.unsubscribe', [ClientId], Topics0), - ?LOG(info, "unsubscribe ~p", [Topics], Session), - Subscriptions1 = lists:foldl( - fun(Topic, SubDict) -> - case dict:find(Topic, SubDict) of - {ok, Qos} -> - emqttd:unsubscribe(ClientId, Topic, Qos), - dict:erase(Topic, SubDict); - error -> - SubDict - end - end, Subscriptions, Topics), - hibernate(Session#session{subscriptions = Subscriptions1}); + case emqttd:run_hooks('client.unsubscribe', [ClientId], Topics0) of + {ok, Topics} -> + ?LOG(info, "unsubscribe ~p", [Topics], Session), + Subscriptions1 = lists:foldl( + fun(Topic, SubDict) -> + case dict:find(Topic, SubDict) of + {ok, Qos} -> + emqttd:unsubscribe(ClientId, Topic, Qos), + dict:erase(Topic, SubDict); + error -> + SubDict + end + end, Subscriptions, Topics), + hibernate(Session#session{subscriptions = Subscriptions1}); + {stop, Topics} -> + ?LOG(info, "Cannot unsubscribe: ~p", [Topics], Session), + hibernate(Session) + end; handle_cast({destroy, ClientId}, Session = #session{client_id = ClientId}) -> ?LOG(warning, "destroyed", [], Session), @@ -644,7 +655,7 @@ acked(PktId, Session = #session{client_id = ClientId, awaiting_ack = Awaiting}) -> case lists:keyfind(PktId, 1, InflightQ) of {_, Msg} -> - emqttd_broker:foreach_hooks('message.acked', [ClientId, Msg]); + emqttd:run_hooks('message.acked', [ClientId], Msg); false -> ?LOG(error, "Cannot find acked pktid: ~p", [PktId], Session) end, From 37fa9a2f9b566b3cba9c77f589863154a0ccca27 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 11 Mar 2016 23:43:41 +0800 Subject: [PATCH 65/69] return ok | {error, any()} --- src/emqttd_backend.erl | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/emqttd_backend.erl b/src/emqttd_backend.erl index 338a6097f..16079bbf3 100644 --- a/src/emqttd_backend.erl +++ b/src/emqttd_backend.erl @@ -49,21 +49,20 @@ mnesia(copy) -> %%-------------------------------------------------------------------- %% @doc Add a static subscription manually. --spec add_subscription(mqtt_subscription()) -> {atom, ok}. +-spec add_subscription(mqtt_subscription()) -> ok | {error, already_existed}. add_subscription(Subscription = #mqtt_subscription{subid = SubId, topic = Topic}) -> Pattern = match_pattern(SubId, Topic), - mnesia:transaction( - fun() -> - case mnesia:match_object(backend_subscription, Pattern, write) of - [] -> - mnesia:write(backend_subscription, Subscription, write); - [Subscription] -> - mnesia:abort({error, existed}); - [Subscription1] -> %% QoS is different - mnesia:delete_object(backend_subscription, Subscription1, write), - mnesia:write(backend_subscription, Subscription, write) - end - end). + return(mnesia:transaction(fun() -> + case mnesia:match_object(backend_subscription, Pattern, write) of + [] -> + mnesia:write(backend_subscription, Subscription, write); + [Subscription] -> + mnesia:abort(already_existed); + [Subscription1] -> %% QoS is different + mnesia:delete_object(backend_subscription, Subscription1, write), + mnesia:write(backend_subscription, Subscription, write) + end + end)). %% @doc Lookup static subscriptions. -spec lookup_subscriptions(binary()) -> list(mqtt_subscription()). @@ -73,12 +72,12 @@ lookup_subscriptions(ClientId) when is_binary(ClientId) -> %% @doc Delete static subscriptions by ClientId manually. -spec del_subscriptions(binary()) -> ok. del_subscriptions(ClientId) when is_binary(ClientId) -> - mnesia:transaction(fun mnesia:delete/1, [{backend_subscription, ClientId}]). + return(mnesia:transaction(fun mnesia:delete/1, [{backend_subscription, ClientId}])). %% @doc Delete a static subscription manually. -spec del_subscription(binary(), binary()) -> ok. del_subscription(ClientId, Topic) when is_binary(ClientId) andalso is_binary(Topic) -> - mnesia:transaction(fun del_subscription_/1, [match_pattern(ClientId, Topic)]). + return(mnesia:transaction(fun del_subscription_/1, [match_pattern(ClientId, Topic)])). del_subscription_(Pattern) -> lists:foreach(fun(Subscription) -> @@ -88,3 +87,5 @@ del_subscription_(Pattern) -> match_pattern(SubId, Topic) -> #mqtt_subscription{subid = SubId, topic = Topic, qos = '_'}. +return({atomic, ok}) -> ok; +return({aborted, Reason}) -> {error, Reason}. From 978413298debc45faea5644f5fdc2eb9f49e98eb Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 11 Mar 2016 23:45:03 +0800 Subject: [PATCH 66/69] HTTP Publish: support 'topics' parameter --- src/emqttd_http.erl | 71 +++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/src/emqttd_http.erl b/src/emqttd_http.erl index 36dc778d9..794193a5d 100644 --- a/src/emqttd_http.erl +++ b/src/emqttd_http.erl @@ -44,27 +44,9 @@ handle_request('GET', "/status", Req) -> %%-------------------------------------------------------------------- handle_request('POST', "/mqtt/publish", Req) -> - Params = mochiweb_request:parse_post(Req), - lager:info("HTTP Publish: ~p", [Params]), case authorized(Req) of - true -> - ClientId = get_value("client", Params, http), - Qos = int(get_value("qos", Params, "0")), - Retain = bool(get_value("retain", Params, "0")), - Topic = list_to_binary(get_value("topic", Params)), - Payload = list_to_binary(get_value("message", Params)), - case {validate(qos, Qos), validate(topic, Topic)} of - {true, true} -> - Msg = emqttd_message:make(ClientId, Qos, Topic, Payload), - emqttd:publish(Msg#mqtt_message{retain = Retain}), - Req:ok({"text/plain", <<"ok">>}); - {false, _} -> - Req:respond({400, [], <<"Bad QoS">>}); - {_, false} -> - Req:respond({400, [], <<"Bad Topic">>}) - end; - false -> - Req:respond({401, [], <<"Fobbiden">>}) + true -> http_publish(Req); + false -> Req:respond({401, [], <<"Fobbiden">>}) end; %%-------------------------------------------------------------------- @@ -97,9 +79,53 @@ handle_request(Method, Path, Req) -> lager:error("Unexpected HTTP Request: ~s ~s", [Method, Path]), Req:not_found(). +%%-------------------------------------------------------------------- +%% HTTP Publish +%%-------------------------------------------------------------------- + +http_publish(Req) -> + Params = mochiweb_request:parse_post(Req), + lager:info("HTTP Publish: ~p", [Params]), + Topics = topics(Params), + ClientId = get_value("client", Params, http), + Qos = int(get_value("qos", Params, "0")), + Retain = bool(get_value("retain", Params, "0")), + Payload = list_to_binary(get_value("message", Params)), + case {validate(qos, Qos), validate(topics, Topics)} of + {true, true} -> + lists:foreach(fun(Topic) -> + Msg = emqttd_message:make(ClientId, Qos, Topic, Payload), + emqttd:publish(Msg#mqtt_message{retain = Retain}) + end, Topics), + Req:ok({"text/plain", <<"ok">>}); + {false, _} -> + Req:respond({400, [], <<"Bad QoS">>}); + {_, false} -> + Req:respond({400, [], <<"Bad Topics">>}) + end. + +topics(Params) -> + Tokens = [get_value("topic", Params) | string:tokens(get_value("topics", Params, ""), ",")], + [list_to_binary(Token) || Token <- Tokens, Token =/= undefined]. + +validate(qos, Qos) -> + (Qos >= ?QOS_0) and (Qos =< ?QOS_2); + +validate(topics, [Topic|Left]) -> + case validate(topic, Topic) of + true -> validate(topics, Left); + false -> false + end; +validate(topics, []) -> + true; + +validate(topic, Topic) -> + emqttd_topic:validate({name, Topic}). + %%-------------------------------------------------------------------- %% basic authorization %%-------------------------------------------------------------------- + authorized(Req) -> case Req:get_header_value("Authorization") of undefined -> @@ -118,11 +144,6 @@ authorized(Req) -> user_passwd(BasicAuth) -> list_to_tuple(binary:split(base64:decode(BasicAuth), <<":">>)). -validate(qos, Qos) -> - (Qos >= ?QOS_0) and (Qos =< ?QOS_2); - -validate(topic, Topic) -> - emqttd_topic:validate({name, Topic}). int(S) -> list_to_integer(S). From c626eadad45854c59ab166e26b11bcffd1130bd7 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 11 Mar 2016 23:46:27 +0800 Subject: [PATCH 67/69] 0.17.0 config --- rel/files/emqttd.config.development | 9 +++------ rel/files/emqttd.config.production | 22 ++++++++-------------- rel/files/emqttd.test.config | 17 ++++------------- 3 files changed, 15 insertions(+), 33 deletions(-) diff --git a/rel/files/emqttd.config.development b/rel/files/emqttd.config.development index 9fce5b427..d0666ae46 100644 --- a/rel/files/emqttd.config.development +++ b/rel/files/emqttd.config.development @@ -79,7 +79,7 @@ %% Max ClientId Length Allowed {max_clientid_len, 1024}, %% Max Packet Size Allowed, 64K default - {max_packet_size, 65536} + {max_packet_size, 65536} ]}, %% Client {client, [ @@ -176,11 +176,8 @@ %% Static subscriptions from backend backend, - %% $u will be replaced with username - {"$Q/username/$u", 1}, - - %% $c will be replaced with clientid - {"$Q/client/$c", 1} + %% $c will be replaced by clientid + {"$queue/clients/$c", 1} ]} %% Rewrite rules diff --git a/rel/files/emqttd.config.production b/rel/files/emqttd.config.production index 095c66550..b1ad6122b 100644 --- a/rel/files/emqttd.config.production +++ b/rel/files/emqttd.config.production @@ -77,9 +77,6 @@ {client, [ %% Socket is connected, but no 'CONNECT' packet received {idle_timeout, 20} %% seconds - %TODO: Network ingoing limit - %{ingoing_rate_limit, '64KB/s'} - %TODO: Reconnet control ]}, %% Session {session, [ @@ -164,19 +161,16 @@ {modules, [ %% Client presence management module. %% Publish messages when client connected or disconnected - {presence, [{qos, 0}]} + {presence, [{qos, 0}]}, %% Subscribe topics automatically when client connected - %% {subscription, [ - %% %% Static subscriptions from backend - %% backend, - %% - %% %% $u will be replaced with username - %% {"$Q/username/$u", 1}, - %% - %% %% $c will be replaced with clientid - %% {"$Q/client/$c", 1} - %% ]} + {subscription, [ + %% Static subscriptions from backend + backend, + + %% $c will be replaced by clientid + {"$queue/clients/$c", 1} + ]} %% Rewrite rules %% {rewrite, [{file, "etc/rewrite.config"}]} diff --git a/rel/files/emqttd.test.config b/rel/files/emqttd.test.config index 496648af6..f837d07ac 100644 --- a/rel/files/emqttd.test.config +++ b/rel/files/emqttd.test.config @@ -151,15 +151,6 @@ {pubsub, [ %% Default should be scheduler numbers {pool_size, 8}, - - %% Subscription: disc | ram | false - {subscription, ram}, - - %% Route shard - {route_shard, false}, - - %% Route delay, false | integer - {route_delay, false}, %% Route aging time(seconds) {route_aging, 5} @@ -182,14 +173,14 @@ %% Subscribe topics automatically when client connected {subscription, [ - %% Subscription from stored table - stored, + %% Static subscriptions from backend + backend, %% $u will be replaced with username - {"$Q/username/$u", 1}, + {"$queue/username/$u", 1}, %% $c will be replaced with clientid - {"$Q/client/$c", 1} + {"$queue/clients/$c", 1} ]} %% Rewrite rules From c94f6f30dd874edae6f3a7cc8621022dc48211ae Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 11 Mar 2016 23:48:56 +0800 Subject: [PATCH 68/69] ct - emqttd.test.config --- Makefile | 4 ++-- rebar.config | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 8c5f84fd9..c1a173c07 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BASE_DIR = $(shell pwd) REBAR = $(BASE_DIR)/rebar DIST = $(BASE_DIR)/rel/$(APP) -all: submods compile +all: compile submods: @git submodule update --init @@ -26,7 +26,7 @@ clean: @$(REBAR) clean test: - ERL_FLAGS="-config rel/files/test.config" $(REBAR) -v skip_deps=true ct + ERL_FLAGS="-config rel/files/emqttd.test.config" $(REBAR) -v skip_deps=true ct #$(REBAR) skip_deps=true eunit edoc: diff --git a/rebar.config b/rebar.config index 695b2dd0d..fc6762faf 100644 --- a/rebar.config +++ b/rebar.config @@ -23,7 +23,7 @@ {ct_log_dir, "logs"}. -{ct_extra_params, "-name ct_emqttd@127.0.0.1 -config rel/files/test.config"}. +{ct_extra_params, "-name ct_emqttd@127.0.0.1 -config rel/files/emqttd.test.config"}. {ct_use_short_names, false}. From a49006779b329b7baf0c997875710fcec73ad517 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 11 Mar 2016 23:52:50 +0800 Subject: [PATCH 69/69] 0.17.0 --- CHANGELOG.md | 10 +++++++++- Makefile | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 937b1629f..a358de727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ emqttd ChangeLog -================= +================ + +0.17.0-beta(2016-03-12) +------------------------ + +Full Documents released on https://docs.emqtt.com + +Improve the design of Hook, PubSub and Router + 0.16.0-beta(2016-02-16) ------------------------ diff --git a/Makefile b/Makefile index c1a173c07..659126c0d 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BASE_DIR = $(shell pwd) REBAR = $(BASE_DIR)/rebar DIST = $(BASE_DIR)/rel/$(APP) -all: compile +all: submods compile submods: @git submodule update --init