一、前言
前面博客大部分介绍了基于EMQ中间件,通信协议使用的是MQTT,而传输的数据为纯文本数据,采用JSON格式。这种方式,大部分一看就知道是熟悉Web开发、软件开发的人喜欢用的方式。由于我也是做web软件开发的,也是比较喜欢这种方式。阿里的物联网平台,也是推荐这种方式。但是,但是做惯硬件开发,嵌入式开发就比较喜欢用裸TCP-Socket连接。采用的是二进制协议。基于此大部分应用场合为了兼容旧设备,就需要单独开发一个TCP服务器的网关。这里使用以前学过的,也是比较流行的Netty框架。
话不多说,下面就开始了。
二、协议
定义 |
描述 |
|
启动符‘@@’ (2字节) |
数据包的第1、2字节,为固定值 64,64。 |
|
控制单元
|
业务流水号 (2字节) |
数据包的第3、4字节。发送/确认模式下,业务流水号由发送端在发送新的数据包时按顺序加一,确认方按发送包的业务流水号返回;请求/应答模式下,业务流水号由请求端在发送新的请求命令时按顺序加一,应答方按请求包的业务流水号返回。低字节传输在前。业务流水号是一个2字节的正整数,由通信双方第一次建立网络连接时确定,初始值为0。业务流水号由业务发起方(业务发起方指发送/确认模式下的发送端或者请求/应答模式下的请求端)独立管理。业务发起方负责业务流水号的分配和回收,保证在业务存续期间业务流水号的唯一性。 |
协议版本号 (2字节) |
协议版本号包含主版本号(第5字节)和用户版本号(第6字节)。主版本号为固定值1,用户版本号由用户自行定义。 |
|
时间标签 (6字节) |
数据包的第7~12字节,为数据包发出的时间,具体定义表2。 |
|
源地址 (6字节) |
数据包的第13~18字节,为数据包的源地址(监控中心或用户信息传输装置地址)。低字节传输在前。 |
|
目的地址 (6字节) |
数据包的第19~24字节,为数据包的目的地址(监控中心或用户信息传输装置地址)。低字节传输在前。 |
|
应用数据单元长度 (2字节) |
数据包的第25、26字节,为应用数据单元的长度,长度不应大于1024;低字节传输在前。 |
|
命令字节 (1字节) |
数据包的第27字节,为控制单元的命令字节,具体定义见表3。 |
|
应用数据单元 (最大1024字节) |
应用数据单元,基本格式见表3,对于确认/否认等命令包,此单元可为空。 |
|
校验和 (1字节) |
控制单元中各字节数据(第3~第27字节)及应用数据单元的算术校验和,舍去8位以上的进位位后所形成的1字节二进制数。 |
|
结束符‘##’ (2字节) |
为固定值 35,35。 |
上面这个是本次需要处理的二进制数据格式。
三、代码部分
3.0 Pom.xml
1 xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0modelVersion> 5 <parent> 6 <groupId>org.springframework.bootgroupId> 7 <artifactId>spring-boot-starter-parentartifactId> 8 <version>2.1.1.RELEASEversion> 9 <relativePath/> 10 parent> 11 <groupId>com.wunaozai.iot.nettyplatformgroupId> 12 <artifactId>NettyPlatformartifactId> 13 <version>0.0.1-SNAPSHOTversion> 14 <name>IoTNettyPlatFormname> 15 <description>基于自定义协议,使用Netty,物联网通信平台description> 16 17 <properties> 18 <java.version>1.8java.version> 19 properties> 20 21 <dependencies> 22 <dependency> 23 <groupId>org.springframework.bootgroupId> 24 <artifactId>spring-boot-starterartifactId> 25 dependency> 26 27 28 <dependency> 29 <groupId>io.nettygroupId> 30 <artifactId>netty-allartifactId> 31 dependency> 32 <dependency> 33 <groupId>org.springframework.bootgroupId> 34 <artifactId>spring-boot-configuration-processorartifactId> 35 <optional>trueoptional> 36 dependency> 37 38 39 <dependency> 40 <groupId>org.springframework.bootgroupId> 41 <artifactId>spring-boot-starter-webartifactId> 42 dependency> 43 44 45 <dependency> 46 <groupId>org.springframework.bootgroupId> 47 <artifactId>spring-boot-devtoolsartifactId> 48 <optional>trueoptional> 49 <scope>truescope> 50 dependency> 51 52 <dependency> 53 <groupId>org.springframework.bootgroupId> 54 <artifactId>spring-boot-starter-testartifactId> 55 <scope>testscope> 56 dependency> 57 dependencies> 58 59 <build> 60 <plugins> 61 <plugin> 62 <groupId>org.springframework.bootgroupId> 63 <artifactId>spring-boot-maven-pluginartifactId> 64 <configuration> 65 <fork>truefork> 66 configuration> 67 plugin> 68 plugins> 69 build> 70 71 project>
3.1 SmartIotProtocol.java
这个主要对通信协议模型进行简单封装
1 package com.wunaozai.iot.nettyplatform.code; 2 3 /** 4 * 自定义协议 5 * @author Administrator 6 * @see https://www.cnblogs.com/sidesky/p/6913109.html 7 */ 8 public class SmartIotProtocol { 9 10 /** 11 * 协议最短长度 30 字节 12 */ 13 public static int MIN_LEN = 30; 14 15 /** 16 * 数据包启动符号 @@ 17 */ 18 public static short START = 25700; 19 20 /** 21 * 业务流水号 22 */ 23 private short flowid; 24 /** 25 * 主版本 26 */ 27 private byte version_major; 28 /** 29 * 次版本 30 */ 31 private byte version_minor; 32 /** 33 * 秒 34 */ 35 private byte second; 36 /** 37 * 分钟 38 */ 39 private byte minute; 40 /** 41 * 小时 42 */ 43 private byte hour; 44 /** 45 * 日 46 */ 47 private byte day; 48 /** 49 * 月 50 */ 51 private byte month; 52 /** 53 * 年 54 */ 55 private byte year; 56 /** 57 * 数据包的源地址 58 */ 59 private byte[] src; 60 /** 61 * 数据包的目的地址 62 */ 63 private byte[] dest; 64 /** 65 * 应用数据单元长度 长度不应大于1024;低字节传输在前 66 */ 67 private short data_len; 68 /** 69 * 命令字节 为控制单元的命令字节 70 */ 71 private byte cmd; 72 /** 73 * 应用数据单元 对于确认/否认等命令包,此单元可为空 74 */ 75 private byte[] data; 76 /** 77 * 校验和 控制单元中各字节数据(第3~第27字节)及应用数据单元的算术校验和,舍去8位以上的进位位后所形成的1字节二进制数 78 */ 79 private byte checksum; 80 /** 81 * 协议结束符号 ## 82 */ 83 public static short END = 13621; 84 85 /** 86 * 打印调试信息 87 */ 88 public void printDebugInfo(){ 89 System.out.println("---------完整数据包开始------------"); 90 System.out.println("|开始标志: " + printHexShort(START)); 91 System.out.println("|业务流水: " + printHexShort(flowid) + "\tFlowID:" + flowid); 92 System.out.println("|协议版本: " + printHexByte(version_major) + printHexByte(version_minor)); 93 System.out.println("|时间标签: " + "20" + year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second); 94 System.out.println("|源地址 : " + printHexBytes(src)); 95 System.out.println("|目的地址: " + printHexBytes(dest)); 96 System.out.println("|数据长度: " + data_len); 97 System.out.println("|命令字节: " + printHexByte(cmd)); 98 System.out.println("|应用数据: " + printHexBytes(data)); 99 System.out.println("|校验字节: " + printHexByte(checksum)); 100 System.out.println("|结束标志: " + printHexShort(END)); 101 System.out.println("---------------------------------"); 102 } 103 private String printHexByte(byte b){ 104 return String.format("%02X", b); 105 } 106 private String printHexBytes(byte[] bytes){ 107 String str = ""; 108 for(int i=0; i){ 109 str += String.format("%02X", bytes[i]); 110 } 111 return str; 112 } 113 private String printHexShort(int s){ 114 byte[] bytes = hexShort(s); 115 return printHexBytes(bytes); 116 } 117 private byte[] hexShort(int s){ 118 byte[] bytes = new byte[2]; 119 bytes[0] = (byte)((s << 24) >> 24); 120 bytes[1] = (byte)((s << 16) >> 24); 121 return bytes; 122 } 123 private byte[] hexInt(int n){ 124 byte[] bytes = new byte[4]; 125 bytes[3] = (byte) ((n ) >> 24); 126 bytes[2] = (byte) ((n << 8) >> 24); 127 bytes[1] = (byte) ((n << 16) >> 24); 128 bytes[0] = (byte) ((n << 24) >> 24); 129 return bytes; 130 } 131 132 public short getFlowid() { 133 return flowid; 134 } 135 public void setFlowid(short flowid) { 136 this.flowid = flowid; 137 } 138 public byte getVersion_major() { 139 return version_major; 140 } 141 public void setVersion_major(byte version_major) { 142 this.version_major = version_major; 143 } 144 public byte getVersion_minor() { 145 return version_minor; 146 } 147 public void setVersion_minor(byte version_minor) { 148 this.version_minor = version_minor; 149 } 150 public byte getSecond() { 151 return second; 152 } 153 public void setSecond(byte second) { 154 this.second = second; 155 } 156 public byte getMinute() { 157 return minute; 158 } 159 public void setMinute(byte minute) { 160 this.minute = minute; 161 } 162 public byte getHour() { 163 return hour; 164 } 165 public void setHour(byte hour) { 166 this.hour = hour; 167 } 168 public byte getDay() { 169 return day; 170 } 171 public void setDay(byte day) { 172 this.day = day; 173 } 174 public byte getMonth() { 175 return month; 176 } 177 public void setMonth(byte month) { 178 this.month = month; 179 } 180 public byte getYear() { 181 return year; 182 } 183 public void setYear(byte year) { 184 this.year = year; 185 } 186 public byte[] getSrc() { 187 return src; 188 } 189 public void setSrc(byte[] src) { 190 this.src = src; 191 } 192 public byte[] getDest() { 193 return dest; 194 } 195 public void setDest(byte[] dest) { 196 this.dest = dest; 197 } 198 public short getData_len() { 199 return data_len; 200 } 201 public void setData_len(short data_len) { 202 this.data_len = data_len; 203 } 204 public byte getCmd() { 205 return cmd; 206 } 207 public void setCmd(byte cmd) { 208 this.cmd = cmd; 209 } 210 public byte[] getData() { 211 return data; 212 } 213 public void setData(byte[] data) { 214 this.data = data; 215 } 216 public byte getChecksum() { 217 return checksum; 218 } 219 public void setChecksum(byte checksum) { 220 this.checksum = checksum; 221 } 222 223 }
3.2 SmartIotDecoder.java
解码器,这个是本次的重点,这个解码器最主要是解决TCP粘包拆包问题,如果有不清楚的,要重点理解一下。
1 package com.wunaozai.iot.nettyplatform.code; 2 3 import java.util.List; 4 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 8 import io.netty.buffer.ByteBuf; 9 import io.netty.channel.ChannelHandlerContext; 10 import io.netty.handler.codec.ByteToMessageDecoder; 11 12 /** 13 * 自定义协议解析 14 * @author Administrator 15 * 16 */ 17 public class SmartIotDecoder extends ByteToMessageDecoder { 18 19 20 private static final Logger log = LoggerFactory.getLogger(SmartIotDecoder.class); 21 22 @Override 23 protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List
3.3 SmartIotEncoder.java
相对于解码,这个编码器,就相对简单了,按照协议,一个byte一本byte进行发送即可。
1 package com.wunaozai.iot.nettyplatform.code; 2 3 import io.netty.buffer.ByteBuf; 4 import io.netty.channel.ChannelHandlerContext; 5 import io.netty.handler.codec.MessageToByteEncoder; 6 7 /** 8 * 自定义协议数据解析 9 * @author Administrator 10 * 11 */ 12 public class SmartIotEncoder extends MessageToByteEncoder{ 13 14 @Override 15 protected void encode(ChannelHandlerContext ctx, SmartIotProtocol msg, ByteBuf out) throws Exception { 16 //写入消息SmartIot具体内容 17 out.writeShort(SmartIotProtocol.START); 18 out.writeShort(msg.getFlowid()); 19 out.writeByte(msg.getVersion_major()); 20 out.writeByte(msg.getVersion_minor()); 21 out.writeByte(msg.getSecond()); 22 out.writeByte(msg.getMinute()); 23 out.writeByte(msg.getHour()); 24 out.writeByte(msg.getDay()); 25 out.writeByte(msg.getMonth()); 26 out.writeByte(msg.getYear()); 27 out.writeBytes(msg.getSrc()); 28 out.writeBytes(msg.getDest()); 29 out.writeShort(msg.getData_len()); 30 out.writeByte(msg.getCmd()); 31 out.writeBytes(msg.getData()); 32 out.writeByte(msg.getChecksum()); 33 out.writeShort(SmartIotProtocol.END); 34 } 35 36 }
3.4 SmartIotHandler.java
这个是工程里面的主要业务操作类,用户Handler处理所有业务操作,这里也可以理解为是一个入口、网关。所有命令都从这里进行分发到子模块。
1 package com.wunaozai.iot.nettyplatform.code; 2 3 import java.net.InetSocketAddress; 4 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 8 import io.netty.channel.ChannelHandlerContext; 9 import io.netty.channel.SimpleChannelInboundHandler; 10 11 /** 12 * 服务Handler 处理 13 * @author Administrator 14 * 15 */ 16 public class SmartIotHandler extends SimpleChannelInboundHandler{ 17 18 19 private static final Logger log = LoggerFactory.getLogger(SmartIotHandler.class); 20 21 @Override 22 protected void channelRead0(ChannelHandlerContext ctx, SmartIotProtocol iot) 23 throws Exception { 24 log.info("收到设备数据包: " + iot.getFlowid()); 25 iot.printDebugInfo(); 26 ctx.write("ok"); 27 } 28 29 @Override 30 public void channelActive(ChannelHandlerContext ctx) throws Exception { 31 InetSocketAddress socket = (InetSocketAddress) ctx.channel().remoteAddress(); 32 String ip = socket.getAddress().getHostAddress(); 33 log.info("收到客户端IP: " + ip); 34 } 35 36 @Override 37 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 38 ctx.close(); 39 } 40 }
3.5 NettyServerInitializer.java
这个就是初始化本次Netty框架中,使用的编解码器,还有对应的处理类。
1 package com.wunaozai.iot.nettyplatform.config; 2 3 import com.wunaozai.iot.nettyplatform.code.SmartIotDecoder; 4 import com.wunaozai.iot.nettyplatform.code.SmartIotEncoder; 5 import com.wunaozai.iot.nettyplatform.code.SmartIotHandler; 6 7 import io.netty.channel.ChannelInitializer; 8 import io.netty.channel.ChannelPipeline; 9 import io.netty.channel.socket.SocketChannel; 10 11 /** 12 * 服务器初始化 13 * @author Administrator 14 * 15 */ 16 public class NettyServerInitializer extends ChannelInitializer{ 17 18 @Override 19 protected void initChannel(SocketChannel ch) throws Exception { 20 // ChannelPipeline pipeline = ch.pipeline(); 21 // //自定义切割符 22 // //ByteBuf delimiter = Unpooled.copiedBuffer(new byte[] {16}); 23 // ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes()); 24 // 25 // pipeline.addLast(new DelimiterBasedFrameDecoder(8192, delimiter)); 26 // pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); 27 // pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8)); 28 // pipeline.addLast(new NettyServerHandler()); 29 30 ChannelPipeline pipeline = ch.pipeline(); 31 //添加自定义编解码器 32 pipeline.addLast(new SmartIotEncoder()); 33 pipeline.addLast(new SmartIotDecoder()); 34 //处理网络IO 35 pipeline.addLast(new SmartIotHandler()); 36 } 37 38 }
3.6 NettyServer.java
Netty功能的入口类,所有Netty框架初始化步骤都在这里进行简单处理。
1 package com.wunaozai.iot.nettyplatform.config; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 import org.springframework.stereotype.Component; 6 7 import io.netty.bootstrap.ServerBootstrap; 8 import io.netty.channel.ChannelFuture; 9 import io.netty.channel.ChannelOption; 10 import io.netty.channel.EventLoopGroup; 11 import io.netty.channel.nio.NioEventLoopGroup; 12 import io.netty.channel.socket.nio.NioServerSocketChannel; 13 import io.netty.handler.logging.LogLevel; 14 import io.netty.handler.logging.LoggingHandler; 15 16 /** 17 * Netty 服务器 18 * @author Administrator 19 * 20 */ 21 @Component 22 public class NettyServer { 23 24 private static final Logger log = LoggerFactory.getLogger(NettyServer.class); 25 26 private int port = 7777; 27 28 public void run(){ 29 EventLoopGroup bossGroup = new NioEventLoopGroup(); 30 EventLoopGroup workerGroup = new NioEventLoopGroup(); 31 try { 32 ServerBootstrap serverBootstrap = new ServerBootstrap(); 33 serverBootstrap.group(bossGroup, workerGroup); 34 serverBootstrap.channel(NioServerSocketChannel.class); 35 serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024); 36 serverBootstrap.handler(new LoggingHandler(LogLevel.INFO)); 37 serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true); 38 serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); 39 serverBootstrap.childHandler(new NettyServerInitializer()); 40 // 绑定端口,开始接收进来的连接 41 ChannelFuture channelFuture = serverBootstrap.bind(port).sync(); 42 log.info("netty服务启动: [port:" + port + "]"); 43 // 等待服务器socket关闭 44 channelFuture.channel().closeFuture().sync(); 45 } catch (Exception e) { 46 log.error("Netty 服务启动失败: " + e.getMessage()); 47 }finally { 48 bossGroup.shutdownGracefully(); 49 workerGroup.shutdownGracefully(); 50 } 51 } 52 }
3.7 IotNettyPlatFormApplication.java
这个是Spring Boot项目的入口函数。在这里调用Netty的入口函数。
1 package com.wunaozai.iot.nettyplatform; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 import org.springframework.boot.SpringApplication; 6 import org.springframework.boot.autoconfigure.SpringBootApplication; 7 import org.springframework.context.annotation.ComponentScan; 8 import org.springframework.web.servlet.config.annotation.EnableWebMvc; 9 10 import com.wunaozai.iot.nettyplatform.config.NettyServer; 11 12 @SpringBootApplication 13 public class IoTNettyPlatFormApplication { 14 15 private static final Logger log = LoggerFactory.getLogger(IoTNettyPlatFormApplication.class); 16 17 18 public static void main(String[] args) { 19 SpringApplication.run(IoTNettyPlatFormApplication.class, args); 20 run(); 21 } 22 23 private static NettyServer nettyServer = new NettyServer(); 24 25 private static void run(){ 26 Thread thread = new Thread(new Runnable() { 27 @Override 28 public void run() { 29 nettyServer.run(); 30 } 31 }); 32 thread.start(); 33 } 34 35 }
我这里通过在@SpringBootApplication 这里调用NettyServer。同时还有其他方式:
1) 通过实现ApplicationListener
1 import org.slf4j.Logger; 2 import org.slf4j.LoggerFactory; 3 import org.springframework.context.ApplicationListener; 4 import org.springframework.context.event.ContextRefreshedEvent; 5 import org.springframework.stereotype.Component; 6 7 /** 8 * 项目初始化 9 * @author wunaozai 10 * @date 2018-05-24 11 */ 12 @Component 13 public class OnStartListener implements ApplicationListener{ 14 15 private static final Logger log = LoggerFactory.getLogger(OnStartListener.class); 16 17 @Override 18 public void onApplicationEvent(ContextRefreshedEvent arg0) { 19 log.info("Run on Start Listener."); 20 } 21 22 }
2) 通过实现CommandLineRunner
1 import org.slf4j.Logger; 2 import org.slf4j.LoggerFactory; 3 import org.springframework.boot.CommandLineRunner; 4 import org.springframework.core.annotation.Order; 5 import org.springframework.stereotype.Component; 6 7 /** 8 * 项目启动时初始化资源
9 * 如 一些初始化操作,提前加载加密证书,初始化线程池等 10 * @author wunaozai 11 * @date 2018-05-24 12 */ 13 @Component 14 @Order(value = 1) //执行顺序 15 public class Runner implements CommandLineRunner { 16 17 private static final Logger log = LoggerFactory.getLogger(Runner.class); 18 19 @Override 20 public void run(String... args) throws Exception { 21 log.info("The Runner start to Initialize."); 22 } 23 24 }
三、协议测试
四、简单架构
由于引入了自定义协议,所以需要对原先的流程进行简单的改造,下面这个图是某项目的架构图。
参考资料:
https://www.cnblogs.com/sidesky/p/6913109.html
架构系列: https://www.cnblogs.com/wunaozai/p/8067577.html
本文地址: https://www.cnblogs.com/wunaozai/p/11403015.html