EMQ集成Kafka插件編寫過程 emq_plugin_kafka

前言

        關於EMQ和erlang的資料感覺實在是太少了,爲啥EMQ不搞個論壇啥的,這樣就不用噁心的度娘了。。

        然後,本文記錄也只是說,怎麼去改,怎麼用,具體很多深層次的東西,我也暫時還沒去深究,後續有時間,再一點點研究,其他有些可能與實際說得有出入,還請見諒,指出,讓我好糾正。

(其實,到最後測試成功,我才發現,我這幾天白搞了,完全可以用另外一種方式實現mqtt->kafka,不需要編寫插件,這是題外話了,後面再說)

準備

在編寫插件之前,首先得先保證自己有以下的知識點

  1. erlang語言的基本知識
  2. emq架構瞭解 -> 傳送門
  3. emq源碼編譯 -> 傳送門

插件結構

在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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章