1. 背景
目前有两类情况可能会导致设备或子系统无法连接至 IoTOS:
- IoTOS 目前支持 MQTT、CoAP、LwM2M、HTTP 这四种协议,且认证方式要符合 IoTOS 的规定,但很多存量设备或者子系统使用了 TCP\UDP\WS 等协议,且认证方式多种多样,甚至连产品标识(对应 IoTOS 里的 PK)也有缺失;
- IoTOS 作为物联网中台对南向设备只有 Server 的角色,没有 Client 的角色,但很多子系统往往提供的是 Server,因此在 IoTOS 和子系统之间必须有一个程序充当 Client 从子系统拉取数据并传到 IoTOS。
本工程,即软件网关,作为 IoTOS 的配套组件,以开源形式提供,研发人员可以基于此代码进行二次开发解决以上2类问题。
2. 使用须知
2.1 环境要求
- JDK 1.8及以上版本
- Maven
- Git
2.2 适用场景
软件网关可用于解决以下2类无法连接 IoTOS 的设备或子系统的情况:
- 基于 TCP\UDP\HTTP 私有协议的设备或子系统;
- 自带上位机的软硬件一体系统,该类系统可能暴露如 HTTP\TCP\UDP\JDBC\ODBC 等各种接口对外提供数据。
2.3 源码地址
https://gitee.com/geekhekr/iotos-soft-gateway
3. 简要设计说明
下图是软件网关的基本工作原理,包含三个主要环节:
设备接入环节
软件网关中内置了 Server 能力,默认支持 TCP、UDP、HTTP 协议的接入。使用者需要自行实现上报数据的拆包/组包(当为 TCP 时)功能。
同时软件网关也内置了 Client 能力,默认支持向子系统发起 TCP、UDP、HTTP 请求,从而实现与子系统的交互。
开发者需要自行实现交互逻辑,还可以自行扩展实现更多协议的支持。
数据转码环节
此环节需实现设备原始数据格式(即设备或子系统认识的数据格式)和 KLink (IoTOS 内置的标准数据格式,采用 JSON 标准)的互相转换,使用者需要自行实现 encode(原始数据转 KLink) 和 decode(KLink 转原始数据)这两个 interface。
与 IoTOS 交互环节
软件网关使用 MQTT 协议实现与 IoTOS 的交互,使用者只需配置相关参数即可。
IoTOS 与软件网关交互的数据中一定包含 PK 和 devID,若存量设备本身不含 PK 等标识信息,开发者则需自行完成映射。
例如,子设备发送亮度状态值
light
为90,软件网关发送给 IoTOS 的数据格式如下:{ "action": "devSend", "msgID": 1, "PK": "3276aa89d25a46b789c7987421396e05", /* 子设备PK */ "devID": "dev-001" /* 子设备ID */ "data": { "cmd": "report", "params": { "light":90 } } }
参数 | 必填 | 类型 | 说明 |
---|---|---|---|
action | 是 | string | 动作,固定为 devSend |
PK | 否 | string | 要发送数据的设备产品PK |
devID | 否 | string | 要发送数据的设备ID |
data | 是 | object | 上报的指令和参数数据 |
data.cmd | 是 | string | 标识符 |
data.params | 否 | object | 参数 |
4. 使用说明
4.1 使用流程
使用者操作流程如下(黄色部分是与软件网关相关的步骤):
第一步:注册登录 IoTOS
因 IoTOS 以私有化部署为主,绝大部分情况下开发者可以 superadmin(即超级管理员)登录内网里部署的 IoTOS,本文以 IoTOS 体验站点为例。
第二步:创建软件网关产品及设备
进入产品中心-产品开发,点击“创建产品”,建立软件网关,“产品信息”栏目根据实际需求而定,“节点类型”和“联网与数据”栏目配置图如下:
进入产品中心-设备管理,点击“创建设备”,其中设备 ID 后续会在软件网关代码里使用,取名方法根据实际需求而定。
第三步:创建子设备产品及设备
进入产品中心-产品开发,点击“创建产品”,“产品信息”栏目根据实际需求而定,“节点类型”和“联网与数据”栏目配置图如下:
若使用者要求规范设备 ID,建议进入产品中心-设备管理,点击“批量添加”,使用表格模板实现批量导入。
注:此时软件网关的数据转码环节中子设备 ID 和表格应一一对应。
第四步:查看并记录网关以及子设备信息
进入产品中心-设备管理,点击软件网关的右侧“查看”按钮。
获取到软件网关 PK、设备 ID 和 devSecret。
然后以相同的方式获取到子设备的产品 PK、设备 ID。
第五步:获取并配置网关设备信息
进入 IoTOS -产品中心-产品开发,点击上一步创建的软件网关产品,可以获取到 MQTT 接入方式信息,以此为 HOST 值。
进入项目路径
src/main/resources
,打开配置文件config.properties
进行参数配置。以下配置项为软件网关配置的必填信息
## mqtt配置(必填) #### 进入产品中心-产品开发-软件网关,"MQTT接入方式"栏目即可查询 iotos.host=106.75.50.110:1883 #### 软件网关的产品pk,进入产品中心-设备管理-软件网关,"产品pk"栏目即可查询 gateway.pk=fc5dbdd26fee4688a6ab35b63a294cc1 #### 软件网关的设备id,进入产品中心-设备管理-软件网关,"设备id"栏目即可查询 gateway.devID=gatewaydemo #### 软件网关的设备密钥,进入产品中心-设备管理-软件网关,"devSecret"栏目点击"复制"按钮即可查询 gateway.devSecret=d10d6a46f6b5462b88f0d07207479bd2
第六步:程序运行
进入项目路径并打开入口程序
src/main/java/hekr/me/iotos/softgateway/IoTGatewayApplication.java
以下注释部分分别为HttpClient、HttpServer、TcpClient、TcpServer、UdpClient、UdpServer的入口,使用者可根据实际需求自行打开需要的部分。
public static void main(String[] args) throws Exception {
// 获取配置文件中的相关参数
P.use("config.properties");
// 软件网关初始化,完成软件网关参数读取、登陆操作
ProxyService.init();
// 软件网关对云端下发指令或回复指令进行相应处理的processor注册
ProxyCallbackService.processorManager.register(new CloudSendProcessor());
// 若要启用http则将下行注释打开
// HttpServerInit.init();
// 使用http client示例,此处example方法调用的接口在HttpServer中,因此若要启动此示例方法务必也将HttpServerInit启动
// Thread.sleep(5000);
// HttpClient.example();
// 若要启用TCP client则将下行注释打开
// TcpClientStarter.start();
// 若要启用TCP server则将下行注释打开
// TcpServerStarter.start();
// 若要启用UDP client则将下行注释打开
// UdpClientStarter.start();
// 若要启用UDP server则将下行注释打开
// UdpService.init();
}
第七步:数据信息上报
本项目调用6种上报的方法 addSub、subLogin、subLogout、subTopo、delSub 以及 devSend,在
SubKLink
类中定义了上述方法,使用者可根据具体情况自行添加或修改方法。若开发者需要将存量设备中的设备 ID 和规范化的设备 ID 进行映射,则需要自行代码实现。
方法 需要参数 说明 addSub pk, devId 配置的软件网关添加子设备 subLogin pk, devId 子设备上线 subLogout pk, devId 子设备下线 subTopo 无 查看软件网关下关联的子设备拓扑 delSub pk, devId 配置的软件网关删除子设备 devSend pk, devId 发送指定数据信息
第八步:IoTOS 收到下发命令并发送给设备
在
ProxyConnectService
类中添加了如下代码,用于软件网关订阅 IoTOS 下发的信息。
client.subscribe(DOWN_TOPIC,0);
示例:首先调用 addSub 方法向网关添加设备,再调用 subTopo 方法查看网关下子设备绑定情况,得到 KLink 返回信息,可以看到成功绑定了一台设备。
- 接收消息主题 : down/dev/fc5dbdd26fee4688a6ab35b63a294cc1/gatewaydemo
- 接收消息Qos : 0
- 接收消息内容 : {
"action":"getTopoResp",
"msgId":0,
"pk":"fc5dbdd26fee4688a6ab35b63a294cc1",
"devId":"gatewaydemo",
"code":0,
"subs":[{
"pk":"3276aa89d25a46b789c7987421396e05",
"devId":"dynamic"
}]
}
可以看到软件网关下的设备 PK 和设备 ID,为成功添加至软件网关的子设备信息。
4.2 代码结构说明
4.2.1 网关总体代码结构
如下图所示,网关总体的相关代码在项目中的src/main/java/hekr/me/iotos/softgateway
路径下:
其中:
common
包含各种常量以及枚举数据,例如 KLink 相关协议都在此包中;
northProxy
为软件网关核心代码部分,负责软件网关与 IoTOS 的连接以及数据的收发;
pluginAsClient
包含 HTTP、TCP、UDP 三种协议下软件网关作为客户端启用的代码;
pluginAsSever
包含 HTTP、TCP、UDP 三种协议下作为服务端启用的代码;
utils
为工具类,提供 json 数据处理、hash 加密等相关工具类,以便于开发者进行二次开发。
config.properties
为程序配置文件,开发者可在此配置所有与连接相关的参数。
4.2.2 软件网关时序图结构
以下展示了软件网关分别作为客户端(pluginAsClient)和服务端(pluginAsServer)时的时序图。
作为客户端时:
作为服务端时:
4.2.3 软件网关主要组成
如下图所示,软件网关的相关代码在项目中的src/main/java/hekr/me/iotos/softgateway/northProxy
路径下:
其中:
ProxyCallbackService
负责 MQTT 回调指令的相应操作;
ProxyConnectService
负责软件网关初始化连接等相应操作;
ProxyServer
负责软件网关发送指令;
processor
包主要由Processor
接口和ProcessorManager
组成,开发者需要实现Processor
类来自行开发具备操作云端下发的各种指令的功能。上图中的CloudSendProcessor
类为 cloudSend 指令操作的示例代码。注:开发者通常无需关注
Proxy*
的类,只需要关注processor
的实现。
4.2.4 httpClient
如下图所示,httpClient的相关代码在项目中的src/main/java/hekr/me/iotos/softgateway/pluginAsClient/http
路径下:
HttpUtils
实现了常用 HTTP 请求(GET、POST 请求),以及相关 hearder 和 body 的生成方法。开发者可通过新建HttpUtils
对象并调用相应的方法,用以对接提供 HTTP Server 能力的子系统。
HttpClient
为示例代码,可供开发者参考。
4.2.5 工具类
如下图所示,httpClient
的相关代码在项目中的src/main/java/hekr/me/iotos/softgateway/utils
路径下,软件网关内置三个工具类以便于开发者进行二次开发。
JsonUtil
负责对象与 json 格式之间得转换,对于转换 KLink 格式有较大的用处。
mapUtils
负责将对象转成 map 格式,便于在 HTTP 请求中构建 header 和 body。
parseUtil
负责进行 hash 算法及其相关数据格式的转换等功能,主要用于登陆注册校验部分。
4.3 二次开发
软件网关中内置了 Server 能力,当为 TCP Server 时需要开发者实现 TCP 层面的拆包和组包,以避免粘包问题。在TcpServerMsgHandler
类中,开发者可实现PacketCodec
来进行适配,如LinePacketCodec
示例是当设备以换行符作为分割标识的具体代码,开发者可进行参考。
/** 拆包部分 */
@Override
public Packet decode(
ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext)
throws AioDecodeException {
return packetCodec.decode(buffer, limit, position, readableLength, channelContext);
}
/** 组包部分 */
@Override
public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) {
return packetCodec.encode(packet, tioConfig, channelContext);
}
软件网关中内置了 Client 能力,示例代码提供了软件网关和子设备简单的交互逻辑,用户可以根据自己的需求自定义开发。
在src/main/java/hekr/me/iotos/softgateway/northProxy/processor
包中,开发者需要实现Processor
接口来对应 IoTOS 下发的指令,完成其具体功能。
软件网关内置了数据协议转换的能力,即将原始数据根据一定的规则转换成 KLink 形式,开发者需要实现DataCodec
中的encode
和decode
两个方法。其中RawDataCodec
是一个具体的示例。假如设备采用了如下的十六进制数据格式:
透传消息类型 | 类型描述 | 消息格式 | 备注 |
---|---|---|---|
0x00 | 动态注册 | 0x00+pk_length(2字节)+pk+devId_length(2字节)+devId+productSecret_length(2字节)+productSecret+0x0a | productSecret为产品秘钥 |
0x01 | 登录请求 | 0x01+pk_length(2字节)+pk+devId_length(2字节)+devId+devSecret_length(2字节)+devSecret+0x0a | devSecret为设备秘钥 |
0x02 | 上行数据报文 | 0x02+pk_length(2字节)+pk+devId_length(2字节)+devId+data_length(2字节)+data+0x0a | |
0x03 | 心跳 | 0x03+0x0a | IoTOS 心跳周期为5分钟,设备需在5分钟内发送心跳报文 |
0x04 | 查看拓扑 | 0x04+0x0a | 查看当前软件网关的拓扑情况 |
示例:
- 登录请求:
pk:3276aa89d25a46b789c7987421396e05
devId:dynamic
devSecret:8cb192d8bbde469b8cd2b100fe02f042
登录请求编码为:01 0020 3332373661613839643235613436623738396337393837343231333936653035 0007 64796e616d6963 0020 3863623139326438626264653436396238636432623130306665303266303432 0a
- 上行数据:
pk:3276aa89d25a46b789c7987421396e05
devId:dynamic
业务数据:{"cmd":"reportFrame","params":{"STS":0}}
上行数据报文编码为:02 0020 3332373661613839643235613436623738396337393837343231333936653035 0007 64796e616d6963 0028 7b22636d64223a227265706f72744672616d65222c22706172616d73223a7b22535453223a307d7d 0a
5. 产品测试
测试工具下载地址:
5.1 TCP Server测试
使用 TCP 模拟器对于 TCP Server 进行测试,设备登录测试(本地测试,端口设置为7000,0a为结束标志):
通过 IoTOS 接收到回复信息:
从 IoTOS 回复的信息可以看出,设备成功登陆。
上行数据测试:
通过 IoTOS 接受到回复信息:
从回复的信息看出设备成功发送了信息。
5.2 TCP Client测试
首先在配置文件中设置服务器IP以及端口:
## tcp 客户端配置
tcp.client.connect.ip=192.168.5.47
tcp.client.connect.port=7000
使用 TCP 模拟器对于 TCP Client 进行测试,设备登录测试
从日志里查看 IoTOS 的回复信息:
desc":"success","sub":{"pk":"3276aa89d25a46b789c7987421396e05","devId":"dynamic"}}
[Proxy Call: dev:fc5dbdd26fee4688a6ab35b63a294cc1:gatewaydemo] INFO hekr.me.iotos.softgateway.ProxyCallbackService - 接收消息主题 : down/dev/fc5dbdd26fee4688a6ab35b63a294cc1/gatewaydemo
[Proxy Call: dev:fc5dbdd26fee4688a6ab35b63a294cc1:gatewaydemo] INFO hekr.me.iotos.softgateway.ProxyCallbackService - 接收消息Qos : 0
[Proxy Call: dev:fc5dbdd26fee4688a6ab35b63a294cc1:gatewaydemo] INFO hekr.me.iotos.softgateway.ProxyCallbackService - 接收消息内容 : {"action":"devLoginResp","msgId":0,"pk":"3276aa89d25a46b789c7987421396e05","devId":"dynamic","code":0,"desc":"success, "}
从回复信息可以看出网关成功绑定设备并且设备成功上线。
上行数据测试
从日志里查看 IoTOS 的回复信息:
[Proxy Call: dev:fc5dbdd26fee4688a6ab35b63a294cc1:gatewaydemo] INFO hekr.me.iotos.softgateway.ProxyCallbackService - 接收消息主题 : down/dev/fc5dbdd26fee4688a6ab35b63a294cc1/gatewaydemo
[Proxy Call: dev:fc5dbdd26fee4688a6ab35b63a294cc1:gatewaydemo] INFO hekr.me.iotos.softgateway.ProxyCallbackService - 接收消息Qos : 0
[Proxy Call: dev:fc5dbdd26fee4688a6ab35b63a294cc1:gatewaydemo] INFO hekr.me.iotos.softgateway.ProxyCallbackService - 接收消息内容 : {"action":"devSendResp","msgId":1,"pk":"3276aa89d25a46b789c7987421396e05","devId":"dynamic","code":0,"desc":"success"}
从回复的信息看出设备成功发送了信息。
5.3 HTTP Server测试
- 在
config.properties
中将http server
端口配置为8070,默认ip
配置为"localhost"
。
## http 服务端配置(按需选填)
http.server.port=8070
- 在主程序的
main
方法中开启 HTTP Server
- 在
HttpController
中定义接口
/** 此接口用来配合测试HttpClient 此处模拟"http server的设备"将指令进行编码后发送给客户端 */
@RequestPath(value = "/test")
public HttpResponse sendCommand(HttpRequest request) throws Exception {
// 此处new DevSend()来模拟云端下发
DevSend devSend = new DevSend();
devSend.setAction(SubKLinkAction.HEARTBEAT);
// 编码后发送
Object resp = dataCodec.encode(devSend);
HttpResponse ret = Resps.bytes(request, (byte[]) resp, "ok");
return ret;
}
- 运行后使用 postman 进行测试访问
从上图中可以看到测试成功,HTTP Server 成功启动。
附录
KLink 使用范例
以设备接入软件网关,软件网关与 IoTOS 进行数据交互为例,以下为 KLink 形式示例:
- 设备绑定网关:
{
"action": "addTopo",
"msgID": 1,
"PK": "fc5dbdd26fee4688a6ab35b63a294cc1", /*网关设备PK*/
"devID": "gatewaydemo", /*网关设备ID*/
"sub": {
"PK": "3276aa89d25a46b789c7987421396e05",
/*设备PK*/
"devID": "dynamic" /*设备ID*/
}
}
- 设备上/下线:
{
"action": "devLogin", /* 下线:"action": "devLogout" */
"msgID": 1,
"PK": "3276aa89d25a46b789c7987421396e05", /*设备PK*/
"devID": "dynamic" /*设备ID*/
}
- 展示网关拓扑关系:
{
"action": "getTopo",
"msgID": 1,
"PK": "fc5dbdd26fee4688a6ab35b63a294cc1", /*网关设备PK*/
"devID": "gatewaydemo" /*网关设备ID*/
}
- 若周围烟雾浓度达到预警,烟雾传感器发送告警上报:
{
"action": "devSend",
"msgID": 1,
"PK": "3276aa89d25a46b789c7987421396e05", /*设备PK*/
"devID": "dynamic" /*设备ID*/
"data": {
"cmd": "reportDev",
"params": {}
}
密码生成规则
参数 | 说明 | 构造方式 |
---|---|---|
username | 用户名 | {hashMethod}:{random} |
password | 密码 | hash(pk+devId+devSecret+random),加密密钥:devSecret。 |
【说明】
- 参数间使用“:”隔开。
- {hashMethod} 支持:HmacMD5、HmacSHA1、HmacSHA256 和 HmacSHA512。
- password 中加密使用的 hashMethod、random 需跟 username 中一致。
【参数构造实例】
Eg:pk=’pk123$’,devId=’1001$’,devSecret=’secret123$’,使用 HashMD5方式进行加密,随机字符(random)为’20191108’。
clientId = dev:pk123$:1001$
username = HashMD5:20191108
password = c0608e3abe2f058df1d33020f963dbaf
password 是通过 HashMD5 方法加密后得到。
要加密的字符串:pk123$1001$secret123$20191108,加密密钥:secret123$,加密后得到“c0608e3abe2f058df1d33020f963dbaf”。
【在线工具】
【加密内容】
开发者在进行加密时,可以直接使用工具包中
utils.ParseUtil
工具类,其中的HmacSHA1Encrypt(String encryptText, String encryptKey)
方法实现了加密,encryptText
为加密内容,encryptKey
为加密密钥,计算后返回byte[]
,开发者可以调用此类下的parseByte2HexStr(byte[] buf)
方法将其转换成字符串。如下图所示: