Jetlinks2.0 设备接入章节代码分析


前言

目前工业物联网上采用的是 jetlinks ,在设备接入上其原生的代码还是有些复杂,特做此篇记录

一、jetlinks 设备接入

具体设备接入见jetlinks 文档: http://doc.v2.jetlinks.cn/Device_access/Device_access3.5.html

二、代码设计

1. 网络层设计

  • 此接口为所有网络组件的接口,所有的和设备网络通信的是此接口的子类
package org.jetlinks.community.network;

/**
 * 网络组件,所有网络相关实例根接口
 *
 * @author zhouhao
 * @version 1.0
 * @since 1.0
 */
public interface Network {

    /**
     * ID唯一标识
     *
     * @return ID
     */
    String getId();

    /**
     * @return 网络类型
     * @see DefaultNetworkType
     */
    NetworkType getType();

    /**
     * 关闭网络组件
     */
    void shutdown();

    /**
     * @return 是否存活
     */
    boolean isAlive();

    /**
     * 当{@link Network#isAlive()}为false是,是否自动重新加载.
     *
     * @return 是否重新加载
     * @see NetworkProvider#reload(Network, Object)
     */
    boolean isAutoReload();
}

  • NetworkProvider 这个接口 为这个network 的 build 类,负责该类的构建过程,以及网络的刷新
package org.jetlinks.community.network;

import org.jetlinks.core.metadata.ConfigMetadata;
import reactor.core.publisher.Mono;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * 网络组件支持提供商
 *
 * @param <P> 网络组件类型
 */
public interface NetworkProvider<P> {

    /**
     * @return 类型
     * @see DefaultNetworkType
     */
    @Nonnull
    NetworkType getType();

    /**
     * 使用配置创建一个网络组件
     *
     * @param properties 配置信息
     * @return 网络组件
     */
    @Nonnull
    Mono<Network> createNetwork(@Nonnull P properties);

    /**
     * 重新加载网络组件
     *
     * @param network    网络组件
     * @param properties 配置信息
     */
    Mono<Network> reload(@Nonnull Network network, @Nonnull P properties);

    /**
     * @return 配置定义元数据
     */
    @Nullable
    ConfigMetadata getConfigMetadata();

    /**
     * 根据可序列化的配置信息创建网络组件配置
     *
     * @param properties 原始配置信息
     * @return 网络配置信息
     */
    @Nonnull
    Mono<P> createConfig(@Nonnull NetworkProperties properties);

    /**
     * 返回网络组件是否可复用,网络组件不能复用时,在设备接入等操作时将无法选择已经被使用的网络组件.
     * <p>
     * 场景:在设备接入时,像TCP服务等同一个网络组件只能接入一种设备,因此同一个TCP服务是
     * 不能被多个设备接入网关使用的.
     *
     * @return 是否可以复用
     */
    default boolean isReusable() {
        return false;
    }

}

  • NetworkConfig 这个类为 网络组件的配置类,一些需要用户数据的配置字段在该类定义
package org.jetlinks.community.network;

import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.validator.ValidatorUtils;
import org.jetlinks.community.network.resource.NetworkTransport;
import org.springframework.util.StringUtils;

/**
 * 网络组件配置
 *
 * @author zhouhao
 * @see ServerNetworkConfig
 * @see ClientNetworkConfig
 * @since 2.0
 */
public interface NetworkConfig {

    /**
     * @return 获取配置ID
     */
    String getId();

    /**
     *
     * @return 网络协议类型 TCP or UDP
     */
    NetworkTransport getTransport();

    /**
     * 传输模式,如: http,mqtt,ws
     * @return 传输模式
     */
    String getSchema();

    /**
     * 是否使用安全加密(TLS,DTLS)
     *
     * @return true or false
     */
    boolean isSecure();

    /**
     * 安全证书ID ,当{@link NetworkConfig#isSecure()}为true时,不能为空.
     *
     * @return 证书ID
     * @see org.jetlinks.community.network.security.Certificate
     * @see org.jetlinks.community.network.security.CertificateManager
     */
    String getCertId();

    /**
     * 验证配置,配置不合法将抛出{@link ValidationException}
     */
   default void validate(){
       ValidatorUtils.tryValidate(this);
       if (isSecure() && !StringUtils.hasText(getCertId())) {
           throw new ValidationException("certId", "validation.cert_id_can_not_be_empty");
       }
   }
}

解析

  • 数据采集 代码大量采用这种方式设计,保证了每个类职责的单一,又易于组合扩展;

2. 设备和物联网交互的操作 代码设计

  • 该接口 职责为 设备状态的管理,对设备是否在线,离线,以及平台主动发送消息给设备 一些方法的定义,包括当前设备使用的是那种通信协议;用户可根据具体的业务规则,设定设备自动离线时间,

package org.jetlinks.core.server.session;

import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.jetlinks.core.command.Command;
import org.jetlinks.core.device.DeviceOperator;
import org.jetlinks.core.message.codec.EncodedMessage;
import org.jetlinks.core.message.codec.TraceDeviceSession;
import org.jetlinks.core.message.codec.Transport;
import org.jetlinks.core.trace.TraceHolder;
import reactor.core.publisher.Mono;

public interface DeviceSession {
    String getId();

    String getDeviceId();

    @Nullable
    DeviceOperator getOperator();

    long lastPingTime();

    long connectTime();

    Mono<Boolean> send(EncodedMessage var1);

    @Nonnull
    default <V> Mono<V> execute(@Nonnull Command<V> command) {
        return Mono.error(UnsupportedOperationException::new);
    }

    Transport getTransport();

    void close();

    /** @deprecated */
    @Deprecated
    void ping();

    boolean isAlive();

    void onClose(Runnable var1);

    /** @deprecated */
    @Deprecated
    default Optional<String> getServerId() {
        return Optional.empty();
    }

    default Optional<InetSocketAddress> getClientAddress() {
        return Optional.empty();
    }

    default void keepAlive() {
        this.ping();
    }

    default void setKeepAliveTimeout(Duration timeout) {
    }

    default Duration getKeepAliveTimeout() {
        return Duration.ZERO;
    }

    default boolean isWrapFrom(Class<?> type) {
        return type.isInstance(this);
    }

    default <T extends DeviceSession> T unwrap(Class<T> type) {
        return (DeviceSession)type.cast(this);
    }

    default Mono<Boolean> isAliveAsync() {
        return Mono.fromSupplier(this::isAlive);
    }

    default boolean isChanged(DeviceSession another) {
        return !this.equals(another);
    }

    static DeviceSession trace(DeviceSession target) {
        return (DeviceSession)(TraceHolder.isDisabled() ? target : TraceDeviceSession.of(target));
    }
}

3. 网关代码设计

网关包含了 设备消息处理,同时 联结协议包,使不同的协议解析成平台能识别的消息;

4. 设备消息链路

NetworkProvider.initMqttClient() --> initMqttClient() 方法内的 setClient(client) --> setClient(client) 内 的sink.getT2().next(mqttMessage)

  • Sink 又是何方神圣呢?
    按照目前只言片语来看,这是一个缓冲序列,可以指定有界或者无界(需注意OOM),
// NetworkProvider 类

public Mono<Network> initMqttClient(VertxMqttClient mqttClient, MqttClientProperties properties) {
        return convert(properties)
            .map(options -> {
                mqttClient.setTopicPrefix(properties.getTopicPrefix());
                mqttClient.setLoading(true);
                MqttClient client = MqttClient.create(vertx, options); // 创建 client 通信类,不同协议可能存在不同的通信类
                mqttClient.setClient(client);  // set 进去的同时,将消息的处理方式也在这个方法里边
                client.connect(properties.getRemotePort(), properties.getRemoteHost(), result -> {
                    mqttClient.setLoading(false);
                    if (!result.succeeded()) {
                        log.warn("connect mqtt [{}@{}:{}] error",
                                 properties.getClientId(),
                                 properties.getRemoteHost(),
                                 properties.getRemotePort(),
                                 result.cause());
                    } else {
                        log.debug("connect mqtt [{}] success", properties.getId());
                    }
                });
                return mqttClient;
            });
    }

// MqttClient
public void setClient(io.vertx.mqtt.MqttClient client) {
        if (this.client != null && this.client != client) {
            try {
                this.client.disconnect();
            } catch (Exception ignore) {

            }
        }
        this.client = client;
        client
            .closeHandler(nil -> log.debug("mqtt client [{}] closed", id))
            .publishHandler(msg -> {
                try {
                    MqttMessage mqttMessage = SimpleMqttMessage
                        .builder()
                        .messageId(msg.messageId())
                        .topic(msg.topicName())
                        .payload(msg.payload().getByteBuf())
                        .dup(msg.isDup())
                        .retain(msg.isRetain())
                        .qosLevel(msg.qosLevel().value())
                        .properties(msg.properties())
                        .build();
                    log.debug("handle mqtt message \n{}", mqttMessage);
                    subscriber
                        .findTopic(msg.topicName().replace("#", "**").replace("+", "*"))
                        .flatMapIterable(Topic::getSubscribers)
                        .subscribe(sink -> {
                            try {
                                sink.getT2().next(mqttMessage); // 可以看到,MQTT 的消息都放入到了sink 里边
                            } catch (Exception e) {
                                log.error("handle mqtt message error", e);
                            }
                        });
                } catch (Throwable e) {
                    log.error("handle mqtt message error", e);
                }
            });
        if (loading) {
            loadSuccessListener.add(this::reSubscribe);
        } else if (isAlive()) {
            reSubscribe();
        }
    }
	
	// MqttClient 
    Flux<MqttMessage> subscribe(List<String> topics, int qos);
	
	// 最后由这个 类的doSubscribe 方法做消息的处理与分发,同时插入消息处理监控,统计...
	// MqttClientDeviceGateway
	protected Disposable doSubscribe(String topic, int qos) {
        return mqttClient
            .subscribe(Collections.singletonList(topic), qos)
            .filter(msg -> isStarted())
            .flatMap(mqttMessage -> codecMono
                .flatMapMany(codec -> codec
                    .decode(FromDeviceMessageContext.of(
                        new UnknownDeviceMqttClientSession(getId(), mqttClient, monitor),
                        mqttMessage,
                        registry)))
                .flatMap(message -> {
                    monitor.receivedMessage();
                    return helper
                        .handleDeviceMessage((DeviceMessage) message,
                                             device -> createDeviceSession(device, mqttClient),
                                             ignore -> {
                                             },
                                             () -> log.warn("can not get device info from message:{},{}", mqttMessage.print(), message)
                        );
                })
                .subscribeOn(Schedulers.parallel())
                .onErrorResume((err) -> {
                    log.error("handle mqtt client message error:{}", mqttMessage, err);
                    return Mono.empty();
                }), Integer.MAX_VALUE)
            .contextWrite(ReactiveLogger.start("gatewayId", getId()))
            .subscribe();
    }
    // handleDeviceMessage()  根据消息的类型, 是ONLINE,REPORT,... 来 执行不同的事件,或设备上线,下线,数据上报  ... 一系列功能

总结

目前还处于观摩学习阶段,代码我认为是写的极好的,后续有新的理解和感悟再出一篇升华下

  • 30
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JetLinks开源物联网平台基于Java8、Spring Boot 2.x、WebFlux、Netty、Vert.x、Reactor等开发,是一个开箱即用,可二次开发的企业级物联网基础平台。平台实现了物联网相关的众多基础功能,能帮助你快速建立物联网相关业务系统。 JetLinks开源物联网平台核心特性: 支持统一物模型管理,多种设备,多种厂家,统一管理。 统一设备连接管理,多协议适配(TCP、MQTT、UDP、CoAP、HTTP等),屏蔽网络编程复杂性,灵活接入不同厂家不同协议的设备。 灵活的规则引擎,设备告警,消息通知,数据转发。可基于SQL进行复杂的数据处理逻辑。 地理位置:统一管理地理位置信息,支持区域搜索。 数据可视化:实现拖拽配置数据图表,设备组态等。 JetLinks开源物联网平台技术栈: Spring Boot 2.2.x Spring WebFlux 响应式Web支持 R2DBC 响应式关系型数据库驱动 Project Reactor 响应式编程框架 Netty、Vert.x 高性能网络编程框架 ElasticSearch 全文检索,日志,时序数据存储 PostgreSQL 业务功能数据管理 hsweb framework 4 业务功能基础框架     JetLinks开源物联网平台 更新日志: v1.9 1、增加设备独立物模型支持,可给单独的设备配置物模型. 2、基本实现GB28181国标视频设备接入,支持直播,云台控制,级联操作.(选配模块) 3、RabbitMQ增加routeKey配置,可在配置文件中指定device.message.writer.rabbitmq.consumer-route-key和device.message.writer.rabbitmq.producer-route-key.(Pro) 4、当设置了device.message.writer.rabbitmq.consumer=false时,不创建MQ消费者.(Pro) 5、设备支持独立物模型,可单独配置设备的物模型. 6、适配tdengine 2.0.16.0,优化sql长度策略. (pro) 7、优化规则引擎编辑器,实现组件模块化动态加载.(Pro) 8、修复启动服务时,如果某个产品物模型发布失败,导致后面的产品终止发布的问题. 9、增加ignoreLatest消息头,message.addHeader("ignoreLatest",true) 忽略记录最新数据到数据库. 10、修复租户下操作设备告警提示无权限.(Pro) 11、优化租户在解绑成员时,同时解绑成员的资产信息.(Pro) 12、优化子设备消息回复处理 13、物模型属性增加存储方式功能,可配置部分属性不存储. 14、增加虚拟属性功能,可通过规则来计算出虚拟属性值.(Pro) 15、增加租户成员绑定(TenantMemberBindEvent),解绑(TenantMemberUnBindEvent)事件.可通过spring-event订阅处理此事件.(Pro) 16、优化子设备状态检查,当检查子设备状态时,将会尝试发送ChildDeviceMessage<DeviceStateCheckMessage>给网关,处理后返回ChildDeviceMessageReply<DeviceStateCheckMessageReply>. 17、增加ClickHouse设备数据存储策略支持.(Pro) 18、增加权限过滤功能,可配置禁止赋予自己没有的权限给其他用户.hsweb.permission.filter相关配置 19、设备和产品的租户绑定逻辑优化: 绑定设备时,自动绑定产品.解绑产品时,自动解绑设备.(Pro) 20、用户管理增加租户权限控制.(Pro) 21、当向keepOnline的设备发送消息时,如果原始连接已断开,将返回CONNECTION_LOST错误. 22、设置keepOnline的会话将被持久化,重启服务后自动恢复.(Pro) 23、默认关闭设备最新数据存储,通过jetlinks.device.storage.enable-last-data-in-db=true开启.(Pro) 24、属性物模型增加属性值来源,配置为手动时,在发送修改属性指令(WritePropertyMessage)时,将直接生效,不会发送到设备. 25、优化租户资产解绑逻辑,当删除数据时,解绑资产全部的绑定关系.(Pro) 26、用户管理,机构管理增加租户端支持,租户可以自己管理自己的用户和机构.(Pro)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值