前言
關於EMQ和erlang的資料感覺實在是太少了,爲啥EMQ不搞個論壇啥的,這樣就不用噁心的度娘了。。
然後,本文記錄也只是說,怎麼去改,怎麼用,具體很多深層次的東西,我也暫時還沒去深究,後續有時間,再一點點研究,其他有些可能與實際說得有出入,還請見諒,指出,讓我好糾正。
(其實,到最後測試成功,我才發現,我這幾天白搞了,完全可以用另外一種方式實現mqtt->kafka,不需要編寫插件,這是題外話了,後面再說)
準備
在編寫插件之前,首先得先保證自己有以下的知識點
插件結構
在emq源碼編譯後,在emq根目錄(emq-relx),會出現一個deps文件夾,這個就是emq在編譯過程中,通過git下載下來的,裏面包含了各種插件的源碼。
這裏着重看下deps文件夾下的emq_plugin_template文件夾,這個就是官網提供的編寫emq插件的模板,來看看裏面的結構
其他那些什麼中間文件,git附屬文件,我就不說了,沒用到。
主要在來看看src文件夾
因爲我主要實現數據採集,所以這裏就沒多關注訂閱發佈權限驗證和登錄授權驗證這一塊,我覺得這一塊,emq都提供了相應的插件去實現了,也就沒有必要再重複造輪子了。所以我直接忽略了權限驗證這塊,衝着數據採集去。
插件創建
cp -r emq_plugin_template emq_plugin_kafka
先修改src/emq_plugin_template_app.erl文件,把acl和auth的模塊註冊代碼去掉,並加些打印語句。
-module(emq_plugin_template_app).
-behaviour(application).
%% Application callbacks
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
{ok, Sup} = emq_plugin_template_sup:start_link(),
emq_plugin_template:load(application:get_all_env()),
io:format("emq_plugin_kafka start.~n", []),
{ok, Sup}.
stop(_State) ->
emq_plugin_template:unload(),
io:format("emq_plugin_kafka stop.~n", []).
(當然,居然acl和auth模塊沒用的,看着礙眼,就直接把文件刪掉吧)接下來乾點體力活,把插件目錄emq_plugin_kafka下的所有文件名和裏面內容的“template”全部改成“kafka”,包括src目錄下的所有文件,Makefile文件,etc下的配置文件等。
順利的話,這個時候插件的雛形算是完成了,把插件掛載到emq上,測試啓動下。
在emq的根目錄的rebar.config文件裏面{emq_plugin_template, load}下一行,加入該插件。
{emq_plugin_kafka, load}
還有再Makefile文件內的DEPS加上emq_plugin_kafka
DEPS += emqttd emq_modules emq_dashboard emq_retainer emq_recon emq_reloader \
emq_auth_clientid emq_auth_username emq_auth_ldap emq_auth_http \
emq_auth_mysql emq_auth_pgsql emq_auth_redis emq_auth_mongo \
emq_sn emq_coap emq_stomp emq_plugin_template emq_web_hook \
emq_lua_hook emq_auth_jwt emq_plugin_kafka
爲了方便查看插件是否加載成功,我們讓插件隨emq啓動就默認加載:
在data/loaded_plugins文件內加上插件
vi data/loaded_plugins
emq_recon.
emq_modules.
emq_retainer.
emq_dashboard.
emq_plugin_kafka.
最後,在emq根目錄下,make一下。
如果編譯不成功,就繼續看看那些文件或內容有沒有改對吧。
控制檯模式啓動emq:
./_rel/emqttd/bin/emqttd console
啓動後,看到我們寫的打印語句,就證明插件加載成功了。
你也可以用瀏覽器打開控制檯,ip:18083,手動來啓用和停止插件
編寫代碼連接Kafka
這裏我就不介紹kafka+zookeeper的搭建了,網上各位大神的博客很多,可以對照着搭建
相關zookeeper的集羣搭建,可以看我另外一篇博客(^^)。
OK,假設你已經搭建好了kafka環境了,接下來讓emq連接到kafka
用到的一個工具是ekaf:https://github.com/helpshift/ekaf
這個工具是用erlang語言寫的,用於連接kafka的,關於這個工具,後面我會再講兩句。
整改配置文件
關於配置文件,簡單說兩句:可以看看官網關於配置文件的變更歷史,後面會創建一個schema文件。
進入插件目錄emq_plugin_kafka的配置文件目錄 emq_plugin_kafka/etc
先把文件名改成後綴爲conf的格式(劃重點!!!!!= = 我被這一點坑了很久,Makefile關於配置文件的轉換,是讀取.conf文件的,然後這裏是config一直未發現。當然,你可以用erlang原生的配置文件方式,就不用大費周章,但是這裏還是遵循emq的統一作風。)
mv emq_plugin_kafka.config emq_plugin_kafka.conf
然後編輯內容,把內容改成如下(server地址寫成你自己的):
emq.plugin.kafka.server = 192.168.52.130:9092
emq.plugin.kafka.topic = test-topic2
這裏先不要着急去思考關於Kafka集羣的操作,後面再講。
然後新建schema文件 emq_plugin_kafka/priv/emq_plugin_kafka.schema,添加以下內容:
{mapping, "emq.plugin.kafka.server", "emq_plugin_kafka.server", [
{default, {"127.0.0.1", 6379}},
{datatype, [integer, ip, string]}
]}.
%% emq.msg.kafka.topic
{mapping, "emq.plugin.kafka.topic", "emq_plugin_kafka.server", [
{default, "test"},
{datatype, string},
hidden
]}.
{
translation,
"emq_plugin_kafka.server",
fun(Conf) ->
{RHost, RPort} = case cuttlefish:conf_get("emq.plugin.kafka.server", Conf) of
{Ip, Port} -> {Ip, Port};
S -> case string:tokens(S, ":") of
[Domain] -> {Domain, 9092};
[Domain, Port] -> {Domain, list_to_integer(Port)}
end
end,
Topic = cuttlefish:conf_get("emq.plugin.kafka.topic", Conf),
[
{host, RHost},
{port, RPort},
{topic, Topic}
]
end
}.
編輯插件的Makefile文件,增加ekaf依賴
PROJECT = emq_plugin_kafka
PROJECT_DESCRIPTION = EMQ Plugin Kafka
PROJECT_VERSION = 2.3.10
BUILD_DEPS = emqttd cuttlefish ekaf
dep_emqttd = git https://github.com/emqtt/emqttd master
dep_cuttlefish = git https://github.com/emqtt/cuttlefish
dep_ekaf = git https://github.com/helpshift/ekaf master
ERLC_OPTS += +debug_info
ERLC_OPTS += +'{parse_transform, lager_transform}'
NO_AUTOPATCH = cuttlefish
COVER = true
include erlang.mk
app:: rebar.config
app.config::
./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/emq_plugin_kafka.conf -i priv/emq_plugin_kafka.schema -d data
編寫邏輯代碼
編輯src/emq_plugin_kafka.erl文件,改爲如下:
-module(emq_plugin_kafka).
-include_lib("emqttd/include/emqttd.hrl").
-define(APP, emq_plugin_kafka).
-export([load/1, unload/0]).
%% Hooks functions
-export([on_client_connected/3, on_client_disconnected/3]).
-export([on_client_subscribe/4, on_client_unsubscribe/4]).
-export([on_session_created/3, on_session_subscribed/4, on_session_unsubscribed/4, on_session_terminated/4]).
-export([on_message_publish/2, on_message_delivered/4, on_message_acked/4]).
%% Called when the plugin application start
load(Env) ->
ekaf_init(Env),
emqttd:hook('client.connected', fun ?MODULE:on_client_connected/3, [Env]),
emqttd:hook('client.disconnected', fun ?MODULE:on_client_disconnected/3, [Env]),
emqttd:hook('client.subscribe', fun ?MODULE:on_client_subscribe/4, [Env]),
emqttd:hook('client.unsubscribe', fun ?MODULE:on_client_unsubscribe/4, [Env]),
emqttd:hook('session.created', fun ?MODULE:on_session_created/3, [Env]),
emqttd:hook('session.subscribed', fun ?MODULE:on_session_subscribed/4, [Env]),
emqttd:hook('session.unsubscribed', fun ?MODULE:on_session_unsubscribed/4, [Env]),
emqttd:hook('session.terminated', fun ?MODULE:on_session_terminated/4, [Env]),
emqttd:hook('message.publish', fun ?MODULE:on_message_publish/2, [Env]),
emqttd:hook('message.delivered', fun ?MODULE:on_message_delivered/4, [Env]),
emqttd:hook('message.acked', fun ?MODULE:on_message_acked/4, [Env]).
on_client_connected(ConnAck, Client = #mqtt_client{client_id = ClientId}, _Env) ->
io:format("client ~s connected, connack: ~w~n", [ClientId, ConnAck]),
{ok, Client}.
on_client_disconnected(Reason, _Client = #mqtt_client{client_id = ClientId}, _Env) ->
io:format("client ~s disconnected, reason: ~w~n", [ClientId, Reason]),
ok.
on_client_subscribe(ClientId, Username, TopicTable, _Env) ->
io:format("client(~s/~s) will subscribe: ~p~n", [Username, ClientId, TopicTable]),
{ok, TopicTable}.
on_client_unsubscribe(ClientId, Username, TopicTable, _Env) ->
io:format("client(~s/~s) unsubscribe ~p~n", [ClientId, Username, TopicTable]),
{ok, TopicTable}.
on_session_created(ClientId, Username, _Env) ->
io:format("session(~s/~s) created.", [ClientId, Username]).
on_session_subscribed(ClientId, Username, {Topic, Opts}, _Env) ->
io:format("session(~s/~s) subscribed: ~p~n", [Username, ClientId, {Topic, Opts}]),
{ok, {Topic, Opts}}.
on_session_unsubscribed(ClientId, Username, {Topic, Opts}, _Env) ->
io:format("session(~s/~s) unsubscribed: ~p~n", [Username, ClientId, {Topic, Opts}]),
ok.
on_session_terminated(ClientId, Username, Reason, _Env) ->
io:format("session(~s/~s) terminated: ~p.", [ClientId, Username, Reason]).
%% transform message and return
on_message_publish(Message = #mqtt_message{topic = <<"$SYS/", _/binary>>}, _Env) ->
{ok, Message};
on_message_publish(Message, _Env) ->
io:format("publish ~s~n", [emqttd_message:format(Message)]),
ekaf_send(Message, _Env),
{ok, Message}.
on_message_delivered(ClientId, Username, Message, _Env) ->
io:format("delivered to client(~s/~s): ~s~n", [Username, ClientId, emqttd_message:format(Message)]),
{ok, Message}.
on_message_acked(ClientId, Username, Message, _Env) ->
io:format("client(~s/~s) acked: ~s~n", [Username, ClientId, emqttd_message:format(Message)]),
{ok, Message}.
%% Called when the plugin application stop
unload() ->
emqttd:unhook('client.connected', fun ?MODULE:on_client_connected/3),
emqttd:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3),
emqttd:unhook('client.subscribe', fun ?MODULE:on_client_subscribe/4),
emqttd:unhook('client.unsubscribe', fun ?MODULE:on_client_unsubscribe/4),
emqttd:unhook('session.created', fun ?MODULE:on_session_created/3),
emqttd:unhook('session.subscribed', fun ?MODULE:on_session_subscribed/4),
emqttd:unhook('session.unsubscribed', fun ?MODULE:on_session_unsubscribed/4),
emqttd:unhook('session.terminated', fun ?MODULE:on_session_terminated/4),
emqttd:unhook('message.publish', fun ?MODULE:on_message_publish/2),
emqttd:unhook('message.delivered', fun ?MODULE:on_message_delivered/4),
emqttd:unhook('message.acked', fun ?MODULE:on_message_acked/4).
ekaf_init(_Env) ->
{ok, Kafka_Env} = application:get_env(?APP, server),
Host = proplists:get_value(host, Kafka_Env),
Port = proplists:get_value(port, Kafka_Env),
Broker = {Host, Port},
%Broker = {"192.168.52.130", 9092},
Topic = proplists:get_value(topic, Kafka_Env),
%Topic = "test-topic",
application:set_env(ekaf, ekaf_partition_strategy, strict_round_robin),
application:set_env(ekaf, ekaf_bootstrap_broker, Broker),
application:set_env(ekaf, ekaf_bootstrap_topics, list_to_binary(Topic)),
%%設置數據上報間隔,ekaf默認是數據達到1000條或者5秒,觸發上報
application:set_env(ekaf, ekaf_buffer_ttl, 100),
{ok, _} = application:ensure_all_started(ekaf).
%io:format("Init ekaf with ~p~n", [Broker]),
%Json = mochijson2:encode([
% {type, <<"connected">>},
% {client_id, <<"test-client_id">>},
% {cluster_node, <<"node">>}
%]),
%io:format("send : ~w.~n",[ekaf:produce_async_batched(list_to_binary(Topic), list_to_binary(Json))]).
ekaf_send(Message, _Env) ->
From = Message#mqtt_message.from,
Topic = Message#mqtt_message.topic,
Payload = Message#mqtt_message.payload,
Qos = Message#mqtt_message.qos,
Dup = Message#mqtt_message.dup,
Retain = Message#mqtt_message.retain,
ClientId = get_form_clientid(From),
Username = get_form_username(From),
io:format("message receive : ~n",[]),
io:format("From : ~w~n",[From]),
io:format("Topic : ~w~n",[Topic]),
io:format("Payload : ~w~n",[Payload]),
io:format("Qos : ~w~n",[Qos]),
io:format("Dup : ~w~n",[Dup]),
io:format("Retain : ~w~n",[Retain]),
io:format("ClientId : ~w~n",[ClientId]),
io:format("Username : ~w~n",[Username]),
Str = [
{client_id, ClientId},
{message, [
{username, Username},
{topic, Topic},
{payload, Payload},
{qos, Qos},
{dup, Dup},
{retain, Retain}
]},
{cluster_node, node()},
{ts, emqttd_time:now_ms()}
],
io:format("Str : ~w.~n", [Str]),
Json = mochijson2:encode(Str),
KafkaTopic = get_topic(),
ekaf:produce_sync_batched(KafkaTopic, list_to_binary(Json)).
get_form_clientid({ClientId, Username}) -> ClientId;
get_form_clientid(From) -> From.
get_form_username({ClientId, Username}) -> Username;
get_form_username(From) -> From.
get_topic() ->
{ok, Topic} = application:get_env(ekaf, ekaf_bootstrap_topics),
Topic.
修改完成後,現在插件目錄下,make一下,成功編譯確保代碼編寫沒問題。。
最後修改emq根目錄下的配置文件,在relx.config文件下,添加ekaf依賴,就跟之前添加emq_plugin_kafka依賴一樣。
OK,安全起見,把_rel文件夾刪掉,最後make一下,理論上是會編譯成功的。
編譯成功之後運行_rel目錄下的emqttd,一切順利,就可以啓動成功。
用emq控制檯的websocket來連接emq和發送消息
我這裏接收Kafka的消息,沒有用到kafka的消費者客戶端,是自己編寫java代碼,去接受數據的,如果你習慣用kafka的消費者客戶端,訂閱相應的topic,也是可以收到一樣的的消息的。
OK,至此,demo版本完成,後續的就看自己項目需求發揮了。
最後說點
1、其實感覺沒必要在emq下編寫kafka插件,比如java,自己寫一個程序去連接mqtt訂閱相關消息,然後丟到kafka中,這樣不是更加省時方便嗎?
2、關於ekaf,我只是淺顯調用了發送的代碼,具體其他一些配置項和接口,可以看看ekaf的源代碼ekaf.erl和ekaf_lib.erl,
關於ekaf怎麼設置kafka的集羣,作者給出的答案是,ekaf目前不支持ekaf設置集羣地址,而是建立一個kafka集羣的LB,ekaf連接LB的地址,LB自己去連接kafka集羣。
附
插件代碼下載地址:https://download.csdn.net/download/caijiapeng0102/10513808