工作调动。暂时停更了一段时间。续上一篇我们学习了如何去自定义一组报文,今天我们接着解析和组装报文。
前面我们讲过在物联网通信中实际上不论我们使用什么方式作为通信介质,其本质就是字节。所以我再一次对本章节的内容进行了调整,我们不讲Socket和ServerSocket这两个阻塞式IO Socket如何写。那个意义不大。
也正式因为在上一节中有读者提出说需要知道报文该如何拆解就有了这一篇。
章节
- Android与物联网设备通信-概念入门
- Android与物联网设备通信-数据传递的本质
- Android与物联网设备通信-网络模型分层
- Android与物联网设备通信-UDP协议原理
- Android与物联网设备通信-TCP协议原理
- Android与物联网设备通信-基于TCP/IP自定义报文
- Android与物联网设备通信-什么是字节序
- Android与物联网设备通信- 字节报文组装与解析
- Android与物联网设备通信-利用UDP广播来做设备查找
- Android与物联网设备通信-实现远程控制Android客户端
- Android与物联网设备通信-Android做小型服务器
- Android与物联网设备通信-调试技巧
- Android与物联网设备通信-并行串行与队列
- Android与物联网设备通信-数据安全
- Android与物联网设备通信-心跳
- Android与物联网设备通信-网络IO模型
目录
- 组装报文
- 解析报文
- 总结
组装报文
接上一节的功能拿来,我们到底是怎么做到把下面的结构体转换成字节的呢?
协议:
转换后的字节:
0x00 0x00 0x00 0x11 | 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 | 0x21 | 0x01 |0x57 0x9D
首先我们需要定义一个对象,当做结构体。(不明白为什么要这样定义的同学请看上一节文章)
1 | public class AirCondBaseStructure { |
针对这个结构体,我们再写一个枚举:1
2
3
4
5
6
7
8
9public enum CodeEnum {
POWER((byte) 0x21),//开关
MODE((byte) 0x22),//模式
TEMPERATURE((byte) 0x23);//温度
byte code;
CodeEnum(byte i) {
code = i;
}
}
该枚举器正好对应功能对照表里的功能。
写一下构造方法。1
2
3
4
5
6
7public AirCondBaseStructure(String sn, AirCondBaseStructure.CodeEnum code, byte[] data){
this.sn = sn.getBytes(); // 序列号
this.code = code.code;
this.data = data;
this.len=computeLen();//获取长度
this.crc =computeCrc();//校验
}
这里构造方法只传入三个参数:
- 序列号
- 功能
- 透传内容
可以看到实际里面还有长度
和crc校验
。是根据computeLen();
和computeCrc();
来自动完成计算的。
computeLen方法:1
2
3
4
5
6
7/**
* 计算长度
* @return
*/
private int computeLen() {
return sn.length + 1/*功能码长度*/ + data.length + 2/*校验和*/;
}
这里的长度是固定字段+可变长。
computeCrc方法:
1 | /** |
crc校验是使用是crc16/modbus
标准。完整代码后面贴出来。
最后是把报文组合起来变成字节。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* 序列化
* @return
*/
public byte[] toBytes(){
byte[] datas=null;
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
dataOutputStream.writeInt(len);
dataOutputStream.write(sn);
dataOutputStream.writeByte(code);
dataOutputStream.write(data);
dataOutputStream.write(crc);
dataOutputStream.flush();
datas = byteArrayOutputStream.toByteArray();
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
return datas;
}
这里我们使用的就是ByteArrayOutputStream+DataOutputStream把对应的字段按照顺序写入,以保证和协议一致。
最后我们看看一下使用结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public static void main(String[] args) {
String sn="1234567890123";
AirCondBaseStructure powerOpen =new AirCondBaseStructure(sn, AirCondBaseStructure.CodeEnum.POWER, new byte[]{0x01});
System.out.println("空调打开报文:"+hex2Str(powerOpen.toBytes()));
AirCondBaseStructure powerCloce =new AirCondBaseStructure(sn, AirCondBaseStructure.CodeEnum.POWER, new byte[]{(byte)0x00});
System.out.println("空调关闭报文:"+hex2Str(powerCloce.toBytes()));
AirCondBaseStructure modeAuto =new AirCondBaseStructure(sn, AirCondBaseStructure.CodeEnum.MODE, new byte[]{(byte)0x03});
System.out.println("空调自动报文:"+hex2Str(modeAuto.toBytes()));
AirCondBaseStructure modeDeh =new AirCondBaseStructure(sn, AirCondBaseStructure.CodeEnum.MODE, new byte[]{(byte)0x05});
System.out.println("空调除湿报文:"+hex2Str(modeDeh.toBytes()));
AirCondBaseStructure temperature =new AirCondBaseStructure(sn, AirCondBaseStructure.CodeEnum.TEMPERATURE, new byte[]{(byte)0x00, (byte) 0x19});
System.out.println("空调25度报文:"+hex2Str(temperature.toBytes()));
}
对上层而言组装的方式比较简单,只需要把对应的枚举功能设置进去,带上该有的数据,剩下的序列化和校验均在内部实现。
这里我们模拟了五组报文,输出结果如下。
控制台输出:1
2
3
4
5空调打开报文:0x00 0x00 0x00 0x11 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 0x21 0x01 0x57 0x9D
空调关闭报文:0x00 0x00 0x00 0x11 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 0x21 0x00 0x96 0x5D
空调自动报文:0x00 0x00 0x00 0x11 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 0x22 0x03 0xD6 0xAC
空调除湿报文:0x00 0x00 0x00 0x11 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 0x22 0x05 0x56 0xAE
空调25度报文:0x00 0x00 0x00 0x12 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 0x23 0x00 0x19 0x4D 0x94
我们来细看一下他们的变化。
到这里我们就已经将报文组装好并转换成了字节数组。
也许我们此时会想,那怎么发出去呢?我们前面第一节就讲过其实并不管中间用什么在传输。他们最后都是字节或二进制的形式。所以我们都已经转换好了,剩下的都是很简单的事情了。就是把数据喂进去,喂给TCP/IP、UDP、蓝牙、红外、无线电、声波、电磁波….看实际情况来了。
完整代码:
1 | import java.io.ByteArrayOutputStream; |
1 |
|
1 | import java.io.*; |
解析报文
解析报文的过程就是把字节数组再反过来转换成对象,也叫反序列化。
这里由于是文章演示(偷懒),就不像前面组装报文的时候再做封装了。
1 | import java.io.ByteArrayInputStream; |
我们看到到main方法。主要就是解析一组电源打开的报文。其实没有什么东西,就是利用DataInputStream来读取。只是长度是变动的,所以我们需要先读出长度,然后根据长度再向后读出其它字段。
然后控制台就会输出1
2
3
4
5报文长度:17
sn:1234567890123
功能码:0x21
数据:0x01
校验:0x57 0x9D
实际的业务情况会比这个复杂,所以最好做一些抽象和封装。不要像我文章里写的流水式编程。因为如果不封装后面维护会看起来很痛苦。还会有大量重复代码。不应该对每一组报文都重复的手写一次解析的过程。
总结
组装和解析字节报文其实是一项非常基础的能力。在java中已经有比较方便的DataInput类型套接字方便解析了。我们平时调试和开发的过程中尽量做到log日志,见行知意,并最好把hex格式转换成字符串来查看。一般出奇两头碰协议的时候容易引发bug。到后期稳定后基本上不用再碰。
再一个可能会有小伙伴疑惑为什么不直接用json来做透传。这个问题很早之前就讨论过,要尽量避免透传内容太大。用字节位来做字段会增加传播的效率和稳定性。学习本篇的内容后,其实还可以尝试去用字节的方式读取MP3、jpg、apk等文件中的头信息。也是一个好练手的办法。