使用java语言且基于netty, spring boot, redis等开源项目开发来的物联网网络中间件, 支持udp, tcp通讯等底层协议和http, mqtt, websocket(默认实现和自定义协议头实现), modbus(tcp,rtu),plc,dtu(支持心跳,设备注册功能以及AT协议和自定义协议支持),dtu for modbus tcp,dtu for modbus rtu组件适配 等上层协议. 主打工业物联网底层网络交互、设备管理、数据存储、大数据处理. (其中plc包括西门子S7系列,欧姆龙Fins,罗克韦尔CIP,三菱MC). 数据存储将使用taos数据库以及redis消息队列
主要特性
- 支持服务端启动监听多个端口, 统一所有协议可使用的api接口
- 包含一套代理客户端通信协议,支持调用:客户端 -> 服务端 -> 设备 -> 服务端 -> 客户端
- 支持设备协议对象和其业务对象进行分离(支持默认业务处理器【spring单例注入】和自定义业务处理器)
- 支持同步和异步调用设备, 支持应用程序代理客户端和设备服务端和设备三端之间的同步和异步调用
- 服务端支持设备上线/下线/异常的事件通知, 支持自定义心跳事件, 客户端支持断线重连
- 丰富的日志打印功能,包括设备上线,下线提示, 一个协议的生命周期(请求或者请求+响应)等
- 支持请求时如果连接断线会自动重连(同步等待成功后发送)
- 支持客户端发送请求时如果客户端不存在将自动创建客户端(同步等待成功后发送)
- 支持作为mqtt网关,将从工业物联网采集的数据更加简单方便的发布到mqtt服务器
- 支持常用的物联网协议比如:mqtt、plc、modbus、websocket
- 支持通过dtu方式使用modbus协议操作plc
模拟工具
- QtSwissArmyKnife 支持udp、tcp、modbus、websocket、串口等调试
- IotClient 支持plc(西门子,欧姆龙,三菱),modbus,串口,mqtt,tcp, udp等模拟和调试
开发文档
1. 名词解释
- 报文对象(Message):报文是对在网络中进行传输的二进制数据的封装,也是二进制数据的载体,在一定程度上 报文 = 二进制数据
- 协议对象(Protocol):协议是报文的一个规范约束,比如报文内容是:0x04 AF CD EE 03,那怎么知道这一串表示的是什么呢,协议就是对这一串数据的声明, 比如第一个字节代表数据后面还有几个字节的长度, 第二个字节是电压,第三字节是电流,第四个字节是校验位
- 组件对象(FrameworkComponent):在服务端,组件用来管理一个端口所需要的各种接口;在客户端,组件用来管理连接同一个服务器的所有接口以及已经连接的所有客户端。比如服务端的设备管理器,报文需要用到的编解码器,同步异步处理都是由组件来管理
- 同步:在调用请求的时候,请求线程会加锁阻塞,直到接收到响应或者超时来解锁
- 异步:在调用请求的时候,请求线程在发送完报文后直接返回,不阻塞调用线程, 而是注册一个回调函数,等到对方响应或者超时的时候在做业务处理
- 编解码器:用来对网络上的二进制数据进行拆包和粘包的处理对象
- 协议工厂(ProtocolFactory):用来创建各个协议对象的地方(因为一个客户端可能包含多个功能(协议), 每个功能对应一个协议对象那就会有很多协议对象,协议工厂用来管理协议对象的创建)
- 协议处理器(ProtocolHandle): 用来对协议做业务处理的
下面将由一个例子来展开说明iot框架的使用 例子:比如服务端接受到客户端报文如下:0x01 11 12 13 05 06 EE FF 八个字节, 如果客户端发送完此报文之后没有连续发送, 服务端接受到的数据就是一包完整的报文, 我们可以很容易的读取缓存区的内容然后进行处理; 那如果第一包发送完之后服务端还在忙其他的时没有及时读取缓冲区内容这时候又接收到了客户端的第二个报文,这时候数据缓冲区的数据如下:0x01 11 12 13 05 06 EE FF 02 21 22 23 25 26 AA BB CC DD,这时候程序读取的缓冲区是两包完整的报文,这时候程序怎么将两包报文拆开处理呢?这时候就需要编解码器上场了! 如何处理上面报文粘包和拆包的情况呢?netty提供了一下几种常用的解码器
2. 编解码器
- FixedLengthFrameDecoder: 固定长度解码器是最简单的一种方式,每个包的长度是固定的,比如每个包都是8个字节,那么程序就可以以8字节为单位进行拆包
- LineBasedFrameDecoder:换行符解码器是让每个报文都用换行符结尾,这时候程序可以循环读取每个字节判断,如果这个字节是换行符就说明已经读完了一包报文
- DelimiterBasedFrameDecoder:如果我们的数据里面刚好包含换行符这时候读取就会出错,这时候可以用自定义分隔符来拆分报文
- LengthFieldBasedFrameDecoder:不管是换行符解码还是自定义分隔符解码,都需要循环判断每个字节,如果在一包完整的报文很长的情况下性能会非常差,这时候有个非常好用且性能极高的解码器,长度字段解码,就是在报文里面加入一个长度字段用来标识整个报文的长度
- ByteToMessageDecoder:如果还有更好的解码方式可以使用自定义报文解码
- SimpleChannelInboundHandler:简单的解码器
在设备对接的时候厂家一般会提供协议文档,然后就需要我们来选择合适的解码器,当我们确认好了解码器之后就可以开始编码了,下面先开始服务端的对接教程
3. 服务端教程
编写服务端网络程序时需要监听某个端口来给客户端连接, 当我们选择某个解码器之后就可以选择对应的服务端解码器组件来开启某个端口,iot框架适配了netty提供的几个常用的解码器
创建解码器组件
- FixedLengthFrameDecoderServerComponent 使用固定长度解码器的服务端组件
- LineBasedFrameDecoderServerComponent 使用换行符解码器的服务端组件
- DelimiterBasedFrameDecoderServerComponent 使用自定义分隔符解码器的服务端组件
- LengthFieldBasedFrameDecoderServerComponent 使用长度字段解码器的服务端组件
- ByteToMessageDecoderServerComponent 使用自定义解码器的服务端组件
- DatagramPacketDecoderServerComponent udp协议的服务端组件
- SimpleChannelDecoderComponent 简单自定义解码器对应的组件
以下是使用LengthFieldBasedFrameDecoderServerComponent示例
// 首先:必须先创建一个组件对象来继承LengthFieldBasedFrameDecoderServerComponent // 以iot-test模块的断路器服务端模拟为例 public class BreakerServerComponent extends LengthFieldBasedFrameDecoderServerComponent { public BreakerServerComponent(ConnectProperties connectProperties) { super(connectProperties, ByteOrder.LITTLE_ENDIAN, 256, 0 , 4, 0, 0, true); } xxx 实现省略 } // 注:要求传入ConnectProperties对象作为构造参数, 此对象可以指定ip和端口
我们看到上面的组件需要一个泛型参数BreakerServerMessage, 此参数就是报文对象,上面我们说过报文对象是一个二进制数据载体,用于在iot框架各个对象中进行使用,下面我们来看看报文对象除了作为数据载体还有哪些扩展功能
创建报文对象
// 创建服务端报文对象必须继承ServerMessage类 public class BreakerServerMessage extends ServerMessage { public BreakerServerMessage(byte[] message) { super(message); } //省略其他构造函数 @Override protected MessageHead doBuild(byte[] message) { this.messageBody = MessageCreator.buildBreakerBody(message); return MessageCreator.buildBreakerHeader(message); } }
- 首先报文对象的构造函数BreakerServerMessage#BreakerServerMessage(byte[])必须存在
- 报文对象是连接组件和协议的桥梁,所以需要为每个服务端组件创建一个与之对应的报文对象
- 需要在BreakerServerMessage#doBuild(byte[])方法里初步解析出对应的equipCode、messageId、protocolType几个参数,这也是对客户端请求数据的初步解析
当我们创建了组件和报文类之后就可以启动应用了, 这时候日志里面会打印出组件配置的端口已经开启监听了。到了这里已经开了一个好头了算是成功了一半了,接下来就是创建协议对象了,从厂家那里拿到的协议文档至少包含一个协议, 一般我们建议为文档里面的每个协议创建一个对应的协议对象(Protocol)
创建协议对象
协议对象就是用来将接收到的二进制数据解析成和协议文档里对应的字段的;出于框架架构的需要我们将协议分成两种类型 如下:
- ClientInitiativeProtocol 声明此协议是客户端主动发起的协议 比如断路器主动上报当前的电流、电压、或报警
- ServerInitiativeProtocol 声明此协议是服务端主动发起的协议 比如服务端下发断开断路器的指令
首先我们先看一下ClientInitiativeProtocol方法声明, 还是以断路器为例
// 用来接收断路器主动上报的电压电流等数据 public class DataAcceptProtocol extends ClientInitiativeProtocol { private double v; // 电压 private double i; // 电流 private double power1; // 有功功率 private double power2; // 无功功率 private double py; // 功率因素 public DataAcceptProtocol(BreakerServerMessage requestMessage) { super(requestMessage); } @Override protected void doBuildRequestMessage(BreakerServerMessage requestMessage) { byte[] message = requestMessage.getBody().getMessage(); this.v = ByteUtil.bytesToInt(message, 0) / 100.0; this.i = ByteUtil.bytesToInt(message, 4) / 100.0; this.power1 = ByteUtil.bytesToInt(message, 8) / 100.0; this.power2 = ByteUtil.bytesToInt(message, 12) / 100.0; this.py = ByteUtil.bytesToShort(message, 16) / 100.0; } // 响应断路器的请求 @Override protected BreakerServerMessage doBuildResponseMessage() { Message.MessageHead head = requestMessage().getHead(); return new BreakerServerMessage(MessageCreator.buildBreakerHeader(head .getEquipCode(), head.getMessageId(), 4, head.getType()), MessageCreator.buildBreakerBody(StatusCode.Success)); } // 省略其他 }
平台已经可以接收设备主动上报的数据了,那平台要怎么主动给设备发送数据呢?ServerInitiativeProtocol协议就是用来声明一个协议是平台主动发给客户端的,下面以平台下发给断路器切换开关为例
/** * 切换断路器的开闭状态 */ public class SwitchStatusProtocol extends ServerInitiativeProtocol { private String deviceSn; public SwitchStatusProtocol(String deviceSn) { this.deviceSn = deviceSn; } /** * 构建要发送给断路器的报文 */ @Override protected BreakerServerMessage doBuildRequestMessage() throws IOException { DefaultMessageHead messageHead = MessageCreator.buildBreakerHeader(Long.valueOf(this.deviceSn), 0, protocolType()); return new BreakerServerMessage(messageHead); } /** * 处理断路器对此处请求的响应 */ @Override protected void doBuildResponseMessage(BreakerServerMessage message) { /*设备响应是否切换成功的处理*/ } @Override public BreakerProtocolType protocolType() { return BreakerProtocolType.SwitchStatus; } } // 然后在业务代码里面调用请求方法:new SwitchStatusProtocol(deviceSn).request(); 这样就可以向指定的设备发起请求了