前言
目前工业物联网上采用的是 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,... 来 执行不同的事件,或设备上线,下线,数据上报 ... 一系列功能
总结
目前还处于观摩学习阶段,代码我认为是写的极好的,后续有新的理解和感悟再出一篇升华下