运用 Java 研发 EMQ X 服务器插件
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">从 v4.1 版本<span style="color: black;">起始</span>,EMQ X <span style="color: black;">供给</span>了专门的多语言支持插件 emqx_extension_hook </span><span style="color: black;">(https://github.com/emqx/emqx-extension-hook) </span><span style="color: black;">,现已支持<span style="color: black;">运用</span>其他编程语言来处理 EMQ X 中的钩子事件,<span style="color: black;">研发</span>者<span style="color: black;">能够</span><span style="color: black;">运用</span> Python <span style="color: black;">或</span> Java 快速<span style="color: black;">研发</span>自己的插件,在官方功能的<span style="color: black;">基本</span>上进行扩展,满足自己的业务场景。例如:</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">验证某客户端的登录权限:客户端连接时触发对应函数,<span style="color: black;">经过</span>参数获取客户端信息后<span style="color: black;">经过</span>读取数据库、比对等操作判定<span style="color: black;">是不是</span>有登录权限</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">记录客户端在线状态与上下线历史:客户端状态变动时触发对应函数,<span style="color: black;">经过</span>参数获取客户端信息,改写数据库中客户端在线状态</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">校验某客户端的 PUB/SUB 的操作权限:发布/订阅时触发对应函数,<span style="color: black;">经过</span>参数获取客户端信息与当前主题,判定客户端<span style="color: black;">是不是</span>有对应的操作权限</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">处理会话 (Sessions) 和 <span style="color: black;">信息</span> (Message) 事件,实现订阅关系与<span style="color: black;">信息</span>处理/存储:<span style="color: black;">信息</span>发布、状态变动时触发对应函数,获取当前客户端信息、<span style="color: black;">信息</span>状态与消息内容,转发到 Kafka 或数据库进行存储。</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">注:<span style="color: black;">信息</span>(Message) 类钩子,仅在企业版中支持。</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">Python 和 Java 驱动基于 Erlang/OTP-Port</span><span style="color: black;"> (https://erlang.org/doc/tutorial/c_port.html)</span><span style="color: black;">进程间通信实现,本身<span style="color: black;">拥有</span>非常高的吞吐性能,本文以 Java 拓展为例介绍 EMQ X 跨语言拓展<span style="color: black;">运用</span>方式。</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><img src="https://mmbiz.qpic.cn/mmbiz_png/u21HUF7OcgBlibSaRjgdeKg8FoGMR3eMjLtiaPibQNO7nvicNJico6SyA6AfmlUUfGGvibXqWYjkOKbE8Ldh93jYu8TA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"></span></p> Java 拓展<span style="color: black;">运用</span>示例 <h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"> <span style="color: black;">需求</span></span></h3>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">EMQ X 所在服务器需安装 JDK 1.8 以上版本</span></p>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><span style="color: black;">起始</span><span style="color: black;">运用</span></span></h3>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">创建 Java 项目</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">下载 io.emqx.extension.jar </span><span style="color: black;">(https://github.com/emqx/emqx-extension-java-sdk/releases)</span><span style="color: black;">和 erlport.jar </span><span style="color: black;">(https://github.com/emqx/emqx-extension-java-sdk/blob/master/deps/erlport-v1.1.1.jar)</span><span style="color: black;"> 文件</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">添加SDK io.emqx.extension.jar和 erlport.jar 到项目依赖</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">复制 examples/SampleHandler.java到您的项目中</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><span style="color: black;">按照</span> SDK SampleHandler.java 中的示例编写业务代码,<span style="color: black;">保证</span>能够成功编译</span></p>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;"><span style="color: black;">安排</span></span></h3>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">编译所有源代码后,需要将 sdk 和代码文件<span style="color: black;">安排</span>到 EMQ X 中:</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">复制 io.emqx.extension.jar 到 emqx/data/extension 目录</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">将编译后的 .class 文件,例如 SampleHandler.class 复制到 emqx/data/extension目录</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">修改 emqx/etc/plugins/emqx_extension_hook.conf 配置文件:</span></p><span style="color: black;">exhook.drivers = java</span><span style="color: black;"><span style="color: black;">#</span><span style="color: black;"># Search path for scripts or library</span></span><span style="color: black;">exhook.drivers.java.path = data/extension/</span><span style="color: black;">exhook.drivers.java.init_module = SampleHandler</span>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><span style="color: black;">起步</span> emqx_extension_hook 插件,<span style="color: black;">倘若</span>配置错误或 Java 代码编写错误将<span style="color: black;">没法</span>正常<span style="color: black;">起步</span>。<span style="color: black;">起步</span>后尝试<span style="color: black;">创立</span> MQTT 连接并观察业务运行<span style="color: black;">状况</span>。</span></p>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;">示例</span></h3>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">以下为 SampleHandler.java </span><span style="color: black;">(https://github.com/emqx/emqx-extension-hook/blob/master/test/scripts/SampleHandler.java)</span><span style="color: black;">示例程序, 该程序继承自 SDK 中的 DefaultCommunicationHandler 类。该示例代码演示了<span style="color: black;">怎样</span>挂载 EMQ X 系统中所有的钩子:</span></p><span style="color: black;"><span style="color: black;">import</span> emqx.extension.java.handler.*;</span><span style="color: black;"><span style="color: black;">import</span> emqx.extension.java.handler.codec.*;</span><span style="color: black;"><span style="color: black;">import</span> emqx.extension.java.handler.ActionOptionConfig.Keys;</span><span style="color: black;"><span style="color: black;">public</span> <span style="color: black;"><span style="color: black;">class</span> <span style="color: black;">SampleHandler</span> <span style="color: black;">extends</span> <span style="color: black;">DefaultCommunicationHandler</span> </span>{</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> ActionOptionConfig <span style="color: black;">getActionOption</span><span style="color: black;">()</span> </span>{</span><span style="color: black;"> ActionOptionConfig option = <span style="color: black;">new</span> ActionOptionConfig();</span><span style="color: black;"> option.set(Keys.MESSAGE_PUBLISH_TOPICS, <span style="color: black;">"#"</span>);</span><span style="color: black;"> option.set(Keys.MESSAGE_DELIVERED_TOPICS, <span style="color: black;">"#"</span>);</span><span style="color: black;"> option.set(Keys.MESSAGE_ACKED_TOPICS, <span style="color: black;">"#"</span>);</span><span style="color: black;"> option.set(Keys.MESSAGE_DROPPED_TOPICS, <span style="color: black;">"#"</span>);</span><span style="color: black;"> <span style="color: black;">return</span> option;</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">// Clients</span></span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onClientConnect</span><span style="color: black;">(ConnInfo connInfo, Property[] props)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onClientConnect: connInfo: %s, props: %s\n"</span>, connInfo, props);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onClientConnack</span><span style="color: black;">(ConnInfo connInfo, ReturnCode rc, Property[] props)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onClientConnack: connInfo: %s, rc: %s, props: %s\n"</span>, connInfo, rc, props);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onClientConnected</span><span style="color: black;">(ClientInfo clientInfo)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onClientConnected: clientinfo: %s\n"</span>, clientInfo);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onClientDisconnected</span><span style="color: black;">(ClientInfo clientInfo, Reason reason)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onClientDisconnected: clientinfo: %s, reason: %s\n"</span>, clientInfo, reason);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">// 判定认证结果,返回 true 或 false </span></span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">boolean</span> <span style="color: black;">onClientAuthenticate</span><span style="color: black;">(ClientInfo clientInfo,<span style="color: black;">boolean</span> authresult)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onClientAuthenticate: clientinfo: %s, authresult: %s\n"</span>, clientInfo, authresult);</span><span style="color: black;"> <span style="color: black;">return</span> <span style="color: black;">true</span>;</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">// 判定 ACL <span style="color: black;">检测</span>结果,返回 true 或 false</span></span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">boolean</span> <span style="color: black;">onClientCheckAcl</span><span style="color: black;">(ClientInfo clientInfo, PubSub pubsub, Topic topic, <span style="color: black;">boolean</span> result)</span> </span>{</span><span style="color: black;">System.err.printf(<span style="color: black;">" onClientCheckAcl: clientinfo: %s, pubsub: %s, topic: %s, result: %s\n"</span>, clientInfo, pubsub, topic, result);</span><span style="color: black;"> <span style="color: black;">return</span> <span style="color: black;">true</span>;</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onClientSubscribe</span><span style="color: black;">(ClientInfo clientInfo, Property[] props, TopicFilter[] topic)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onClientSubscribe: clientinfo: %s, topic: %s, props: %s\n"</span>, clientInfo, topic, props);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onClientUnsubscribe</span><span style="color: black;">(ClientInfo clientInfo, Property[] props, TopicFilter[] topic)</span> </span>{</span><span style="color: black;">System.err.printf(<span style="color: black;">" onClientUnsubscribe: clientinfo: %s, topic: %s, props: %s\n"</span>, clientInfo, topic, props);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">// Sessions</span></span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onSessionCreated</span><span style="color: black;">(ClientInfo clientInfo)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onSessionCreated: clientinfo: %s\n"</span>, clientInfo);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onSessionSubscribed</span><span style="color: black;">(ClientInfo clientInfo, Topic topic, SubscribeOption opts)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onSessionSubscribed: clientinfo: %s, topic: %s\n"</span>, clientInfo, topic);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onSessionUnsubscribed</span><span style="color: black;">(ClientInfo clientInfo, Topic topic)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onSessionUnsubscribed: clientinfo: %s, topic: %s\n"</span>, clientInfo, topic);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onSessionResumed</span><span style="color: black;">(ClientInfo clientInfo)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onSessionResumed: clientinfo: %s\n"</span>, clientInfo);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onSessionDiscarded</span><span style="color: black;">(ClientInfo clientInfo)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onSessionDiscarded: clientinfo: %s\n"</span>, clientInfo);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onSessionTakeovered</span><span style="color: black;">(ClientInfo clientInfo)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onSessionTakeovered: clientinfo: %s\n"</span>, clientInfo);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onSessionTerminated</span><span style="color: black;">(ClientInfo clientInfo, Reason reason)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onSessionTerminated: clientinfo: %s, reason: %s\n"</span>, clientInfo, reason);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">// Messages</span></span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> Message <span style="color: black;">onMessagePublish</span><span style="color: black;">(Message message)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onMessagePublish: message: %s\n"</span>, message);</span><span style="color: black;"> <span style="color: black;">return</span> message;</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onMessageDropped</span><span style="color: black;">(Message message, Reason reason)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onMessageDropped: message: %s, reason: %s\n"</span>, message, reason);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onMessageDelivered</span><span style="color: black;">(ClientInfo clientInfo, Message message)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onMessageDelivered: clientinfo: %s, message: %s\n"</span>, clientInfo, message);</span><span style="color: black;"> }</span><span style="color: black;"> <span style="color: black;">@Override</span></span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">onMessageAcked</span><span style="color: black;">(ClientInfo clientInfo, Message message)</span> </span>{</span><span style="color: black;"> System.err.printf(<span style="color: black;">" onMessageAcked: clientinfo: %s, message: %s\n"</span>, clientInfo, message);</span><span style="color: black;"> }</span><span style="color: black;">}</span>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">SampleHandler <span style="color: black;">重点</span><span style="color: black;">包括</span>两部分:</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">重载了 getActionOption <span style="color: black;">办法</span>。该<span style="color: black;">办法</span>对<span style="color: black;">信息</span>(Message)<span style="color: black;">关联</span>的钩子进行配置,指定了需要生效的主题列表。</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><img src="https://mmbiz.qpic.cn/mmbiz_png/u21HUF7OcgBlibSaRjgdeKg8FoGMR3eMjpDuUu8smyCRSg5bzSeYHAqSdribVtOJXb7RvlM5TjDUYibT8wiboW2qyQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">重载了 on<hookName> <span style="color: black;">办法</span>,这些<span style="color: black;">办法</span>是<span style="color: black;">实质</span>处理钩子事件的回调函数,函数命名方式为各个钩子名<span style="color: black;">叫作</span>变体后前面加 on 前缀,变体方式为钩子名<span style="color: black;">叫作</span>去掉下划线后<span style="color: black;">运用</span>骆驼拼写法(CamelCase),例如,钩子client_connect对应的函数名为onClientConnect。EMQ X 客户端产生的事件,例如:连接、发布、订阅等,都会<span style="color: black;">最后</span>分发到这些钩子事件回调函数上,<span style="color: black;">而后</span>回调函数可对各属性及状态进行<span style="color: black;">关联</span>操作。示例程序中仅对各参数进行了打印输出。<span style="color: black;">倘若</span>只关心部分钩子事件,只需对这部分钩子事件的回调函数进行重载<span style="color: black;">就可</span>,不需要重载所有回调函数。</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">各回调函数的执行<span style="color: black;">机会</span>和支持的钩子列表与 EMQ X 内置的钩子完全一致,参见:Hooks - EMQ X</span><span style="color: black;">(https://docs.emqx.io/broker/latest/en/advanced/hooks.html#hookpoint)</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">在实现自己的扩展程序时,最简单的方式<span style="color: black;">亦</span>是继承 DefaultCommunicationHandler 父类,该类对各钩子与回调函数的绑定进行了封装,并进一步封装了回调函数<span style="color: black;">触及</span>到的参数数据结构,以方便快速上手<span style="color: black;">运用</span>。</span></p>
<h3 style="color: black; text-align: left; margin-bottom: 10px;"><span style="color: black;">进阶<span style="color: black;">研发</span></span></h3>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><span style="color: black;">倘若</span>对 Java 扩展程序的可控性<span style="color: black;">需求</span>更高,DefaultCommunicationHandler 类已<span style="color: black;">没法</span>满足<span style="color: black;">需要</span>时,<span style="color: black;">能够</span><span style="color: black;">经过</span>实现 CommunicationHandler 接口,从更底层<span style="color: black;">掌控</span>代码<span style="color: black;">规律</span>,编写更灵活的扩展程序。</span></p><span style="color: black;"><span style="color: black;">package</span> emqx.extension.java.handler;</span><span style="color: black;"><span style="color: black;">public</span> <span style="color: black;"><span style="color: black;">interface</span> <span style="color: black;">CommunicationHandler</span> </span>{</span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> Object <span style="color: black;">init</span><span style="color: black;">()</span></span>;</span><span style="color: black;"> <span style="color: black;"><span style="color: black;">public</span> <span style="color: black;">void</span> <span style="color: black;">deinit</span><span style="color: black;">()</span></span>;</span><span style="color: black;">}</span>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">init() <span style="color: black;">办法</span>:用于初始化,声明扩展需要挂载<span style="color: black;">那些</span>钩子,以及挂载的配置</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;">deinit() <span style="color: black;">办法</span>:用于注销。</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><span style="color: black;"><span style="color: black;">仔细</span>数据格式说明,参见:设计文档</span><span style="color: black;"> (https://github.com/emqx/emqx-extension-hook/blob/master/docs/design.md) 。</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"><img src="https://mmbiz.qpic.cn/mmbiz_png/u21HUF7OcgDYPH7Y6QeGICicLR2R5Y4BCgAyzR7YaR0aSMa0sTKWnju1RWJHKhjqCBZEvARJxpY2tqN5aW5YzCw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" style="width: 50%; margin-bottom: 20px;"></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;"> 点击"阅读原文" ,<span style="color: black;">认识</span><span style="color: black;">更加多</span></p>
<p style="font-size: 16px; color: black; line-height: 40px; text-align: left; margin-bottom: 15px;">↓↓↓</p>
页:
[1]