物联网架构成长之路(8)-EMQ-Hook了解、连接Kafka发送消息
1. 前言
按照我自己设计的物联网框架,对于MQTT集群中的所有消息,是要持久化到磁盘的,这里采用一个消息队列中间件Kafka作为数据缓冲,缓冲结果存到数据仓库中,以供后续作为数据分析。由于MQTT集群中的消息都是比较分散的,所以使用Kafka来聚合、采集消息。
2. 下载&编译&安装
Kafka依赖ZooKeeper
在这里下载 http://mirrors.shuosc.org/apache/zookeeper/zookeeper-3.4.11/zookeeper-3.4.11.tar.gz
http://mirrors.shuosc.org/apache/kafka/1.0.0/kafka-1.0.0-src.tgz
http://mirror.bit.edu.cn/apache/kafka/1.0.0/kafka_2.12-1.0.0.tgz
学习的话, 可以参考这个文档 http://orchome.com/kafka/index
配置使用过程可以参考官网的 http://kafka.apache.org/quickstart http://zookeeper.apache.org/doc/current/zookeeperStarted.html ,有些资料因为版本升级的原因,已经不是以前的那种启动方式了。https://cwiki.apache.org/confluence/display/KAFKA/Clients
3. 启动Zookeeper
cp ./conf/zoo_sample.cfg ./conf/zoo.cfg
./bin/zkServer.sh start
上图,就表示启动单机模式。
./bin/zkCli.sh -server 127.0.0.1:2181 进行连接
然后进入命令行模式,可以慢慢玩了
单击模式就没有什么要配置的。最多修改zoo.cfg中的dataDir文件
ZooKeeper启动replicated模式,集群模式
zoo.cfg 增加服务器集群信息
server.=172.16.20.229::
server.=172.16.23.203::
server.=172.16.23.204::
./bin/zkServer.sh start-foreground #启动
注意在echo “1” > %dataDir%/myid 对于每个服务器都要创建一个myid文件
启动都是会有一些奇奇怪怪的问题,上网找资料就可以了。
一般第一台ZooKeeper启动是会有Connection refused 出错,这个是正常的,后面的两台还没有启动,不过后面也一个一个启动了。
如果过程中,有一个断开了,然后修改数据,然后这个断开的又连上了,那么ZooKeeper集群内部会镜像diff
-- ::, [myid:] - INFO [QuorumPeer[myid=]/0.0.0.0::Learner@] - Getting a diff from the leader 0x100000005
然后就用客户端A
./bin/zkCli.sh -server 172.16.20.229:
ls /
create /zk_test my_data
ls /
然后用客户端B
./bin/zkCli.sh -server 172.16.23.203:
ls /
get /zk_test
ls /
可以看到ZooKeeper信息在内部进行了共享
具体可以参考这篇博客 http://www.cnblogs.com/sunddenly/p/4018459.html
4. 启动 Kafka
由于kafka以来ZooKeeper,所以有了上面一步的ZooKeeper了解。
实际中,可以直接下载kafka的二进制包,直接使用,http://mirror.bit.edu.cn/apache/kafka/1.0.0/kafka_2.12-1.0.0.tgz
启动Zookeeper
./bin/zookeeper-server-start.sh config/zookeeper.properties
启动Kafka
./bin/kafka-server-start.sh config/server.properties
创建主题
./bin/kafka-topics.sh --create --zookeeper localhost: --replication-factor --partitions --topic test
查看主题
./bin/kafka-topics.sh --list --zookeeper localhost:
发送消息
./bin/kafka-console-producer.sh --broker-list localhost: --topic test
消费消息
./bin/kafka-console-consumer.sh --zookeeper localhost: --topic test --from-beginning
5. Erlang 连接 kafka
主要参考这个 https://github.com/msdevanms/emqttd_plugin_kafka_bridge
增加依赖 https://github.com/helpshift/ekaf.git
首先在Makefile增加
DEPS = eredis ecpool clique ekaf
dep_ekaf = git https://github.com/helpshift/ekaf master
然后在rebar.config 增加
{ekaf, “.*”, {git, “https://github.com/helpshift/ekaf”, “master”}}
在etc/emq_plugin_wunaozai.conf 中增加
## kafka config
wunaozai.msg.kafka.server = 127.0.0.1:
wunaozai.msg.kafka.topic = test
在priv/emq_plugin_wunaozai.schema 中增加
%% wunaozai.msg.kafka.server = 127.0.0.1:
{
mapping,
"wunaozai.msg.kafka.server",
"emq_plugin_wunaozai.kafka",
[
{default, {"127.0.0.1", }},
{datatype, [integer, ip, string]}
]
}. %% wunaozai.msg.kafka.topic = test
{
mapping,
"wunaozai.msg.kafka.topic",
"emq_plugin_wunaozai.kafka",
[
{default, "test"},
{datatype, string},
hidden
]
}. %% translation
{
translation,
"emq_plugin_wunaozai.kafka",
fun(Conf) ->
{RHost, RPort} = case cuttlefish:conf_get("wunaozai.msg.kafka.server", Conf) of
{Ip, Port} -> {Ip, Port};
S -> case string:tokens(S, ":") of
[Domain] -> {Domain, };
[Domain, Port] -> {Domain, list_to_integer(Port)}
end
end,
Topic = cuttlefish:conf_get("wunaozai.msg.kafka.topic", Conf),
[
{host, RHost},
{port, RPort},
{topic, Topic}
]
end
}.
6. 数据发往Kafka
接下来,由于功能基本上是基于EMQ框架的Hook钩子设计,在EMQ接收到客户端上下线、主题订阅或消息发布确认时,触发钩子顺序执行回调函数,所以大部分功能在 src/emq_plugin_wunaozai.erl 文件进行修改。
-module(emq_plugin_wunaozai). -include("emq_plugin_wunaozai.hrl"). -include_lib("emqttd/include/emqttd.hrl"). -export([load/, unload/]). %% Hooks functions -export([on_client_connected/, on_client_disconnected/]). -export([on_client_subscribe/, on_client_unsubscribe/]). -export([on_session_created/, on_session_subscribed/, on_session_unsubscribed/, on_session_terminated/]). -export([on_message_publish/, on_message_delivered/, on_message_acked/]). %% Called when the plugin application start
load(Env) ->
ekaf_init([Env]),
emqttd:hook('client.connected', fun ?MODULE:on_client_connected/, [Env]),
emqttd:hook('client.disconnected', fun ?MODULE:on_client_disconnected/, [Env]),
emqttd:hook('client.subscribe', fun ?MODULE:on_client_subscribe/, [Env]),
emqttd:hook('client.unsubscribe', fun ?MODULE:on_client_unsubscribe/, [Env]),
emqttd:hook('session.created', fun ?MODULE:on_session_created/, [Env]),
emqttd:hook('session.subscribed', fun ?MODULE:on_session_subscribed/, [Env]),
emqttd:hook('session.unsubscribed', fun ?MODULE:on_session_unsubscribed/, [Env]),
emqttd:hook('session.terminated', fun ?MODULE:on_session_terminated/, [Env]),
emqttd:hook('message.publish', fun ?MODULE:on_message_publish/, [Env]),
emqttd:hook('message.delivered', fun ?MODULE:on_message_delivered/, [Env]),
emqttd:hook('message.acked', fun ?MODULE:on_message_acked/, [Env]),
io:format("start wunaozai Test Reload.~n", []). on_client_connected(ConnAck, Client = #mqtt_client{client_id = ClientId}, _Env) ->
io:format("client ~s connected, connack: ~w~n", [ClientId, ConnAck]),
ekaf_send(<<"connected">>, ClientId, {}, _Env),
{ok, Client}. on_client_disconnected(Reason, _Client = #mqtt_client{client_id = ClientId}, _Env) ->
io:format("client ~s disconnected, reason: ~w~n", [ClientId, Reason]),
ekaf_send(<<"disconnected">>, ClientId, {}, _Env),
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}]),
ekaf_send(<<"subscribed">>, ClientId, {Topic, Opts}, _Env),
{ok, {Topic, Opts}}. on_session_unsubscribed(ClientId, Username, {Topic, Opts}, _Env) ->
io:format("session(~s/~s) unsubscribed: ~p~n", [Username, ClientId, {Topic, Opts}]),
ekaf_send(<<"unsubscribed">>, ClientId, {Topic, Opts}, _Env),
ok. on_session_terminated(ClientId, Username, Reason, _Env) ->
io:format("session(~s/~s) terminated: ~p.~n", [ClientId, Username, Reason]),
stop. %% 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(<<"public">>, {}, 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/),
emqttd:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/),
emqttd:unhook('client.subscribe', fun ?MODULE:on_client_subscribe/),
emqttd:unhook('client.unsubscribe', fun ?MODULE:on_client_unsubscribe/),
emqttd:unhook('session.created', fun ?MODULE:on_session_created/),
emqttd:unhook('session.subscribed', fun ?MODULE:on_session_subscribed/),
emqttd:unhook('session.unsubscribed', fun ?MODULE:on_session_unsubscribed/),
emqttd:unhook('session.terminated', fun ?MODULE:on_session_terminated/),
emqttd:unhook('message.publish', fun ?MODULE:on_message_publish/),
emqttd:unhook('message.delivered', fun ?MODULE:on_message_delivered/),
emqttd:unhook('message.acked', fun ?MODULE:on_message_acked/). %% ==================== ekaf_init STA.===============================%%
ekaf_init(_Env) ->
% clique 方式读取配置文件
Env = application:get_env(?APP, kafka),
{ok, Kafka} = Env,
Host = proplists:get_value(host, Kafka),
Port = proplists:get_value(port, Kafka),
Broker = {Host, Port},
Topic = proplists:get_value(topic, Kafka),
io:format("~w ~w ~w ~n", [Host, Port, Topic]), % init kafka
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)),
%application:set_env(ekaf, ekaf_bootstrap_broker, {"127.0.0.1", }),
%application:set_env(ekaf, ekaf_bootstrap_topics, <<"test">>), %io:format("Init ekaf with ~s:~b~n", [Host, Port]),
%%ekaf:produce_async_batched(<<"test">>, list_to_binary(Json)),
ok.
%% ==================== ekaf_init END.===============================%% %% ==================== ekaf_send STA.===============================%%
ekaf_send(Type, ClientId, {}, _Env) ->
Json = mochijson2:encode([
{type, Type},
{client_id, ClientId},
{message, {}},
{cluster_node, node()},
{ts, emqttd_time:now_ms()}
]),
ekaf_send_sync(Json);
ekaf_send(Type, ClientId, {Reason}, _Env) ->
Json = mochijson2:encode([
{type, Type},
{client_id, ClientId},
{cluster_node, node()},
{message, Reason},
{ts, emqttd_time:now_ms()}
]),
ekaf_send_sync(Json);
ekaf_send(Type, ClientId, {Topic, Opts}, _Env) ->
Json = mochijson2:encode([
{type, Type},
{client_id, ClientId},
{cluster_node, node()},
{message, [
{topic, Topic},
{opts, Opts}
]},
{ts, emqttd_time:now_ms()}
]),
ekaf_send_sync(Json);
ekaf_send(Type, _, Message, _Env) ->
Id = Message#mqtt_message.id,
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,
Timestamp = Message#mqtt_message.timestamp, ClientId = c(From),
Username = u(From), Json = mochijson2:encode([
{type, Type},
{client_id, ClientId},
{message, [
{username, Username},
{topic, Topic},
{payload, Payload},
{qos, i(Qos)},
{dup, i(Dup)},
{retain, i(Retain)}
]},
{cluster_node, node()},
{ts, emqttd_time:now_ms()}
]),
ekaf_send_sync(Json). ekaf_send_async(Msg) ->
Topic = ekaf_get_topic(),
ekaf_send_async(Topic, Msg).
ekaf_send_async(Topic, Msg) ->
ekaf:produce_async_batched(list_to_binary(Topic), list_to_binary(Msg)).
ekaf_send_sync(Msg) ->
Topic = ekaf_get_topic(),
ekaf_send_sync(Topic, Msg).
ekaf_send_sync(Topic, Msg) ->
ekaf:produce_sync_batched(list_to_binary(Topic), list_to_binary(Msg)). i(true) -> ;
i(false) -> ;
i(I) when is_integer(I) -> I.
c({ClientId, Username}) -> ClientId;
c(From) -> From.
u({ClientId, Username}) -> Username;
u(From) -> From.
%% ==================== ekaf_send END.===============================%% %% ==================== ekaf_set_host STA.===============================%%
ekaf_set_host(Host) ->
ekaf_set_host(Host, ).
ekaf_set_host(Host, Port) ->
Broker = {Host, Port},
application:set_env(ekaf, ekaf_bootstrap_broker, Broker),
io:format("reset ekaf Broker ~s:~b ~n", [Host, Port]),
ok.
%% ==================== ekaf_set_host END.===============================%% %% ==================== ekaf_set_topic STA.===============================%%
ekaf_set_topic(Topic) ->
application:set_env(ekaf, ekaf_bootstrap_topics, list_to_binary(Topic)),
ok.
ekaf_get_topic() ->
Env = application:get_env(?APP, kafka),
{ok, Kafka} = Env,
Topic = proplists:get_value(topic, Kafka),
Topic.
%% ==================== ekaf_set_topic END.===============================%%
上面是所有源代码,下面对其进行简单说明
ekaf_init 函数,主要对配置文件的读取和解析并存放到application的环境变量中
ekaf_send 函数,主要是封装成对应的JSON数据,然后发到Kafka中
ekaf_send_async 函数,主要是异步发送JSON数据,不确保发往Kafka的顺序与Kafka消费者的接收时的顺序
ekaf_send_sync 函数,是同步发送JSON数据,确保按照顺序发往kafka与Kafka消费者有序接收数据
ekaf_set_host 函数,设置kafka的域名与端口
ekaf_set_topic 函数,设置发往kafka时的主题
ekaf_get_topic 函数,获取当前主题
load函数增加ekaf_init调用
剩下的在每个钩子回调中调用 ekaf_send函数
7. 测试
(1)启动Zookeeper ./bin/zookeeper-server-start.sh config/zookeeper.properties
(2)启动Kafka ./bin/kafka-server-start.sh config/server.properties
(3)启动消费者 ./bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic test --from-beginning
(4)启动一个生产者 ./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
(5)启动EMQ ./_rel/emqttd/bin/emqttd console
(6)打开MQTT客户端并连接、订阅、发布等操作
(7)可以在消费者界面上看到获取到的信息
8. 插件源码
最后给出本次插件开发的所有源码
https://files.cnblogs.com/files/wunaozai/emq_plugin_wunaozai.zip
物联网架构成长之路(8)-EMQ-Hook了解、连接Kafka发送消息的更多相关文章
- 物联网架构成长之路(31)-EMQ基于HTTP权限验证
看过之前的文章就知道,我之前是通过搞插件,或者通过里面的MongoDB来进行EMQ的鉴权登录和权限验证.但是前段时间发现,还是通过HTTP WebHook 方式来调用鉴权接口比较适合实际使用.还是实现 ...
- 物联网架构成长之路(33)-EMQ数据存储到influxDB
一.前言 时隔一年半,技术变化特别快,学习也要跟上才行.以前写过EMQ数据转存问题,当时用了比较笨的方法,通过写插件的方式,把MQTT里面的数据发送到数据库进行存储.当时也是为了学习erlang和em ...
- 物联网架构成长之路(3)-EMQ消息服务器了解
1. 了解 物联网最基础的就是通信了.通信协议,物联网协议好像有那么几个,以前各个协议都有优劣,最近一段时间,好像各大厂商都采用MQTT协议,所以我也不例外,不搞特殊,采用MQTT协议,选定了协议,接 ...
- 物联网架构成长之路(4)-EMQ插件创建
1. 说明 以下用到的知识,是建立在我目前所知道的知识领域,以后如果随着知识的拓展,不一定会更新内容.由于不是EMQ公司的人,EMQ的文档又很少,很多知识点都是靠猜的.2. 一些资料 架构设计 htt ...
- 物联网架构成长之路(6)-EMQ权限控制
1. 前言 EMQTT属于一个比较小众的开源软件,很多资料不全,很麻烦,很多功能都是靠猜测,还有就是看官方提供的那几个插件,了解. 2. 说明 上一小节的插件 emq_plugin_wunaozai ...
- 物联网架构成长之路(7)-EMQ权限验证小结
1. 前言 经过前面几小节,讲了一下插件开发,这一小节主要对一些代码和目录结构进行讲解,这些都是测试过程中一些个人经验,不一定是官方做法.而且也有可能会因为版本不一致导致差异. 2. 目录结构 这个目 ...
- 物联网架构成长之路(5)-EMQ插件配置
1. 前言 上一小结说了插件的创建,这一节主要怎么编写代码,以及具体流程之类的.2. 增加一句Hello World 修改 ./deps/emq_plugin_wunaozai/src/emq_plu ...
- 物联网架构成长之路(25)-Docker构建项目用到的镜像1
0. 前言 现在项目处于初级阶段,按照规划,先构建几个以后可能会用到的Image,并上传到阿里云的Docker仓库.以后博客中用到的Image,大部分都会用到这几个基础的Image,构建一个简单的物联 ...
- 物联网架构成长之路(35)-利用Netty解析物联网自定义协议
一.前言 前面博客大部分介绍了基于EMQ中间件,通信协议使用的是MQTT,而传输的数据为纯文本数据,采用JSON格式.这种方式,大部分一看就知道是熟悉Web开发.软件开发的人喜欢用的方式.由于我也是做 ...
随机推荐
- TotoriseGit安装
1.前提 前提是有安装过git. 2.下载 3.安装 二:参考的文档 1.不错的文档 https://www.cnblogs.com/xinlj/p/5978730.html http://blog. ...
- Linux学习之常用文件处理命令(一)
(一)文件命名规则 (二)常用文件处理命令 1.ls命令 2.cd命令 3.pwd命令 4.mkdir命令 5.touch命令 6.cp命令 7.mv命令 8.rm命令 9.cat命令 10.more ...
- 005.Docker存储管理
一 Docker volume形态 因为Docker 采用 AFUS 分层文件系统时,文件系统的改动都是发生在最上面的容器层,在容器的生命周期内,它是持续的,包括容器在被停止后.但是,当容器被删除后, ...
- window配置右键菜单
window配置右键菜单 cmd -> regeidt :打开注册表 文件右键 依次点开HKEY_CLASSES_ROOT ---> * ---> shell. 右键shell,新建 ...
- APIO2018 铜滚记
「一旦闭上双眼,就昏昏欲睡」「仿佛与这个世界的联系,被瞬间切断」「可是,负罪感与背德感又会在黑暗中将我吞噬」「即使这样,却也无法与身体的疲惫抗衡」 「如果,这些东西也无法让意识的存在稳定下来的话」「那 ...
- centos7 修改同步时间
同步时间安装ntp软件 [root@node2 ~]# yum install ntp 将ntp设为开机启动 [root@node2 ~]# systemctl enable ntpd 修改启动参数, ...
- C# 论接口的意义---共享协议
我对接口的理解: 接口是一种协议.是一种模型. 我认为接口的意义: 接口更好的实现了项目资源共享 , 指定了可共享的范围 , 允许可使用而不可篡改的项目资源 . 我认为接口和模型是一类的: 接口一般与 ...
- C++构造函数初始化列表与构造函数中的赋值的区别
C++类中成员变量的初始化有两种方式:构造函数初始化列表和构造函数体内赋值. 一.内部数据类型(char,int……指针等) class Animal { public: Animal(int wei ...
- sql ,内连接,外连接,自然连接等各种连接
1.内联接(典型的联接运算,使用像 = 或 <> 之类的比较运算符).包括相等联接和自然联接. 内联接使用比较运算符根据每个表共有的列的值匹配两个表中的行.例如,检索 students和c ...
- Java中的ReentrantLock和synchronized两种锁定
原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之 ...