Java根据协议进行拆包解决串口粘包
串口通信原理
1.串口通信指串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。
2.串口是计算机上一种非常通用的设备通信协议(不要与通用串行总线Universal SerialBus或者USB混淆)
3.典型地,串口用于ASCII码字符的传输。通信使用3根线完成:(1)地线,(2)发送,(3)接收。由于串口通信是异步的,端口能够在一根线上发送数据同时在另一根线上接收数据。其他线用于握手,但是不是必须的。串口通信最重要的参数是比特率、数据位、停止位和奇偶校验。对于两个进行通信的端口,这些参数必须匹配
粘包拆包
我们将串口通信的数据流类比作河流的流水,是连成一片的,中间并没有分隔界限。
举个例子,假设 PC 机(上位机)要通过串口向某传感器设备(下位机)发送两条消息,第一条是「 Hello 」;第二条是「 Serial Port 」。
理想情况下下位机应该收到两条消息分别解析为「 Hello 」和「 Serial Port 」,但是串口数据流是没有分隔界限的。
倘若两次通信的时间间隔较短,下位机可能只会收到一条消息「 Hello Serial Port 」,这就是粘包。
下位机也有可能会收到两条消息但是读取的时候分别识别为「 Hello Ser 」和 「 ial Port 」,这就是拆包。
因为串口数据流底层并不了解上层业务数据的具体含义,这就涉及到两个设备在进行串口通信时如何进行数据截断的问题,粘包拆包问题只能通过上层的应用协议栈设计来解决。
粘包和拆包不仅出现在串口通信中,而是一类问题。TCP 通信同样需要处理战报和拆包,对此,业界已经形成了几种主流的解决方案,netty解决粘包拆包可以见我上一篇博客。
准备工作
由于笔记本或台式机上基本上都没有成对的串口提供给我们调试使用,我们就需要下载虚拟串口软件来实现串口调试。
1.下载虚拟串口软件http://pan.baidu.com/s/1hqhGDbI(这里提供的还是比较好用)。下载安装完成后先不要急着运行,把压缩包中的vspdctl.dll文件复制到安装目录下如:我的目录为–>D:\SoftWareInstall\Virtual Serial Port Driver 7.2 替换原有文件即可成功激活。
2.打开软件添加虚拟串口,一般都是成对添加的(添加COM1、COM2)后如图所示:
另外还需要一个串口调试工具(如果你发的数据是16进制要把发送设置成hex模式)
串口处理粘包
串口处理粘包网上的代码基本都是c++,java的参考代码少之又少。
串口的基础代码可以看着篇博客,讲的很详细,获取可用串口,发送接收数据和注册监听器等
串口通信基础代码
给串口注册监听器,如果串口监听到有数据传来,就会触发readdata方法,如果缓冲区的数据不进行读取,就会不断就行循环,直到缓冲区数据被读取为止。
串口接收缓冲区是inputstream。用串口自带缓冲区就会出现一个问题,inputstream是不支持mark和reset方法的。而且进行拆包的精髓就是用mark和reset方法判断包头来进行拆包。串口和netty拆包最大的区别就是需要自建缓冲区。用netty的bytebuf来接收inputstream中的数据,用bytebuf作为一个媒介,bytebuf中封装了很多能用来直接用的方法。
如图所示,我申请了一个bytebuf缓冲区,为了防止被重新刷新我把他放在方法外。inputstream缓冲区的数据一个字节一个字节的读到bytebuf中,然后剩下的就根据你的协议来写对应的逻辑进行拆包,和netty一模一样了。
netty根据协议进行粘包拆包
可以参考我的上一篇博客,链接地址贴出来了。
放一下readdata的代码
static SerialPortParameter parameter;
static InputStream is = null;
static int length ;
static byte[] data = null;
static byte[] bytesData = null;
static ByteBuf buf = Unpooled.compositeBuffer();//无限制长度
static ByteBuffer frameBytes = null;
static int bufflenth ;
public static byte[] readData(SerialPort serialPort) {
try {
//获得串口的输入流
is = serialPort.getInputStream();
//获得数据长度
bufflenth = is.available();
if (bufflenth != 0) {//如果缓冲区无数据就不读取
buf.writeByte(is.read());//每次读取一个字节
//判断是否满足字节最小数目,不满足直接return等待数据到来
if (buf.readableBytes() < ProtocolConstant.FRAME_SIZE_MIN) {
return null;
}
int beginReader;//记录包头标志
while (true) {
// 获取包头开始的index
beginReader = buf.readerIndex();
// 标记包头开始的index
buf.markReaderIndex();
// 读到了协议的开始标志,结束while循环
if (buf.readByte() == ProtocolConstant.FRAME_HEAD_FLAG) {
byte[] lengthBytes1 = new byte[2];
lengthBytes1[0] = buf.readByte();
lengthBytes1[1] = buf.readByte();
int length1 = FrameCommUtil.bytes2Int(lengthBytes1, 0, lengthBytes1.length);//读取长度
if (buf.readByte() == ProtocolConstant.FRAME_MIDDLE_FLAG) {//如果跳过长度字节后一个为标志位
if (buf.readableBytes() >= length1 + 2) {//如果长度符合完整报文长度继续执行。
buf.readBytes(length1);//跳过数据区
buf.readByte();//跳过校验和
if (buf.readByte() == ProtocolConstant.FRAME_TAIL_FLAG) {//如果是包尾标志直接reset
buf.resetReaderIndex();//还原到包头位置
buf.readByte();//读取包头标志,因为默认包头已经读过
break;
}
} else {//如果不符合长度要求reset并return,等待剩余数据到来
buf.resetReaderIndex();
return null;
}
}
}
// 未读到包头,略过一个字节
// 每次略过,一个字节,去读取,包头信息的开始标记
buf.resetReaderIndex();
buf.readByte();
// 当略过,一个字节之后,
// 数据包的长度,又变得不满足
// 此时,应该结束。等待后面的数据到达
if (buf.readableBytes() < ProtocolConstant.FRAME_SIZE_MIN) {
return null;
}
}
byte[] lengthBytes = new byte[2];
lengthBytes[0] = buf.readByte();
lengthBytes[1] = buf.readByte();
length = FrameCommUtil.bytes2Int(lengthBytes, 0, lengthBytes.length);
// System.out.println(length);
if (buf.readableBytes() < length + 3) {//如果剩下可读的信息长度小于帧所需要的长度 +3的意思是校验和和尾帧标识和中间标识的字节数
// 还原读指针
buf.readerIndex(beginReader);//还原到帧头开始位置
// log.info("剩余字节数小于帧最小满足数2");
return null;
}
byte middle = buf.readByte();//读取中间标志位
data = new byte[length];//创造一个长度为length的容纳数据的数组
try {
buf.readBytes(data);//将数据写入数组
} catch (Exception e) {
e.printStackTrace();
return null;
}
byte check = buf.readByte();//读取校验和
byte tail = buf.readByte();//读取尾部标识
frameBytes = ByteBuffer.allocate(6 + length);//6+length的意思是数据字节总数+标志位+校验和
frameBytes.put(ProtocolConstant.FRAME_HEAD_FLAG);//放入头部标识
frameBytes.put(FrameCommUtil.int2Bytes(length), 0, ProtocolConstant.FRAME_LENGTH_BYTESIZE);//放入长度
frameBytes.put(ProtocolConstant.FRAME_MIDDLE_FLAG);//放入中间标识
frameBytes.put(data);//放入数据
frameBytes.put(check);//放入校验和
frameBytes.put(ProtocolConstant.FRAME_TAIL_FLAG);//放入尾部标识
bytesData = frameBytes.array();//转换为数组
return bytesData;//返回完整报文
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
我们最后来进行测试,如图一次性发三条报文。发现拆成了一条一条的并且显示了,还可以处理垃圾报文,如果把一条报文拆成两部分发也会合成一条报文并显示,感兴趣的可以测试测试
最后如果对这部分感兴趣可以私信我,我发给你们项目代码。