产品管理服务--连接EMQ X

简介

从EMQ X中获取消息的方式比较多

  • 购买EMQ X的企业版,包括很多消息流转模块;

  • 创建个EMQ X超级用户,订阅所有的消息事件;

  • 使用规则引擎;

  • 移植EMQ X的kafaka插件;

  • 通过WebHook插件获取消息。

    image-20210728134815313

本文通过WebHook插件获取消息。设备的所有事件会通过webhook发送到产品服务器。

image-20211121090519234

WebHook介绍

WebHook 是由 emqx_web_hook (opens new window)插件提供的 将 EMQ X 中的钩子事件通知到某个 Web 服务的功能。

可以理解为EMQ X创建了一个客户端,这个客户端可以收集设备的在线、下下线记录、订阅与消息存储、消息送达确认等事件消息,通过钩子上挂载回调函数将事件发送到web器。

    Client      |    EMQ X     |  emqx_web_hook |   HTTP       +------------+
  =============>| - - - - - - -> - - - - - - - ->===========>  | Web Server |
                |    Broker    |                |  Request     +------------+
WebHook消息是单向的。

Webhook配置

web.hook.url
TypeValue
stringhttp://192.168.31.216:9200/emqx/webhook

Webhook 请求转发的目的 Web 服务器地址。

web.hook.headers.
web.hook.headers.content-type = application/json
web.hook.headers.accept = */*
web.hook.headers.webhook-username = makerknz
web.hook.headers.webhook-password = 123456

指定 HTTP 请求头部中的数据。<Key> 指定 HTTP 请求头部中的字段名,此配置项的值为相应的字段值。<Key> 可以是标准的 HTTP 请求头部字段,也可以自定义的字段,可以配置多个不同的请求头部字段。

webhook-username和webhook-password 作为访问后端的凭据,当emq x是一个集群时用来区别不同的服务器。当然认证也可以通过添加证书进行SSL认证,这部分感兴趣的同学可以自己探索。

触发规则

etc/plugins/emqx_web_hooks.conf 可配置触发规则,其配置的格式如下:

## 格式示例
web.hook.rule.<Event>.<Number> = <Rule>

## 示例值
web.hook.rule.message.publish.1 = {"action": "on_message_publish", "topic": "a/b/c"}
web.hook.rule.message.publish.2 = {"action": "on_message_publish", "topic": "foo/#"}
web.hook.rule.message.publish.2 = {"action": "on_message_publish"}

项目中会设置全部转发到后端服务器,不设置topic过滤。

Event 触发事件

目前支持以下事件:

名称说明执行时机
client.connect处理连接报文服务端收到客户端的连接报文时
client.connack下发连接应答服务端准备下发连接应答报文时
client.connected成功接入客户端认证完成并成功接入系统后
client.disconnected连接断开客户端连接层在准备关闭时
client.subscribe订阅主题收到订阅报文后,执行 client.check_acl 鉴权前
client.unsubscribe取消订阅收到取消订阅报文后
session.subscribed会话订阅主题完成订阅操作后
session.unsubscribed会话取消订阅完成取消订阅操作后
message.publish消息发布服务端在发布(路由)消息前
message.delivered消息投递消息准备投递到客户端前
message.acked消息回执服务端在收到客户端发回的消息 ACK 后
message.dropped消息丢弃发布出的消息被丢弃后
Number

同一个事件可以配置多个触发规则,配置相同的事件应当依次递增。

Rule

触发规则,其值为一个 JSON 字符串,其中可用的 Key 有:

  • action:字符串,取固定值
  • topic:字符串,表示一个主题过滤器,操作的主题只有与该主题匹配才能触发事件的转发

例如,我们只将与 a/b/cfoo/# 主题匹配的消息转发到 Web 服务器上,其配置应该为:

web.hook.rule.message.publish.1 = {"action": "on_message_publish", "topic": "a/b/c"}
web.hook.rule.message.publish.2 = {"action": "on_message_publish", "topic": "foo/#"}  

这样 Webhook 仅会转发与 a/b/cfoo/# 主题匹配的消息,例如 foo/bar 等,而不是转发 a/b/dfo/bar

WebFlux 介绍

WebFlux是Spring中的异步非阻塞的响应式web框架,EMQ X的设备有12种事件类型,对于单机安装的EMQ X并发连接11万会产品webhook的请求,后端服务器是同步MVC会导致请求堆积,使服务处理逻辑。

如果通过web.hook.pool_size 配置连接池数,又会导致数据的延迟。

img

MVC JDBC是一个同步请求的过程,每一次请求都会在一个线程内执行,等一次请求完成之后才能释放资源,如果在service方法中业务逻辑如果碰到io操作时间比较长的操作,这样这个service方法就会长时间占用tomcat容器线程池中的线程,这样是不利于其他请求的处理的,当线程池中的线程处理任务时,任务由于长时间io操作,肯定会阻塞线程处理其他任务。

WebFlux R2DBC是一个异步非阻塞的请求过程,此处会涉及大量的EMQX的请求,如果请求堆积,可以通过设置背压调整每次处理的数量来达到对请求的削峰处理。

WebHook接口设计

增加WebFlux接口

添加依赖包

添加包WebFlux使用的包。r2dbc-mysql用来连接数据库。

    implementation('org.springframework.boot:spring-boot-starter-webflux')
    implementation('org.springframework.boot:spring-boot-starter-data-r2dbc')
    implementation('dev.miku:r2dbc-mysql')
配置
spring:
  r2dbc:
    url: r2dbcs:mysql://192.168.31.216:3306/product_manager?characterEncoding=utf-8&useSSL=false
    username: product_manager
    password: 123456
添加repository

WebHook目前设计只对外开放一个接口,仅用到device_events和device两张表,所以只写这两张表的repository即可。

DeviceEventsRepository

package cn.makerknz.product.server.repository;

import cn.makerknz.product.server.entity.DeviceEvents;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface DeviceEventsRepository extends ReactiveCrudRepository<DeviceEvents, Integer> {
}

DeviceRepository

package cn.makerknz.product.server.repository;

import cn.makerknz.product.server.entity.Device;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;

public interface DeviceRepository extends ReactiveCrudRepository<Device, Integer> {

    Mono<Device> findByClientId(String clientId);

}
service实现

在DeviceEventsServiceImpl增加实现

package cn.makerknz.product.server.service.impl;

import cn.makerknz.product.server.entity.DeviceEvents;
import cn.makerknz.product.server.mapper.DeviceEventsMapper;
import cn.makerknz.product.server.repository.DeviceEventsRepository;
import cn.makerknz.product.server.service.IDeviceEventsService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author maker knz
 * @since 2021-10-27
 */

@Service
public class DeviceEventsServiceImpl extends ServiceImpl<DeviceEventsMapper, DeviceEvents> implements IDeviceEventsService {

    @Autowired
    private DeviceEventsRepository deviceEventsRepository;

    @Override
    public Mono<DeviceEvents> add(DeviceEvents deviceEvents) {
        return deviceEventsRepository.save(deviceEvents);
    }

}

在DeviceServiceImpl中增加实现

package cn.makerknz.product.server.service.impl;

import cn.makerknz.product.server.entity.Device;
import cn.makerknz.product.server.mapper.DeviceMapper;
import cn.makerknz.product.server.repository.DeviceRepository;
import cn.makerknz.product.server.service.IDeviceService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author maker knz
 * @since 2021-10-27
 */
@Service
public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> implements IDeviceService {

    @Autowired
    private DeviceRepository deviceRepository;

    @Override
    public Mono<Device> findByClientId(String clientId) {
        return deviceRepository.findByClientId(clientId);
    }
}

对外接口
package cn.makerknz.product.server.controller;

import cn.makerknz.product.server.annotation.CheckEmqxWebhookIdentity;
import cn.makerknz.product.server.domain.enums.ResponseEnum;
import cn.makerknz.product.server.domain.form.EmqxWebhookForm;
import cn.makerknz.product.server.domain.vo.ResultVO;
import cn.makerknz.product.server.entity.DeviceEvents;
import cn.makerknz.product.server.service.IDeviceEventsService;
import cn.makerknz.product.server.service.IDeviceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import java.util.UUID;

@RestController
@RequestMapping("/emqx")
public class EmqxController {

    @Autowired
    private IDeviceEventsService deviceEventsService;

    @Autowired
    private IDeviceService deviceService;

    @CheckEmqxWebhookIdentity
    @PostMapping("/webhook")
    public Mono<ResultVO<Object>> webhook(@RequestBody EmqxWebhookForm emqxWebhookForm) {

        return deviceService.findByClientId(emqxWebhookForm.getClientid()).flatMap(e -> {
            DeviceEvents deviceEvents = DeviceEvents.builder()
                    .productId(e.getProductId())
                    .data(emqxWebhookForm.toString())
                    .deviceId(e.getId())
                    .eventAction(emqxWebhookForm.getAction())
                    .streamId(UUID.randomUUID().toString())
                    .topic(emqxWebhookForm.getTopic())
                    .dataType(1)
                    .build();
            return deviceEventsService.add(deviceEvents).then(Mono.just(ResultVO.success()));
        }).defaultIfEmpty(ResultVO.error(ResponseEnum.ERROR));
    }

}

Webhook接口权限

原因

webhook接口如果不做权限限制任何人都可以访问,可能会导致数据恶意添加。

配置

对于分布式的EMQ X 每台可以配置不同的账户和密码

emqx:
  webhook:
    users:
      makerknz: 1234567
      makerknz_1: 12313123

读取配置

package cn.makerknz.product.server.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

@EnableConfigurationProperties
@Configuration
@ConfigurationProperties(prefix = "emqx.webhook")
@Data
public class EmqxConfig {

    private Map<String,String> users;

}

增加切面注解

package cn.makerknz.product.server.annotation;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface CheckEmqxWebhookIdentity {
}

Webhook接口认证实现

package cn.makerknz.product.server.auth;

import cn.makerknz.product.server.config.EmqxConfig;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @Author: maker_knz
 * @Date: 2021/6/1/001 10:05
 * @Version 1.0
 */

@Aspect
@Component
public class EmqxWebhookIdentityAspect {

    @Autowired
    private EmqxConfig emqxConfig;

    /**
     * 切面验证webhook接口是否可以访问
     * @param point
     * @return
     */
    @Around("@annotation(cn.makerknz.product.server.annotation.CheckEmqxWebhookIdentity)")
    public Object checkLogin(ProceedingJoinPoint point) {

        try {
            HttpServletRequest request = this.getHttpServletRequest();

            // 1.从header中获取username和passowrd
            String username = request.getHeader("webhook-username");
            String password = request.getHeader("webhook-password");

            // 2.验证账户的有效性
            String userPassword = emqxConfig.getUsers().get(username);
            if (!userPassword.equals(password)) {
                throw new SecurityException("没有权限使用WebHook");
            }

            return point.proceed();
        } catch (Throwable throwable) {
            throw new SecurityException("WebHook配置错误");
        }
    }

    /**
     * 从请求头中获取HttpServletRequest
     * @return
     */
    private HttpServletRequest getHttpServletRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        return servletRequestAttributes.getRequest();
    }

}

Webhook修改配置

docker ps
docker exec -it c46cb71330f1 bash
cd /opt/emqx/etc/plugins
vi vi emqx_web_hook.conf
exit
docker restart c46cb71330f1

修改配置文件为

##====================================================================
## WebHook
##====================================================================

## Webhook URL
##
## Value: String
web.hook.url = http://192.168.31.97:9200/emqx/webhook

## HTTP Headers
##
## Example:
## 1. web.hook.headers.content-type = application/json
## 2. web.hook.headers.accept = *
##
## Value: String
web.hook.headers.content-type = application/json

## 配置接口访问权限,不使用CA证书
web.hook.headers.webhook-username = makerknz
web.hook.headers.webhook-password = 123456

## The encoding format of the payload field in the HTTP body
## The payload field only appears in the on_message_publish and on_message_delivered actions
##
## Value: plain | base64 | base62
web.hook.body.encoding_of_payload_field = plain

##-----------------------------使用https请求---------------------------------------
## PEM format file of CA's
##
## Value: File
## web.hook.ssl.cacertfile  = <PEM format file of CA's>

## Certificate file to use, PEM format assumed
##
## Value: File
## web.hook.ssl.certfile = <Certificate file to use>

## Private key file to use, PEM format assumed
##
## Value: File
## web.hook.ssl.keyfile = <Private key file to use>

## Turn on peer certificate verification
##
## Value: true | false
## web.hook.ssl.verify = false

## If not specified, the server's names returned in server's certificate is validated against
## what's provided `web.hook.url` config's host part.
## Setting to 'disable' will make EMQ X ignore unmatched server names.
## If set with a host name, the server's names returned in server's certificate is validated
## against this value.
##
## Value: String | disable
## web.hook.ssl.server_name_indication = disable

## Connection process pool size
##
## Value: Number
## HTTP 连接进程池大小。
web.hook.pool_size = 32

## Whether to enable HTTP Pipelining
##
## See: https://en.wikipedia.org/wiki/HTTP_pipelining
web.hook.enable_pipelining = true

##--------------------------------------------------------------------
## Hook Rules
## These configuration items represent a list of events should be forwarded
##
## Format:
##   web.hook.rule.<HookName>.<No> = <Spec>
## 有客户端连接时触发
web.hook.rule.client.connect.1       = {"action": "on_client_connect"}
## EMQ X下发连接应答
web.hook.rule.client.connack.1       = {"action": "on_client_connack"}
## 客户端成功接入
web.hook.rule.client.connected.1     = {"action": "on_client_connected"}
## 客户端断开连接
web.hook.rule.client.disconnected.1  = {"action": "on_client_disconnected"}
## 客户端订阅事件
web.hook.rule.client.subscribe.1     = {"action": "on_client_subscribe"}
## 客户端取消订阅事件
web.hook.rule.client.unsubscribe.1   = {"action": "on_client_unsubscribe"}
## EMQ X确认订阅事件
web.hook.rule.session.subscribed.1   = {"action": "on_session_subscribed"}
## EMQ X确认取消订阅事件
web.hook.rule.session.unsubscribed.1 = {"action": "on_session_unsubscribed"}
## 会话终止
web.hook.rule.session.terminated.1   = {"action": "on_session_terminated"}
## 发布消息
web.hook.rule.message.publish.1      = {"action": "on_message_publish"}
## 消息投递成功
web.hook.rule.message.delivered.1    = {"action": "on_message_delivered"}
## 消息已应答
web.hook.rule.message.acked.1        = {"action": "on_message_acked"}

测试

接口测试

权限测试

总结

这里使用WebFlux可以很好的起到消息中间件的作用,这也是为什么不使用kafka作为消息件的原因。如果使用消息订阅的方式,后面对MQTT消息处理是一件复杂的过程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Uni-app是一种跨平台的开发框架,可以用于开发多个平台的应用程序,包括iOS、Android和Web。而EMQX是一个开源的分布式物联网消息服务器,用于处理大规模的物联网设备连接和消息通信。 要在Uni-app中连接EMQX,可以按照以下步骤操作: 1. 首先,需要安装EMQX服务器。可以去EMQX官方网站下载安装包,并按照官方文档的指引进行安装和配置。 2. 在Uni-app项目中引入MQTT库。可以选择一种MQTT库,如paho-mqtt,通过npm或其他方式将其添加到Uni-app项目中。 3. 在Uni-app的代码中,创建一个MQTT客户端实例,并配置连接EMQX所需的相关参数,如服务器地址、端口号、用户名和密码等。 4. 使用MQTT客户端实例,可以通过调用相应的方法来连接EMQX服务器,并订阅或发布消息,进行数据通信。 5. 在Uni-app的页面中,可以通过监听MQTT客户端的事件,如连接成功、接收到消息等,来实时更新页面的数据显示。 需要注意的是,要成功连接EMQX,需要确保Uni-app所在的设备能够正常访问EMQX服务器,并且服务器的配置和网络设置正确。 在开发过程中,可以参考MQTT库的文档和示例代码,以及EMQX的官方文档,进行适当的调试和配置。同时,为了提高连接的稳定性和安全性,可以考虑使用SSL/TLS协议进行加密通信,以及适当设置QoS等参数。 ### 回答2: uni-app是一款跨平台的开发框架,可以方便地开发基于H5和小程序的应用。而EMQX是一款开源的MQTT消息服务器,用于实现物联网设备间的消息传输。下面将详细介绍如何在uni-app中连接EMQX。 首先,我们需要在uni-app项目中引入MQTT客户端库。可以选择一些开源的MQTT库,如paho-mqtt.js或MQTT.js。这些库可以在uni-app项目的依赖管理器中进行安装。 接下来,在需要连接EMQX的uni-app页面中,我们可以创建一个MQTT客户端实例,并设置连接EMQX所需的参数,如主机IP地址、端口、用户名和密码等。然后,通过调用客户端实例的connect方法来建立与EMQX连接。 在连接成功后,我们可以订阅指定主题的消息,通过调用客户端实例的subscribe方法,传入要订阅的主题。同时,我们也可以发送消息给某个主题,通过调用客户端实例的publish方法,传入要发送的主题和消息内容。 此外,我们还可以设置一些回调函数来处理连接状态的改变、接收到消息和发送消息的结果等。例如,可以通过设置onConnectionLost回调函数来处理连接断开的情况,设置onMessageArrived回调函数来处理接收到的消息,设置onMessageDelivered回调函数来处理消息发送结果等。 最后,在不需要连接EMQX的时候,可以通过调用客户端实例的disconnect方法来断开与EMQX连接。 综上所述,通过使用合适的MQTT库,我们可以方便地在uni-app中连接EMQX,并实现与物联网设备的消息传输。这样,我们就可以实现一些基于EMQX物联网应用,如远程控制和数据监测等功能。 ### 回答3: uni-app是一款基于Vue.js框架的跨平台开发工具,可以帮助开发者快速构建同时适配多个平台的应用。而EMQX则是一款开源的分布式消息中间件,用于实现高可靠性的消息传递与数据流动。 要在uni-app中连接EMQX,我们可以遵循以下步骤: 1. 在uni-app项目的根目录下,使用命令行工具运行`npm install uni-easemob --save`来安装相关依赖包,其中`uni-easemob`是用于操作EMQX的插件。 2. 在uni-app的主页面中,通过`import`关键字引入EMQX插件,例如`import uEMQX from 'uni-easemob'`。 3. 在页面的`mounted`生命周期函数中,通过创建EMQX实例来连接EMQX服务器。可以使用如下代码示例:`const emqx = new uEMQX({ appkey: 'your_appkey', server: 'your_server' })`。其中`appkey`是你的应用密钥,`server`是你的EMQX服务器地址。 4. 接下来,你可以调用EMQX实例的方法来订阅主题、发布消息等。例如,可以使用`emqx.subscribe(topic)`来订阅某个主题,使用`emqx.publish(topic, message)`来发布消息。 需要注意的是,连接EMQX服务器需要填写正确的Appkey和服务器地址,否则无法成功连接。此外,还可以根据需要自定义处理EMQX消息的逻辑,例如在收到消息时更新页面内容。 总之,通过以上步骤,我们可以在uni-app中成功连接EMQX服务器,实现消息的订阅和发布功能。希望以上信息对你有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值