SL651协议数据通过在互联网中发送时可看成是一种应用层协议。本文用框架netty解析了该协议的中心站端的一部分,协议其余部分解析类似。
首先需要了解在SL651协议中的中心站、遥测站、遥测终端设备分别代表是什么。
在SL651-2014规约中建议遥测终端设备和遥测站通信采用的是Modbus-RTU或SDI-12协议。参考
详解Modbus通信协议---清晰易懂-CSDN博客
本文涉及到的是在SL651-2014中定义的遥测站和中心站的通信协议。
1.概要
1.1报文解析
1.1.1编码格式
SL651协议提供了2种编码格式:HEX/BCD编码和ASCII字符编码。本文实现主要基于ASCII字符编码,HEX/BCD编码也简要分析。文中A设备采用了ASCII编码,B设备采用了HEX/BCD编码。
1.1.2数据解析
报文中的主要数据为:帧起始符,中心站地址,遥测站地址,密码,功能码,帧起始符,功能码(报文类型),遥测站分类码,观测时间,监测项,校验码等。
1.1.3数据校验
在SL651协议中提供了数据的校验方式,通过CRC16方式计算正文及之前的所有报文的校验码,再在报文正文后面加上结束符和校验码。在链路层、传输层本身分别也有校验方式。但它们是独立的。
1.2.数据的完整性和通信方式
主要考虑的是半包粘包问题,还有数据校验,通信方式及连接维持方式。
1.2.1通信方式及连接维持方式
首先SL651协议定义了4种中心站和遥测站的通信方式(M1,M2,M3,M4)。其中M1为发送/无回答传输模式。M2为发送/确认传输模式。M3为多包发送/确认传输模式,M4为查询/响应模式。本文是基于M2传输模式。
在传输层,操作系统的默认保活时间是7200秒。为了保持连接提高效率,在SL651应用层也定义了一些维持连接机制。遥测端会定时发送链路维持报,以保持连接。这样可以在应用层自定义保活时间。本文在应用层简化不做处理,所以保活时间默认是传输层的7200秒。
1.2.2半包粘包
粘包问题在netty中提供了类中已经基本解决了。而半包问题比较复杂,如需开发高效的解决半包问题可以参考netty提供的类io.netty.handler.codec.http.HttpObjectDecoder的处理方式。因为半包发生时一次完整数据可能在任何字节截断。HttpObjectDecoder中对每个解析阶段都定义了一个状态,如遇到半包情况,只需从当前状态解析就可以了,但是编程较复杂。
本文较为简单的处理了半包问题:先创建一个ByteBuf副本,解析一遍数据,在ByteBuf副本读取数据不足时抛出InsufficientDataException异常,不做任何处理。如果解析成功就将原ByteBuf指针设置到ByteBuf副本的位置(等价于原ByteBuf解析了数据)。 遇到其他解析错误的情况,就清空ByteBuf所有数据。以上处理效率不高,如果有多个半包,可能重复解析起始的byte多次,最差的情况下复杂度为O(n^2)。这里水文协议因为一次请求数据不长,所以影响不大。
2.报文解析
2.1编码格式
SL651协议中定义了2种编码格式HEX/BCD编码和ASCII字符编码。SL651规定了在报文的起始字节标识报文属于那种编码格式。ASCII的报文容易理解但较长,在自定义监测项时有较强的扩展性易读性,起始字节01。HEX编码较短,在自定义报文时扩展性较差,起始字节7E7E。
文中A设备采用了ASCII编码,B设备采用了HEX/BCD编码。
图2.1.1、图2.1.2分别是A设备的ASCII编码的测试报文,B设备的HEX/BCD编码测试报文。
图2.1.1 ASCII编码的测试报文(A设备)
图2.1.2 HEX/BCD编码测试报文(B设备)
图2.1.1,图2.1.2是2种编码的报文在EditPlus中的16进制显示格式。
计算机中数据都是二进制字节流。一般将二进制字节流代表字符含义的情况时称为字符流,当二进制字节流不代表字符含义称为字节流。
两种报文解析细节见SL651文档。它们的报文起始符报文结束符是相同的,差不多就是图2.1.1中显示不出字符的那些字节。根据SL651文档中的定义,ASCII报文与HEX/BCD报文的不同之处大致体现在:
1.数据在ASCII报文中表示为一串ASCII字符,在HEX/BCD报文按照字节的原始值或者BCD码原始值表示。
2.数据在ASCII报文中通常以字节20(空格字符) 表示监测值、监测项数据的分隔。而在HEX/BCD报文中需要额外增加1字节的数据定义字节,表示监测值数据长度(如图2.1.2中的第0x26的字节)。
3.SL651协议主要是定义降雨等相关数据的,在遇到自定义的数据时,ASCII报文扩展性较强,因为有空格字符作为分格符,可以任意定义字符串名称。HEX/BCD报文需要在已定义的监测项编码后面扩展(见SL651-2014附录C),并且没有可读性。在图2.1.2中在字节0x36后就自定义了的FF12和FF11监测项编码,还定义了无数据时数据为AAAAAA。
2.2数据解析
数据解析时,ASCII报文和HEX/BCD报文的主要对应的原始数据含义是一样的。主要区别就是:HEX/BCD报文尽量将一个原始数据的2位数,用HEX或BCD编码进一个字节,用字节16进制查看能直观看清,比如图2.1.2中字节0x20——0x24 用BCD编码2206021420代表了22年6月2日14时20分。而ASCII报文会将这个时间的解析为每一个数字的ASCII报文,比如图2.1.1中字节0x3B——0x46就是240115110000的ASCII码。
参考SL651-2014全协议解析(HEX编码格式)-CSDN博客
2.2.1 报文解析注意点和一些简化处理
1.在SL651-2014附录B中提供了多种报文类型。本文简化处理,只解析定时报。
2.在SL651-2014的6.6.2.7节中,规定报文正文可以存在多个遥测站。本文简化处理默认一条报文中只存在一个站点。
2.3数据校验
在传输层TCP协议中已经提供了很多确保数据可靠性机制。比如对报文的编号,ack确认,超时重传,数据校验等机制,能确保应用层SL651接收到的报文是按顺序的、完整的。在SL651协议中也简单的定义了数据校验机制。SL651应用层接收报文需要根据校验码进行CRC16数据校验,确认报文需要发送校验码。这里简化只对确认确认报文发送校验码。需要注意的时HEX/BCD报文的校验码是2个字节,ASCII编码的校验码是对HEX/BCD报文2个字节校验码的HEX格式在进行ASCII编码,也就是4个字节。
参考
Java实现CRC16算法校验_java 参数为16进制字符串crc16实现-CSDN博客
3.数据的完整性和通信方式
3.1通信方式及连接维持方式
本文的协议解析实现是基于ASCII编码的。通过测试报文日志可以看出,在接收到遥测站上行报文后,需要发送下行报文确认。否则遥测站会因为超时重传机制发送多次上行报文。可以推断出遥测站应该是工作在SL651协议的M2模式上的。
图3.1 遥测站上行报文日志
从图3.1可以看出一些点位因为发送小时报后没有接收到确认报文,所以多次发送小时报。上图中还能看到链路维持报,链路维报并不需要回复确认报文。链路维报可在应用层提供连接维持机制。本文在应用层简化不做这些处理,所以保活时间默认是操作系统在传输层(TCP)定义的7200秒。
3.2半包粘包
半包产生原因主要是链路层最大传输限制。又有发送端端编程等问题。
参考【网络】什么是MTU|MTU 优化|最大传输单元-CSDN博客
粘包产生原因主要是发送端编程问题和接收端接收的缓冲区的问题。
3.2.1解决方案
在netty框架中提供了io.netty.handler.codec.ByteToMessageDecoder解决部分半包粘包问题。
ByteToMessageDecoder扩充了ChannelInboundHandlerAdapter.channelRead方法,ByteBuf没读完的数据在ByteToMessageDecoder.callDecode方法中会继续读,部分解决了粘包半包问题,数据读取多少,怎么读取,异常处理需要自己解决。ByteBuf在ByteToMessageDecoder释放,在这里如果读取错误只需跳过所有ByteBuf字节。这里较为简单的处理了半包问题:先创建一个ByteBuf副本,解析一遍数据,在ByteBuf副本读取数据不足时抛出InsufficientDataException异常,不做任何处理。如果解析成功就将原ByteBuf指针设置到ByteBuf副本的位置(等价于原ByteBuf解析了数据)。
3.2.2参考代码
列出部分参考代码。解析完成后处理数据的那部分不列出。
netty版本:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.101.Final</version>
</dependency>
netty服务端启动
@Slf4j
public class ProtocolSL651ServerServer {
public void start(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);//负责连接
EventLoopGroup workGroup = new NioEventLoopGroup(16);
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workGroup)
//选择服务器的SeverSocketChannel
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast("DeviceDataDecodeHandler", new ProtocolSL651DecodeHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 256) // determining the number of connections queued
.childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE);
ChannelFuture future = b.bind(port).sync();
log.info("netty:{}启动成功",port);
future.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
netty服务端handler解码器
/**
* @author yaoct
* @date 2024/1/16 10:32
* @description:
* SL651解析,这里较为简单的处理了半包问题:先创建一个ByteBuf副本,解析一遍数据,
* 在ByteBuf副本读取数据不足时抛出InsufficientDataException异常,不做任何处理。
* 如果解析成功就将原ByteBuf指针设置到ByteBuf副本的位置(等价于原ByteBuf解析了数据)。
* 遇到其他解析错误的情况,就清空ByteBuf所有数据。
* 以上处理效率不高,如果有多个半包,可能重复解析起始的byte多次,最差的情况下复杂度为O(n2)。因为半包本身可能在任何字节截断。
* 这里水文协议因为一次请求数据不长所以影响不大。
* 如需效率高的解析可以参考{@link io.netty.handler.codec.http.HttpObjectDecoder}
* HttpObjectDecoder中对每个解析阶段都定义了一个状态,如遇到半包情况,只需从当前状态解析就可以了。但是编程较复杂。
* 注意要定义最大数据长度。因为可能会遇到恶意报文。
*/
@Slf4j
public class ProtocolSL651DecodeHandler extends ByteToMessageDecoder {
private String pointCode=null;
private StringBuilder logStringBuilder = null;//日志记录
final private String YL="YL";
final private String HDSW="HDSW";
final private String CSJS="JS";
final private int maxLength=1000;//最大包长度定为1000,避免报文发送错误或恶意报文情况
static HashMap<String,HydrologyDTO> pointMap=new HashMap<>();
public static void main(String[] args) {
byte[] bytes = new byte[100];
for(int i=0;i<bytes.length;i++)bytes[i]=(byte) i;
ByteBuf byteBuf = Unpooled.copiedBuffer(bytes);
System.out.println(byteBuf.readerIndex());
int i = byteBuf.readInt();
byte[] bytes1 = ByteBufUtil.getBytes(byteBuf, 0, 3);
System.out.println(byteBuf.readerIndex()+":"+byteBuf.readableBytes());
//深拷贝
ByteBuf copy = byteBuf.copy();
System.out.println(copy.readerIndex()+":"+copy.readableBytes());
byte[] bytes2 = ByteBufUtil.getBytes(copy, 0, 3);
//浅拷贝,并拷贝指针
ByteBuf duplicate = byteBuf.duplicate();
int i1 = duplicate.readInt();
duplicate.retain();//引用计数+1,引用计数为0是释放
System.out.println(duplicate.readerIndex()+":"+duplicate.readableBytes());
byte[] bytes3 = ByteBufUtil.getBytes(copy, 0, 3);
//浅拷贝,不拷贝指针
ByteBuf slice = byteBuf.slice();
System.out.println(slice.readerIndex()+":"+slice.readableBytes());
byte[] bytes4 = ByteBufUtil.getBytes(copy, 0, 3);
ByteBuf retain = slice.retain();
int c1 = byteBuf.refCnt();
boolean release = ReferenceCountUtil.release(byteBuf);
int c2 = byteBuf.refCnt();
boolean release1 = ReferenceCountUtil.release(byteBuf);
int c3=duplicate.refCnt();
int c4=slice.refCnt();
int v=1;
}
/**
* ByteToMessageDecoder扩充了ChannelInboundHandlerAdapter.channelRead方法,
* ByteBuf没读完的数据在ByteToMessageDecoder.callDecode中会继续读,部分解决了粘包半包问题,数据读取多少,怎么读取,异常处理需要自己解决。
* ByteBuf在ByteToMessageDecoder释放,在这里如果读取错误只需跳过所有ByteBuf字节
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
logStringBuilder=new StringBuilder();
//System.out.println(bytes.length);
//ctx.writeAndFlush(Unpooled.copiedBuffer(new byte[100]));
//if(1==1)throw new RuntimeException(); //报异常需自己跳过ByteBuf,否则ByteBuf会累加可能导致内存泄漏,
// 参考io.netty.handler.codec.http.HttpObjectDecoder.invalidMessage,报异常直接丢弃ByteBuf
try {
int num = in.readableBytes();
if(num>maxLength){
throw new RuntimeException("ByteBuf长度为"+num+",超过最大长度"+maxLength);
}
ByteBuf duplicate = in.duplicate();//浅拷贝,并拷贝指针,为了初步解决半包问题
decodeSL651(ctx, duplicate, out);
byte[] parseData = ByteBufUtil.getBytes(in, in.readerIndex(), duplicate.readerIndex() - in.readerIndex());
log.info("{},成功解析报文{}",logStringBuilder.toString(),bytes2HEXString(parseData));//当前成功解析报文
in.readerIndex(duplicate.readerIndex());//成功读取一次报文后设置新的指针
}catch (InsufficientDataException e){
byte[] bytes = ByteBufUtil.getBytes(in);
String byteHex = bytes2HEXString(bytes);
log.info("当前报文为半包{}",byteHex);
}catch (Exception e){
//解析错误需清空ByteBuf,否则ByteBuf会累加可能导致内存泄漏,参考io.netty.handler.codec.http.HttpObjectDecoder.invalidMessage,报异常直接丢弃ByteBuf
handleParseErrorByIgnore(in, ctx);
log.error("",e);
return;
}
}
private void decodeSL651(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws InsufficientDataException {
byte[] bytes = ByteBufUtil.getBytes(in);
List<Byte> outBuffer=new ArrayList<>();//下行报文
String hexStr=bytes2HEXString(bytes);
boolean isValid = checkCrc16Ascii(bytes);
// messageThreadLocal.set(hexStr);
//651协议起始码,01:ascii编码,7E7E:HEX编码
byte c = in.readByte();
outBuffer.add(c);
if(Byte.toUnsignedInt(c)==0x01){
DeviceData deviceData = parseAscii(ctx, in, out,outBuffer);
//deviceData==null 代表没有解析
if(deviceData!=null){
HashMap<String,String> dataMap = deviceData.getDataMap();//数据
String gcsj = deviceData.getGcsj();//观测时间
String dzbm = deviceData.getDzbm();//站点地址
String yczflm = deviceData.getYczflm();//遥测站分类码
Date observeTime=null;
//报文中观测时间格式不规范
try {
observeTime=TimeUtil.str2date(gcsj,"yyMMddHHmm");
}catch (Exception e){
}
try {
Date observeTime1=TimeUtil.str2date(gcsj,"yyMMddHHmmss");
if(observeTime1!=null){
observeTime=observeTime1;
}
}catch (Exception e){
}
if(observeTime==null){
log.error("站点{}观测时间解析失败:{}",dzbm,gcsj);
throw new RuntimeException("站点观测时间解析失败");
}
HydrologyDTO dto = new HydrologyDTO();
dto.setTimeStamp(observeTime.getTime());
log.info("站点{},观测时间{}",dzbm,gcsj);
if(dataMap.containsKey(YL)){
String yl = dataMap.get(YL);
try {
Double val = Double.valueOf(yl);
dto.setYl(val);
log.info("站点{}压力,值{}",dzbm,yl);
}catch (Exception e){
log.error("站点{}压力解析失败,值{}",dzbm,yl);
}
}
if(dataMap.containsKey(CSJS)){
String js = dataMap.get(CSJS);
try {
Double val = Double.valueOf(js);
dto.setJs(val);
log.info("站点{}城市积水,值{}",dzbm,js);
}catch (Exception e){
log.error("站点{}城市积水解析失败,值{}",dzbm,js);
}
}
if(dataMap.containsKey(HDSW)){
String hdsw = dataMap.get(HDSW);
try {
Double val = Double.valueOf(hdsw);
dto.setHdsw(val);
log.info("站点{}河道水位,值{}",dzbm,hdsw);
}catch (Exception e){
log.error("站点{}河道水位解析失败,值{}",dzbm,hdsw);
}
}
HashMap<String, String> deviceMap = DeviceRunner.deviceMap;
String sbbm = deviceMap.get(dzbm);//出厂编码转设备编码
if(sbbm==null){
log.error("设备出厂编号{}找不到对应",dzbm);
}else{
log.info("设备出厂编号{},推送设备编号{}",dzbm,sbbm);
}
if(sbbm==null)sbbm=dzbm;
if(dto.getHdsw()!=null||dto.getJs()!=null||dto.getYl()!=null){//有其中一项数据才记录
HydrologyDTO preHydrologyDTO = pointMap.get(dzbm);
if(preHydrologyDTO==null||preHydrologyDTO.getTimeStamp()<dto.getTimeStamp()){
pointMap.put(dzbm,dto);//只推最新数据,原始地址编码为key
DataSyncService dataSyncService = SpringContextUtil.getBean(DataSyncService.class);
HydrologyService hydrologyService = SpringContextUtil.getBean(HydrologyService.class);
dataSyncService.pushData(hydrologyService.constructSyncParam(sbbm,dto));
}else{
log.info("设备出厂编号{},推送设备编号{}当前不是最新数据,不推送",dzbm,sbbm);
}
}else{
log.info("设备出厂编号{},推送设备编号{}无数据",dzbm,sbbm);
}
}else{
in.skipBytes(in.readableBytes());//跳过
// log.info("报文未解析:"+hexStr);
}
}else{
//暂不支持HEX编码
// handleParseErrorByIgnore(in, ctx);
throw new RuntimeException("不支持的编码标识符");
}
}
private boolean checkCrc16Ascii(byte[] bytes) {
//验证Ascii数据的有效性最后4字节是验证位,Ascii码转换为2字节HEX
//这里不验证 TODO
return true;
}
private DeviceData parseAscii(ChannelHandlerContext ctx, ByteBuf in, List<Object> out, List<Byte> outBuffer) throws InsufficientDataException {
//中心站地址,不需要skip
byte[] bytes1 = readBytes(in, 2);
//遥测站地址
//根据协议可以接收多个遥测站地址,这里默认只解析一个 TODO
byte[] bytes = readBytes(in, 10);
String address=byte2Str(bytes);
pointCode=address;
outBuffer.addAll(bytes2List(bytes));
outBuffer.addAll(bytes2List(bytes1));
//密码,暂不解析skip
bytes = readBytes(in, 4);
String password = byte2Str(bytes);
if(!checkPassword(address,password)){
log.error("报文密码不正确,设备地址{},密码{}",address,password);
return null;
}
outBuffer.addAll(bytes2List(bytes));
//功能码
bytes = readBytes(in, 2);
String gnm=byte2Str(bytes);
gnm=gnm.toLowerCase();
outBuffer.addAll(bytes2List(bytes));
//上下行标识符
outBuffer.add((byte)'8');//下行报文标识符固定‘8’
List<Byte> outBufferContent = new ArrayList<>();
//报文上行标识符及长度
readBytes(in,1);//0标识上行,8表示下行,这里都是下行,暂不校验
String lenHex = readBytes2Str(in, 3);//正文字节长度
int len = strHex2Int(lenHex);
//报文起始符 02,暂不校验
byte b = in.readByte();
// int len1=Math.min(len,in.readableBytes());
byte[] content = readBytes(in, len);//正文
DeviceData deviceData=null;
String type="";
if(gnm.equals("32")){//解析定时报
logStringBuilder.append("SL651协议定时报数据接入地址:"+address);
type="定时报";
// log.info("SL651协议定时报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
deviceData=parseDSB(content,outBufferContent);
}else if(gnm.equals("34")){
// log.info("SL651协议小时报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
logStringBuilder.append("SL651协议小时报数据接入地址:"+address);
type="小时报";
deviceData=parseXSB(content,outBufferContent);
}else if(gnm.equals("2f")){
// log.info("SL651协议链路维持报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
logStringBuilder.append("SL651协议链路维持报数据接入地址:"+address);
type="链路维持报";
return null;//不解析,可能是链路维持报2F,不需要发送确认报文
}else{
// log.info("SL651协议其他报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
logStringBuilder.append("SL651协议其他报数据接入地址:"+address);//暂不解析,不发送确认报文
return null;
}
//结束符
bytes = readBytes(in, 1);
//校验码
String jym = readBytes2Str(in, 4);//hex转ascii的结果
//相应报文
//3个HEX字符标识正文长度
int size = outBufferContent.size();
String outContentLen = Integer.toHexString(size);
for(;outContentLen.length()<3;){
outContentLen='0'+outContentLen;
}
bytes=str2Bytes(outContentLen);
outBuffer.addAll(bytes2List(bytes));
outBuffer.add((byte)0x02);//起始符
outBuffer.addAll(outBufferContent);//正文
outBuffer.add((byte)0x06);//报文结束符,这里是ACK
//发送下行报文
byte[] crc16 = Crc16Utils.getCrc16(outBuffer);
byte[] crc16HexAscii = byte2HexAsciiByte(crc16);
for(byte crc:crc16HexAscii){
outBuffer.add(crc);
}
ByteBuf buffer = ctx.alloc().buffer();
byte[] resByte=new byte[outBuffer.size()];
for(int i=0;i<outBuffer.size();i++){
resByte[i]=outBuffer.get(i);
}
log.info("站点{}发送{}确认报文:{}",address,type,bytes2HEXString(resByte));
ctx.writeAndFlush(buffer.writeBytes(resByte));
return deviceData;
}
//验证密码,暂不验证
private boolean checkPassword(String address, String password) {
return true;
}
private byte[] str2Bytes(String str) {
byte[] ret=new byte[str.length()];
for(int i=0;i<str.length();i++){
char c=str.charAt(i);
ret[i]=(byte) c;
}
return ret;
}
private List<Byte> bytes2List(byte[] bytes) {
List<Byte> ret = new ArrayList<>();
for(byte b:bytes){
ret.add(b);
}
return ret;
}
private String bytes2HEXString(byte[] bytes) {
StringBuilder sb=new StringBuilder();
for(byte b:bytes){
String s = Integer.toHexString(Byte.toUnsignedInt(b));
if(s.length()==1)s='0'+s;
sb.append(s.toUpperCase());
}
return sb.toString();
}
//定时报
private DeviceData parseDSB(byte[] bytes, List<Byte> outBuffer) {
DeviceData deviceData = new DeviceData();
Content content = new Content(bytes);
//流水号
String lsh = content.readStr(4);
byte[] bytes2 = str2Bytes(lsh);
outBuffer.addAll(bytes2List(bytes2));//流水号相应正文
String xysj = TimeUtil.getThisTime("yyMMddHHmmss");//相应发报时间
bytes2 = str2Bytes(xysj);
outBuffer.addAll(bytes2List(bytes2));
deviceData.setLsh(lsh);
//发报时间yyMMddHHmmss
String fbsj = content.readStr(12);
deviceData.setFbsj(fbsj);
//地址标识符ST
String dzbsf = content.nextBySpace();
//地址编号
String dzbm = content.nextBySpace();
deviceData.setDzbm(dzbm);
//遥测站分类码
String yczflm = content.nextBySpace();
deviceData.setYczflm(yczflm);
//观测时间分类码
String gcsjflm = content.nextBySpace();
String gcsj = content.nextBySpace();//yyMMddHHmm 或 yyMMddHHmmss 遥测站报文时间格式不一致
deviceData.setGcsj(gcsj);
//要素信息组,最后是电压
while(content.isReadable()){
String label = content.nextBySpace();//监测项标识符,
if(label==null){//解析结束
break;
}
String val = content.nextBySpace();//监测值,注意值不存在的情况可能是FFFFFFFF
deviceData.getDataMap().put(label,val);
if(label.equals("VT")){//电压固定为最后一项
deviceData.getDataMap().put(label,val);
break;
}
}
return deviceData;
}
@Data
class DeviceData{
//流水号
String lsh;
//发报时间
String fbsj;
//地址编号
String dzbm;
//遥测站分类码
String yczflm;
//观测时间
String gcsj;
//监测值(要素标识符,数据)
HashMap dataMap=new HashMap();
}
//小时报,暂不解析,为了发送确认报文,这里只解析确认报文需要的信息
private DeviceData parseXSB(byte[] bytes, List<Byte> outBuffer) {
DeviceData deviceData = new DeviceData();
Content content = new Content(bytes);
//流水号
String lsh = content.readStr(4);
byte[] bytes2 = str2Bytes(lsh);
outBuffer.addAll(bytes2List(bytes2));//流水号相应正文
String xysj = TimeUtil.getThisTime("yyMMddHHmmss");//相应发报时间
bytes2 = str2Bytes(xysj);
outBuffer.addAll(bytes2List(bytes2));
return null;
}
private class Content{
int startIdx;
int endIdx;
int idx;//当前下标
byte[] content;
Content(byte[] content){
this(content,0,content.length-1);
}
Content(byte[] content,int startIdx,int endIdx){
this.content=content;
this.idx=startIdx;
this.startIdx=startIdx;
this.endIdx=Math.min(endIdx,content.length);
}
boolean isReadable() {
return idx <= endIdx;
}
String readStr(int nextLength){
String ret=byte2Str(content,idx,idx+nextLength-1);
idx=idx+nextLength;
return ret;
}
//空格分隔(暂时写死)
String nextBySpace(){
idx=nextSpaceIdxSkip();
if(idx==endIdx+1){//后面全是空格
return null;
}
int nextIdx=nextSpaceIdx();
String str = byte2Str(content, idx, nextIdx - 1);
idx=nextIdx;
return str;
}
//后面第一个 空格 的下标
int nextSpaceIdx(){
for(int i=idx;i<=endIdx;i++){
byte b=content[i];
if(b==0x20){
return i;
}
}
return endIdx+1;
}
//可处理多个空格情况,非必须
int nextSpaceIdxSkip(){
for(int i=idx;i<=endIdx;i++){
byte b=content[i];
if(Byte.toUnsignedInt(b)!=0x20){
return i;
}
}
return endIdx+1;
}
}
String byte2Str(byte[] bytes){
return byte2Str(bytes,0,bytes.length-1);
}
//将每1字节转换位2个16进制码,在转换位2字节Ascii
byte[] byte2HexAsciiByte(byte[] bytes){
int len=bytes.length*2;
byte[] ret=new byte[len];
for(int i=0;i<bytes.length;i++){
byte b=bytes[i];
String s = Integer.toHexString(Byte.toUnsignedInt(b));
if(s.length()==1)s='0'+s;
char ch1=s.charAt(0);
char ch2=s.charAt(1);
ret[2*i]=(byte) ch1;
ret[2*i+1]=(byte) ch2;
}
return ret;
}
String byte2Str(byte[] bytes,int s,int e){
StringBuilder sb = new StringBuilder();
for(int i=s;i<=e;i++){
sb.append((char) bytes[i]);
}
return sb.toString();
}
int strHex2Int(String str){
int ret=0;
str=str.toLowerCase();
for(int i=0;i<str.length();i++){
char c=str.charAt(i);
ret=ret*16;
if('0'<=c&&c<='9'){
ret+=c-'0';
}
if('a'<=c&&c<='f'){
ret+=10+c-'a';
}
}
return ret;
}
byte[] readBytes(ByteBuf byteBuf,int length) throws InsufficientDataException {
if(!byteBuf.isReadable(length)){
throw new InsufficientDataException();
}
byte[] ret = new byte[length];
byteBuf.readBytes(ret);
return ret;
}
String readBytes2Str(ByteBuf byteBuf,int length) throws InsufficientDataException {
byte[] bytes = readBytes(byteBuf, length);
return byte2Str(bytes);
}
//解析失败,断开连接
private void handleParseErrorByDisconnect(ByteBuf in,ChannelHandlerContext ctx) {
// Keep discarding until disconnection.
String hex=getByteBufHEXStr(in);
log.error("==============================>解析失败,断开连接,数据为:"+hex);
in.skipBytes(in.readableBytes());
//解析失败直接断开连接
ctx.close();
}
//解析失败,无视报文
private void handleParseErrorByIgnore(ByteBuf in,ChannelHandlerContext ctx) {
// Keep discarding until disconnection.
String hex=getByteBufHEXStr(in);
log.error("==============================>解析失败,忽略报文,数据为:"+hex);
in.skipBytes(in.readableBytes());
}
private String getByteBufHEXStr(ByteBuf byteBuf) {
byte[] bytes = ByteBufUtil.getBytes(byteBuf);
return bytes2HEXString(bytes);
}
//ProtocolSL651DecodeHandler自定义异常,作为半包标识
public static class InsufficientDataException extends Exception{
}
}
================================20240614===============================
代码部分重构,并新加HEX/BCD编码解析
Crc16校验
参考Java实现CRC16算法校验_java 参数为16进制字符串crc16实现-CSDN博客
import java.util.List;
//https://blog.csdn.net/weixin_43652507/article/details/122479304
public class Crc16Utils {
// /**
// * 获取源数据和验证码的组合byte数组
// *
// * @param strings 可变长度的十六进制字符串
// * @return
// */
// public static byte[] getData(List<String>strings) {
// byte[] data = new byte[]{};
// for (int i = 0; i<strings.size();i++) {
// int x = Integer.parseInt(strings.get(i), 16);
// byte n = (byte)x;
// byte[] buffer = new byte[data.length+1];
// byte[] aa = {n};
// System.arraycopy( data,0,buffer,0,data.length);
// System.arraycopy( aa,0,buffer,data.length,aa.length);
// data = buffer;
// }
// return getData(data);
// }
// /**
// * 获取源数据和验证码的组合byte数组
// *
// * @param aa 字节数组
// * @return
// */
// private static byte[] getData(byte[] aa) {
// byte[] bb = getCrc16(aa);
byte[] cc = new byte[aa.length+bb.length];
System.arraycopy(aa,0,cc,0,aa.length);
System.arraycopy(bb,0,cc,aa.length,bb.length);
// return bb;
// }
public static byte[] getCrc16(List<Byte> list) {
byte[] bytes = new byte[list.size()];
for(int i=0;i<bytes.length;i++){
bytes[i]=list.get(i);
}
return getCrc16(bytes);
}
/**
* 获取验证码byte数组,基于Modbus CRC16的校验算法
*/
public static byte[] getCrc16(byte[] arr_buff) {
int len = arr_buff.length;
// 预置 1 个 16 位的寄存器为十六进制FFFF, 称此寄存器为 CRC寄存器。
int crc = 0xFFFF;
int i, j;
for (i = 0; i < len; i++) {
// 把第一个 8 位二进制数据 与 16 位的 CRC寄存器的低 8 位相异或, 把结果放于 CRC寄存器
crc = ((crc & 0xFF00) | (crc & 0x00FF) ^ (arr_buff[i] & 0xFF));
for (j = 0; j < 8; j++) {
// 把 CRC 寄存器的内容右移一位( 朝低位)用 0 填补最高位, 并检查右移后的移出位
if ((crc & 0x0001) > 0) {
// 如果移出位为 1, CRC寄存器与多项式A001进行异或
crc = crc >> 1;
crc = crc ^ 0xA001;
} else
// 如果移出位为 0,再次右移一位
crc = crc >> 1;
}
}
return intToBytes(crc);
}
// /**
// * 将int转换成byte数组,低位在前,高位在后
// * 改变高低位顺序只需调换数组序号
// */
//高位在前
private static byte[] intToBytes(int value) {
byte[] src = new byte[2];
src[0] = (byte) ((value>>8) & 0xFF);
src[1] = (byte) (value & 0xFF);
return src;
}
// /**
// * 将字节数组转换成十六进制字符串
// */
// public static String byteTo16String(byte[] data) {
// StringBuffer buffer = new StringBuffer();
// for (byte b : data) {
// buffer.append(byteTo16String(b));
// }
// return buffer.toString();
// }
// /**
// * 将字节转换成十六进制字符串
// *
// * int转byte对照表
// * [128,255],0,[1,128)
// * [-128,-1],0,[1,128)
// */
// public static String byteTo16String(byte b) {
// StringBuffer buffer = new StringBuffer();
// int aa = (int)b;
// if (aa<0) {
// buffer.append(Integer.toString(aa+256, 16)+" ");
// }else if (aa==0) {
// buffer.append("00 ");
// }else if (aa>0 && aa<=15) {
// buffer.append("0"+Integer.toString(aa, 16)+" ");
// }else if (aa>15) {
// buffer.append(Integer.toString(aa, 16)+" ");
// }
// return buffer.toString();
// }
}
ByteUtil
/**
* @author yaoct
* @date 2024/6/13 11:23
* @description:
*/
public class ByteUtil {
static public String bytes2HexStr(byte[] bytes) {
return bytes2HexStr(bytes,0,bytes.length-1);
}
static public String bytes2HexStr(byte[] bytes, int s, int e) {
StringBuilder sb=new StringBuilder();
for(int i=s;i<=e;i++){
byte b=bytes[i];
String hex = Integer.toHexString(Byte.toUnsignedInt(b));
if(hex.length()==1)hex='0'+hex;
sb.append(hex.toUpperCase());
}
return sb.toString();
}
static public byte[] hexStr2Bytes(String str) {
char[] chars = str.toCharArray();
int len=chars.length/2;
byte[] ret=new byte[len];
for(int i=0;i<len;i++){
char h=chars[i*2];
char l=chars[i*2+1];
int hv=0;
int lv=0;
if(h<='9'&&h>='0'){
hv=(int)(h-'0');
}
if(h<='f'&&h>='a'){
hv=(int)(h-'a')+10;
}
if(h<='F'&&h>='A'){
hv=(int)(h-'A')+10;
}
if(l<='9'&&l>='0'){
lv=(int)(l-'0');
}
if(l<='f'&&l>='a'){
lv=(int)(l-'a')+10;
}
if(l<='F'&&l>='A'){
lv=(int)(l-'A')+10;
}
ret[i]=(byte)(hv*16+lv);
}
return ret;
}
//字节数组转换整型
static public int bytes2Int(byte[] bytes) {
return bytes2Int(bytes, 0, bytes.length-1);
}
//字节数组转换整型
static public int bytes2Int(byte[] bytes, int s, int e) {
int ret=0;
for(int i=s;i<=e;i++){
ret=ret*256+(bytes[i]&0xff);
}
return ret;
}
static public String getByteBufHexStr(ByteBuf byteBuf) {
byte[] bytes = ByteBufUtil.getBytes(byteBuf);
return bytes2HexStr(bytes);
}
static public String bytes2AsciiStr(byte[] bytes){
return bytes2AsciiStr(bytes,0,bytes.length-1);
}
//将每1字节转换位2个16进制码,再转换位2字节Ascii
static public byte[] bytes2HexAsciiByte(byte[] bytes){
int len=bytes.length*2;
byte[] ret=new byte[len];
for(int i=0;i<bytes.length;i++){
byte b=bytes[i];
String s = Integer.toHexString(Byte.toUnsignedInt(b));
if(s.length()==1)s='0'+s;
char ch1=s.charAt(0);
char ch2=s.charAt(1);
ret[2*i]=(byte) ch1;
ret[2*i+1]=(byte) ch2;
}
return ret;
}
static public String bytes2AsciiStr(byte[] bytes, int s, int e){
StringBuilder sb = new StringBuilder();
for(int i=s;i<=e;i++){
sb.append((char) bytes[i]);
}
return sb.toString();
}
static public int strHex2Int(String str){
int ret=0;
str=str.toLowerCase();
for(int i=0;i<str.length();i++){
char c=str.charAt(i);
ret=ret*16;
if('0'<=c&&c<='9'){
ret+=c-'0';
}
if('a'<=c&&c<='f'){
ret+=10+c-'a';
}
}
return ret;
}
static public byte[] readBytes(ByteBuf byteBuf, int length) throws SL651DecodeHandler.InsufficientDataException {
if(!byteBuf.isReadable(length)){
throw new SL651DecodeHandler.InsufficientDataException();
}
byte[] ret = new byte[length];
byteBuf.readBytes(ret);
return ret;
}
static public byte readByte(ByteBuf byteBuf) throws SL651DecodeHandler.InsufficientDataException {
if(!byteBuf.isReadable(1)){
throw new SL651DecodeHandler.InsufficientDataException();
}
return byteBuf.readByte();
}
static public String readBytes2Str(ByteBuf byteBuf,int length) throws SL651DecodeHandler.InsufficientDataException {
byte[] bytes = readBytes(byteBuf, length);
return bytes2AsciiStr(bytes);
}
static public byte[] str2Bytes(String str) {
byte[] ret=new byte[str.length()];
for(int i=0;i<str.length();i++){
char c=str.charAt(i);
ret[i]=(byte) c;
}
return ret;
}
static public List<Byte> bytes2List(byte[] bytes) {
List<Byte> ret = new ArrayList<>();
for(byte b:bytes){
ret.add(b);
}
return ret;
}
public static byte[] extractBytes(byte[] bytes, int s, int e) {
byte[] ret=new byte[e-s+1];
for(int i=s;i<=e;i++){
ret[i-s]=bytes[i];
}
return ret;
}
}
SL651PacketTypeEnum
/**
* @author yaoct
* @date 2024/6/14 13:47
* @description:
*/
public enum SL651PacketTypeEnum {
DSB("定时报","32"),
XSB("小时报","34"),
LLWCB("链路维持报","2f")
;
//类型名称
private String type;
//类型编码
private String code;
SL651PacketTypeEnum(String type, String code) {
this.type = type;
this.code = code;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public static SL651PacketTypeEnum find(String code) {
return Arrays.stream(values()).filter(n -> n.getCode().equals(code) ).findAny().orElse(null);
}
}
SL651DecodeHandler
/**
* @author yaoct
* @date 2024/1/16 10:32
* @description:
* SL651解析,这里较为简单的处理了半包问题:先创建一个ByteBuf副本,解析一遍数据,
* 在ByteBuf副本读取数据不足时抛出InsufficientDataException异常,不做任何处理。
* 如果解析成功就将原ByteBuf指针设置到ByteBuf副本的位置(等价于原ByteBuf解析了数据)。
* 遇到其他解析错误的情况,就清空ByteBuf所有数据。
* 以上处理效率不高,如果有多个半包,可能重复解析起始的byte多次,最差的情况下复杂度为O(n2)。因为半包本身可能在任何字节截断。
* 这里水文协议因为一次请求数据不长所以影响不大。
* 如需效率高的解析可以参考{@link io.netty.handler.codec.http.HttpObjectDecoder}
* HttpObjectDecoder中对每个解析阶段都定义了一个状态,如遇到半包情况,只需从当前状态解析就可以了。但是编程较复杂。
* 注意要定义最大数据长度。因为可能会遇到恶意报文。
*/
@Slf4j
public class SL651DecodeHandler extends ByteToMessageDecoder {
private String pointCode=null;
private StringBuilder logStringBuilder = null;//日志记录
final private int maxLength=2000;//最大包长度定为2000,避免报文发送错误或恶意报文情况
public static void main(String[] args) {
byte[] bytes = new byte[100];
for(int i=0;i<bytes.length;i++)bytes[i]=(byte) i;
ByteBuf byteBuf = Unpooled.copiedBuffer(bytes);
System.out.println(byteBuf.readerIndex());
int i = byteBuf.readInt();
byte[] bytes1 = ByteBufUtil.getBytes(byteBuf, 0, 3);
System.out.println(byteBuf.readerIndex()+":"+byteBuf.readableBytes());
//深拷贝
ByteBuf copy = byteBuf.copy();
System.out.println(copy.readerIndex()+":"+copy.readableBytes());
byte[] bytes2 = ByteBufUtil.getBytes(copy, 0, 3);
//浅拷贝,并拷贝指针
ByteBuf duplicate = byteBuf.duplicate();
int i1 = duplicate.readInt();
duplicate.retain();//引用计数+1,引用计数为0是释放
System.out.println(duplicate.readerIndex()+":"+duplicate.readableBytes());
byte[] bytes3 = ByteBufUtil.getBytes(copy, 0, 3);
//浅拷贝,不拷贝指针
ByteBuf slice = byteBuf.slice();
System.out.println(slice.readerIndex()+":"+slice.readableBytes());
byte[] bytes4 = ByteBufUtil.getBytes(copy, 0, 3);
ByteBuf retain = slice.retain();
int c1 = byteBuf.refCnt();
boolean release = ReferenceCountUtil.release(byteBuf);
int c2 = byteBuf.refCnt();
boolean release1 = ReferenceCountUtil.release(byteBuf);
int c3=duplicate.refCnt();
int c4=slice.refCnt();
int c5 = copy.refCnt();
int v=1;
}
/**
* ByteToMessageDecoder扩充了ChannelInboundHandlerAdapter.channelRead方法,
* ByteBuf没读完的数据在ByteToMessageDecoder.callDecode中会继续读,部分解决了粘包半包问题,数据读取多少,怎么读取,异常处理需要自己解决。
* ByteBuf在ByteToMessageDecoder释放,在这里如果读取错误只需跳过所有ByteBuf字节
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
logStringBuilder=new StringBuilder();
//System.out.println(bytes.length);
//ctx.writeAndFlush(Unpooled.copiedBuffer(new byte[100]));
//if(1==1)throw new RuntimeException(); //报异常需自己跳过ByteBuf,否则ByteBuf会累加可能导致内存泄漏,
// 参考io.netty.handler.codec.http.HttpObjectDecoder.invalidMessage,报异常直接丢弃ByteBuf
try {
int num = in.readableBytes();
if(num>maxLength){
throw new RuntimeException("ByteBuf长度为"+num+",超过最大长度"+maxLength);
}
ByteBuf duplicate = in.duplicate();//浅拷贝,并拷贝指针,为了初步解决半包问题
decodeSL651(ctx, duplicate, out,logStringBuilder);
byte[] parseData = ByteBufUtil.getBytes(in, in.readerIndex(), duplicate.readerIndex() - in.readerIndex());
log.info("{},成功解析报文{}",logStringBuilder.toString(),ByteUtil.bytes2HexStr(parseData));//当前成功解析报文
in.readerIndex(duplicate.readerIndex());//成功读取一次报文后设置新的指针
}catch (InsufficientDataException e){
byte[] bytes = ByteBufUtil.getBytes(in);
String byteHex = ByteUtil.bytes2HexStr(bytes);
log.info("当前报文为半包{}",byteHex);
}catch (Exception e){
//解析错误需清空ByteBuf,否则ByteBuf会累加可能导致内存泄漏,参考io.netty.handler.codec.http.HttpObjectDecoder.invalidMessage,报异常直接丢弃ByteBuf
handleParseErrorByIgnore(in, ctx);
log.error("",e);
return;
}
}
private void decodeSL651(ChannelHandlerContext ctx, ByteBuf in, List<Object> out, StringBuilder logStringBuilder) throws InsufficientDataException {
// byte[] bytes = ByteBufUtil.getBytes(in);
List<Byte> outBuffer=new ArrayList<>();//下行报文
// String hexStr=bytes2HEXString(bytes);
// messageThreadLocal.set(hexStr);
//651协议起始码,01:ascii编码,7E7E:HEX编码
// byte c = in.readByte();
SL651DeviceData deviceData=null;
int pre = in.readerIndex();
byte b1 = ByteUtil.readByte(in);
outBuffer.add(b1);
if(Byte.toUnsignedInt(b1)==0x01){
SL651AsciiParse sL651AsciiParse = new SL651AsciiParse();
deviceData = sL651AsciiParse.parseAscii(ctx, in, out,logStringBuilder,outBuffer);
int cur = in.readerIndex();
sL651AsciiParse.checkCrc16Ascii(ByteBufUtil.getBytes(in, pre, cur-pre));
if(deviceData!=null){//deviceData返回null代表未解析
ackMessage(ctx, outBuffer, deviceData);
}else{
in.skipBytes(in.readableBytes());//跳过,一些数据类型未解析,但格式正确
}
}else if(Byte.toUnsignedInt(b1)==0x7e){
byte b2 = ByteUtil.readByte(in);
if(Byte.toUnsignedInt(b2)==0x7e){
outBuffer.add(b2);
SL651HexParse sl651HexParse = new SL651HexParse();
deviceData = sl651HexParse.parseHex(ctx, in, out,logStringBuilder,outBuffer);
int cur = in.readerIndex();
sl651HexParse.checkCrc16Hex(ByteBufUtil.getBytes(in, pre, cur-pre));
if(deviceData!=null&&deviceData.getType()!=SL651PacketTypeEnum.LLWCB){//deviceData返回null代表未解析
ackMessage(ctx, outBuffer, deviceData);
}else{
in.skipBytes(in.readableBytes());//跳过,一些数据类型未解析,但格式正确
}
}else{
throw new RuntimeException("不支持的编码标识符");
}
}else {
//暂不支持HEX编码
// handleParseErrorByIgnore(in, ctx);
throw new RuntimeException("不支持的编码标识符");
}
if(deviceData!=null){
//TODO 处理deviceData
}
}
private static void ackMessage(ChannelHandlerContext ctx, List<Byte> outBuffer, SL651DeviceData deviceData) {
//发送下行报文
ByteBuf buffer = ctx.alloc().buffer();
byte[] resByte=new byte[outBuffer.size()];
for(int i = 0; i< outBuffer.size(); i++){
resByte[i]= outBuffer.get(i);
}
log.info("站点{}发送{}确认报文:{}", deviceData.getDzbm(), deviceData.getType(),ByteUtil.bytes2HexStr(resByte));
ctx.writeAndFlush(buffer.writeBytes(resByte));
}
//解析失败,断开连接
private void handleParseErrorByDisconnect(ByteBuf in,ChannelHandlerContext ctx) {
// Keep discarding until disconnection.
String hex=ByteUtil.getByteBufHexStr(in);
log.error("==============================>解析失败,断开连接,数据为:"+hex);
in.skipBytes(in.readableBytes());
//解析失败直接断开连接
ctx.close();
}
//解析失败,无视报文
private void handleParseErrorByIgnore(ByteBuf in,ChannelHandlerContext ctx) {
// Keep discarding until disconnection.
String hex=ByteUtil.getByteBufHexStr(in);
log.error("==============================>解析失败,忽略报文,数据为:"+hex);
in.skipBytes(in.readableBytes());
}
//ProtocolSL651DecodeHandler自定义异常,作为半包标识
public static class InsufficientDataException extends Exception{
}
}
SL651AsciiParse:Ascii解析部分
/**
* @author yaoct
* @date 2024/6/13 10:55
* @description:
*/
@Slf4j
public class SL651AsciiParse {
public boolean checkCrc16Ascii(byte[] bytes) {
//验证Ascii数据的有效性最后4字节是验证位,Ascii码转换为2字节HEX
//这里不验证 TODO
// byte[] bytes = ByteBufUtil.getBytes(in);
return true;
}
public SL651DeviceData parseAscii(ChannelHandlerContext ctx, ByteBuf in, List<Object> out, StringBuilder logStringBuilder, List<Byte> outBuffer) throws SL651DecodeHandler.InsufficientDataException {
//中心站地址,不需要skip
byte[] bytes1 = ByteUtil.readBytes(in, 2);
//遥测站地址
//根据协议可以接收多个遥测站地址,这里默认只解析一个 TODO
byte[] bytes = ByteUtil.readBytes(in, 10);
String address=ByteUtil.bytes2AsciiStr(bytes);
outBuffer.addAll(ByteUtil.bytes2List(bytes));
outBuffer.addAll(ByteUtil.bytes2List(bytes1));
//密码,暂不解析skip
bytes = ByteUtil.readBytes(in, 4);
String password = ByteUtil.bytes2AsciiStr(bytes);
if(!checkPassword(address,password)){
log.error("报文密码不正确,设备地址{},密码{}",address,password);
return null;
}
outBuffer.addAll(ByteUtil.bytes2List(bytes));
//功能码
bytes = ByteUtil.readBytes(in, 2);
String gnm = ByteUtil.bytes2AsciiStr(bytes);
gnm=gnm.toLowerCase();
outBuffer.addAll(ByteUtil.bytes2List(bytes));
//上下行标识符
outBuffer.add((byte)'8');//下行报文标识符固定‘8’
List<Byte> outBufferContent = new ArrayList<>();
//报文上行标识符及长度
ByteUtil.readBytes(in,1);//0标识上行,8表示下行,这里都是下行,暂不校验
String lenHex = ByteUtil.readBytes2Str(in, 3);//正文字节长度
int len = ByteUtil.strHex2Int(lenHex);
//报文起始符 02,暂不校验
byte b = in.readByte();
// int len1=Math.min(len,in.readableBytes());
byte[] content = ByteUtil.readBytes(in, len);//正文
SL651DeviceData sL651DeviceData =null;
if(gnm.equals(SL651PacketTypeEnum.DSB.getCode())){//解析定时报
logStringBuilder.append("SL651协议定时报数据接入地址:"+address);
// log.info("SL651协议定时报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
sL651DeviceData =parseDSB(content,outBufferContent);
sL651DeviceData.setType(SL651PacketTypeEnum.DSB);
}else if(gnm.equals(SL651PacketTypeEnum.XSB.getCode())){
// log.info("SL651协议小时报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
logStringBuilder.append("SL651协议小时报数据接入地址:"+address);
sL651DeviceData.setType(SL651PacketTypeEnum.XSB);
sL651DeviceData =parseXSB(content,outBufferContent);
}else if(gnm.equals(SL651PacketTypeEnum.LLWCB.getCode())){
// log.info("SL651协议链路维持报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
logStringBuilder.append("SL651协议链路维持报数据接入地址:"+address);
sL651DeviceData.setType(SL651PacketTypeEnum.LLWCB);
return null;//不解析,可能是链路维持报2F,不需要发送确认报文
}else{
// log.info("SL651协议其他报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
logStringBuilder.append("SL651协议其他报数据接入地址:"+address);//暂不解析,不发送确认报文
return null;//不解析,可能是链路维持报2F,不需要发送确认报文
}
//结束符
bytes = ByteUtil.readBytes(in, 1);
//校验码
String jym = ByteUtil.readBytes2Str(in, 4);//hex转ascii的结果
//相应报文
//3个HEX字符标识正文长度
int size = outBufferContent.size();
String outContentLen = Integer.toHexString(size);
for(;outContentLen.length()<3;){
outContentLen='0'+outContentLen;
}
bytes=ByteUtil.str2Bytes(outContentLen);
outBuffer.addAll(ByteUtil.bytes2List(bytes));
outBuffer.add((byte)0x02);//起始符
outBuffer.addAll(outBufferContent);//正文
outBuffer.add((byte)0x06);//报文结束符,这里是ACK
//发送下行报文
byte[] crc16 = Crc16Utils.getCrc16(outBuffer);
byte[] crc16HexAscii = ByteUtil.bytes2HexAsciiByte(crc16);
for(byte crc:crc16HexAscii){
outBuffer.add(crc);
}
return sL651DeviceData;
}
private String bytes2HEXString(byte[] bytes) {
StringBuilder sb=new StringBuilder();
for(byte b:bytes){
String s = Integer.toHexString(Byte.toUnsignedInt(b));
if(s.length()==1)s='0'+s;
sb.append(s.toUpperCase());
}
return sb.toString();
}
//验证密码,暂不验证
private boolean checkPassword(String address, String password) {
return true;
}
//定时报
private SL651DeviceData parseDSB(byte[] bytes, List<Byte> outBuffer) {
SL651DeviceData sL651DeviceData = new SL651DeviceData();
Content content = new Content(bytes);
//流水号
String lsh = content.readStr(4);
byte[] bytes2 = ByteUtil.str2Bytes(lsh);
outBuffer.addAll(ByteUtil.bytes2List(bytes2));//流水号相应正文
String xysj = TimeUtil.getThisTime("yyMMddHHmmss");//相应发报时间
// xysj="190531134354";
bytes2 = ByteUtil.str2Bytes(xysj);
outBuffer.addAll(ByteUtil.bytes2List(bytes2));
sL651DeviceData.setLsh(lsh);
//发报时间yyMMddHHmmss
String fbsj = content.readStr(12);
sL651DeviceData.setFbsj(fbsj);
//地址标识符ST
String dzbsf = content.nextBySpace();
//地址编号
String dzbm = content.nextBySpace();
sL651DeviceData.setDzbm(dzbm);
//遥测站分类码
String yczflm = content.nextBySpace();
sL651DeviceData.setYczflm(yczflm);
//观测时间分类码
String gcsjflm = content.nextBySpace();
String gcsj = content.nextBySpace();//yyMMddHHmm 或 yyMMddHHmmss 遥测站报文时间格式不一致
sL651DeviceData.setGcsj(gcsj);
//要素信息组,最后是电压
while(content.isReadable()){
String label = content.nextBySpace();//监测项标识符,
if(label==null){//解析结束
break;
}
String val = content.nextBySpace();//监测值,注意值不存在的情况可能是FFFFFFFF
try {
sL651DeviceData.getDataMap().put(label,Double.valueOf(val));
}catch (Exception e){
log.error("监测值解析失败,地址{},监测项{},数值{}",dzbm,label,val);
}
if(label.equals("VT")){//电压固定为最后一项
break;
}
}
return sL651DeviceData;
}
//小时报,暂不解析,为了发送确认报文,这里只解析确认报文需要的信息
private SL651DeviceData parseXSB(byte[] bytes, List<Byte> outBuffer) {
SL651DeviceData SL651DeviceData = new SL651DeviceData();
Content content = new Content(bytes);
//流水号
String lsh = content.readStr(4);
byte[] bytes2 = ByteUtil.str2Bytes(lsh);
outBuffer.addAll(ByteUtil.bytes2List(bytes2));//流水号相应正文
String xysj = TimeUtil.getThisTime("yyMMddHHmmss");//相应发报时间
bytes2 = ByteUtil.str2Bytes(xysj);
outBuffer.addAll(ByteUtil.bytes2List(bytes2));
return null;
}
private class Content{
int startIdx;
int endIdx;
int idx;//当前下标
byte[] content;
Content(byte[] content){
this(content,0,content.length-1);
}
Content(byte[] content,int startIdx,int endIdx){
this.content=content;
this.idx=startIdx;
this.startIdx=startIdx;
this.endIdx=Math.min(endIdx,content.length);
}
boolean isReadable() {
return idx <= endIdx;
}
String readStr(int nextLength){
String ret = ByteUtil.bytes2AsciiStr(content,idx,idx+nextLength-1);
idx=idx+nextLength;
return ret;
}
//空格分隔(暂时写死)
String nextBySpace(){
idx=nextSpaceIdxSkip();
if(idx==endIdx+1){//后面全是空格
return null;
}
int nextIdx=nextSpaceIdx();
String str = ByteUtil.bytes2AsciiStr(content, idx, nextIdx - 1);
idx=nextIdx;
return str;
}
//后面第一个 空格 的下标
int nextSpaceIdx(){
for(int i=idx;i<=endIdx;i++){
byte b=content[i];
if(b==0x20){
return i;
}
}
return endIdx+1;
}
//可处理多个空格情况,非必须
int nextSpaceIdxSkip(){
for(int i=idx;i<=endIdx;i++){
byte b=content[i];
if(Byte.toUnsignedInt(b)!=0x20){
return i;
}
}
return endIdx+1;
}
}
}
SL651HexParse解析部分
/**
* @author yaoct
* @date 2024/6/13 10:55
* @description:
*/
@Slf4j
public class SL651HexParse {
final public static String INVALID_VAL ="AAAA";//设备无数据
static public boolean checkCrc16Hex(byte[] bytes) {
//验证Hex数据的有效性最后4字节是验证位,Hex码转换为2字节HEX
//这里不验证 TODO
// byte[] bytes = ByteBufUtil.getBytes(in);
return true;
}
public SL651DeviceData parseHex(ChannelHandlerContext ctx, ByteBuf in, List<Object> out, StringBuilder logStringBuilder, List<Byte> outBuffer) throws SL651DecodeHandler.InsufficientDataException {
//中心站地址,不需要skip
byte[] bytes1 = ByteUtil.readBytes(in, 1);
//遥测站地址
//根据协议可以接收多个遥测站地址,这里默认只解析一个 TODO
byte[] bytes = ByteUtil.readBytes(in, 5);
String address=ByteUtil.bytes2HexStr(bytes);
outBuffer.addAll(ByteUtil.bytes2List(bytes));
outBuffer.addAll(ByteUtil.bytes2List(bytes1));
//密码,暂不解析skip
bytes = ByteUtil.readBytes(in, 2);
String password = ByteUtil.bytes2HexStr(bytes);
if(!checkPassword(address,password)){
log.error("报文密码不正确,设备地址{},密码{}",address,password);
throw new RuntimeException("报文密码不正确,设备地址"+address+",密码"+password);
}
outBuffer.addAll(ByteUtil.bytes2List(bytes));
//功能码
bytes = ByteUtil.readBytes(in, 1);
String gnm = ByteUtil.bytes2HexStr(bytes);
gnm=gnm.toLowerCase();
outBuffer.addAll(ByteUtil.bytes2List(bytes));
// //上下行标识符
// outBuffer.add((byte)(0b10000000));//下行高4位1000
List<Byte> outBufferContent = new ArrayList<>();
//报文上行标识符及长度
bytes = ByteUtil.readBytes(in, 2);//报文上下行标识及长度
byte bwbs=(byte) (bytes[0]>>4);//高四位是上下行报文标识(0000 表示上行,1000 表示下行)
if(bwbs!=0){
//这里都认为是上行,暂不校验
}
bytes[0]&=0b00001111;
int len = ByteUtil.bytes2Int(bytes);//正文字节长度
//报文起始符 02,暂不校验
byte b = ByteUtil.readByte(in);
// int len1=Math.min(len,in.readableBytes());
byte[] content = ByteUtil.readBytes(in, len);//正文
SL651DeviceData sL651DeviceData =null;
if(gnm.equals(SL651PacketTypeEnum.DSB.getCode())){//解析定时报
logStringBuilder.append("SL651协议定时报数据接入地址:"+address);
// log.info("SL651协议定时报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
sL651DeviceData = parseDSB(content,outBufferContent);
sL651DeviceData.setType(SL651PacketTypeEnum.DSB);
}else if(gnm.equals(SL651PacketTypeEnum.XSB.getCode())){
// log.info("SL651协议小时报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
logStringBuilder.append("SL651协议小时报数据接入地址:"+address);
sL651DeviceData = parseXSB(content,outBufferContent);
sL651DeviceData.setType(SL651PacketTypeEnum.XSB);
}else if(gnm.equals(SL651PacketTypeEnum.LLWCB.getCode())){
// log.info("SL651协议链路维持报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
logStringBuilder.append("SL651协议链路维持报数据接入地址:"+address);
sL651DeviceData.setType(SL651PacketTypeEnum.LLWCB);
return null;//暂不解析,链路维持报2F,不需要发送确认报文
}else{
// log.info("SL651协议其他报数据接入地址:{},数据:{}",address,messageThreadLocal.get());
logStringBuilder.append("SL651协议其他报数据接入地址:"+address);//暂不解析,不发送确认报文
return null;
}
//结束符,暂不校验
bytes = ByteUtil.readBytes(in, 1);
//校验码,暂不校验
bytes = ByteUtil.readBytes(in, 2);
//相应报文
//2字节报文上下行标识及长度
int size = outBufferContent.size();
bytes=new byte[2];
bytes[0]=(byte)(size/256);
bytes[1]=(byte)(size%256);
bytes[0]|=0b10000000;
bytes[0]&=0b10001111;//高4位用作上下行标识(0000 表示上行,1000 表示下行);
outBuffer.add(bytes[0]);
outBuffer.add(bytes[1]);
outBuffer.add((byte)0x02);//起始符
outBuffer.addAll(outBufferContent);//正文
outBuffer.add((byte)0x06);//报文结束符,这里是ACK
//发送下行报文
byte[] crc16 = Crc16Utils.getCrc16(outBuffer);
for(byte crc:crc16){
outBuffer.add(crc);
}
return sL651DeviceData;
}
private String bytes2HEXString(byte[] bytes) {
StringBuilder sb=new StringBuilder();
for(byte b:bytes){
String s = Integer.toHexString(Byte.toUnsignedInt(b));
if(s.length()==1)s='0'+s;
sb.append(s.toUpperCase());
}
return sb.toString();
}
//验证密码,暂不验证
private boolean checkPassword(String address, String password) {
return true;
}
//定时报
private SL651DeviceData parseDSB(byte[] bytesContent, List<Byte> outBuffer) {
SL651DeviceData sL651DeviceData = new SL651DeviceData();
Content content = new Content(bytesContent);
//流水号
byte[] lsh = content.readBytes(2);
// byte[] bytes2 = ByteUtil.str2Bytes(lsh);
outBuffer.addAll(ByteUtil.bytes2List(lsh));//流水号相应正文
String xysj = TimeUtil.getThisTime("yyMMddHHmmss");//相应发报时间
// xysj="200724172349";
byte[] bytes = ByteUtil.hexStr2Bytes(xysj);
outBuffer.addAll(ByteUtil.bytes2List(bytes));
sL651DeviceData.setLsh(ByteUtil.bytes2List(lsh)+"");
//发报时间yyMMddHHmmss
bytes = content.readBytes(6);
sL651DeviceData.setFbsj(ByteUtil.bytes2HexStr(bytes));
//地址标识符ST
bytes = content.readBytes(2);//F1F1地址标识符
//地址编号
bytes = content.readBytes(5);
String address=ByteUtil.bytes2HexStr(bytes);
sL651DeviceData.setDzbm(address);
//遥测站分类码
bytes = content.readBytes(1);
String yczflm = ByteUtil.bytes2HexStr(bytes);
sL651DeviceData.setYczflm(yczflm);
//观测时间分类码
bytes = content.readBytes(2);
String gcsjflm = ByteUtil.bytes2HexStr(bytes);
//遥测站报文时间yyMMddHHmm
bytes = content.readBytes(5);
String gcsj=ByteUtil.bytes2HexStr(bytes);
sL651DeviceData.setGcsj(gcsj);
//要素信息组,最后是电压
while(content.isReadable()){
//监测项标识符,Hex字符串格式表示,自定义会有2个字节
bytes = content.readBytes(1);
String label = Integer.toHexString(Byte.toUnsignedInt(bytes[0]));
if(Byte.toUnsignedInt(bytes[0])==0xff){
bytes = content.readBytes(1);
label+=Integer.toHexString(Byte.toUnsignedInt(bytes[0]));
}
bytes = content.readBytes(1);//数据长度解析
int xsd=bytes[0]&(byte)0b00000111;//小数点
int sjcd=bytes[0]>>3;//高5位是数据长度(字节数)
bytes = content.readBytes(sjcd);//监测值BCD编码,数据长度除以2,注意值不存在的情况可能是FFFFFFFF
String hexVal=ByteUtil.bytes2HexStr(bytes);
try {
if(hexVal.startsWith(INVALID_VAL)){
log.info("监测值无效,地址{},监测项{},数值{}",address,label,hexVal);
}else{
int flag=1;//符号
if(hexVal.startsWith("FF")){//负数,根据实际情况调整
flag=-1;
hexVal=hexVal.substring(2);
}
Double val=Double.valueOf(hexVal)*flag;
val=val/Math.pow(10,xsd);
sL651DeviceData.getDataMap().put(label,val);
}
}catch (Exception e){
log.error("监测值解析失败,地址{},监测项{},数值{}",address,label,hexVal);
}
if(label.equals("38")){//电压固定为最后一项
break;
}
}
return sL651DeviceData;
}
//小时报,暂不解析,为了发送确认报文,这里只解析确认报文需要的信息
private SL651DeviceData parseXSB(byte[] bytes, List<Byte> outBuffer) {
SL651DeviceData SL651DeviceData = new SL651DeviceData();
Content content = new Content(bytes);
//流水号
byte[] lsh = content.readBytes(2);
outBuffer.addAll(ByteUtil.bytes2List(lsh));//流水号相应正文
String xysj = TimeUtil.getThisTime("yyMMddHHmmss");//相应发报时间
byte[] bytes2 = ByteUtil.hexStr2Bytes(xysj);
outBuffer.addAll(ByteUtil.bytes2List(bytes2));
SL651DeviceData.setLsh(ByteUtil.bytes2List(lsh)+"");
//发报时间yyMMddHHmmss
bytes = content.readBytes(6);
SL651DeviceData.setFbsj(ByteUtil.bytes2HexStr(bytes));
//地址标识符ST
bytes = content.readBytes(2);//F1F1地址标识符
//地址编号
bytes = content.readBytes(5);
String address=ByteUtil.bytes2HexStr(bytes);
SL651DeviceData.setDzbm(address);
return SL651DeviceData;
}
private class Content{
int startIdx;
int endIdx;
int idx;//当前下标
byte[] content;
Content(byte[] content){
this(content,0,content.length-1);
}
Content(byte[] content,int startIdx,int endIdx){
this.content=content;
this.idx=startIdx;
this.startIdx=startIdx;
this.endIdx=Math.min(endIdx,content.length);
}
boolean isReadable() {
return idx <= endIdx;
}
byte[] readBytes(int nextLength){
byte[] ret = ByteUtil.extractBytes(content,idx,idx+nextLength-1);
idx=idx+nextLength;
return ret;
}
}
}
SL651DeviceData:数据解析实体类
/**
* @author yaoct
* @date 2024/6/13 11:17
* @description:
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SL651DeviceData {
//流水号
String lsh;
//发报时间
String fbsj;
//地址编号
String dzbm;
//遥测站分类码
String yczflm;
//观测时间
String gcsj;
//观测时间
SL651PacketTypeEnum type;
//监测值(要素标识符,数据)
HashMap<String,Double> dataMap=new HashMap();
}
测试数据
ascii编码测试数据:
发送报文(HEX字符串):013031303035353636373738384130303033323030363702303030323139303532323137303433375354203030353536363737383820482054542031393035323231373030204149204D2056542031322E30302052533438352D322C20C6F8CEC22C4D2CA1E6204449302C31204449312C31204449322C31204449332C31200342363130
响应报文(HEX字符串,时间为190531134354):013030353536363737383830314130303033323830313002303030323139303533313133343335340637356566
HEX/BCD编码测试数据:
发送报文(HEX字符串):7E7E010010100001A00032002B02000C200724172349F1F1001010000148F0F020072417232019000130221900004039230000055038121268038397
响应报文(HEX字符串,时间为200724172349):7E7E001010000101A00032800802000C20072417234906DB17
模拟发送端:
/**
* @author yaoct
* @date 2024/1/11 16:33
* @description:
*/
public class TestClient {
public static void main(String[] args) {
//
// byte[] ffs = strHex2Byte("11");
// if(1==1)return;
//1.客户端定义一个循环事件组
EventLoopGroup group = new NioEventLoopGroup();
try {
//2.创建客户端启动对象
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) //设置客户端通道实现类
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyClientHandle());
}
});
System.err.println("client is ready......");
//3.启动客户端去连接服务端
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 19001).sync();
//4.设置通道关闭监听(当监听到通道关闭时,关闭client)
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
group.shutdownGracefully();
}
}
static class NettyClientHandle extends ChannelInboundHandlerAdapter {
//如果client 端服务启动完成后
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//HEX编码
//7E7E010000009644123432003A02004E210911103501F1F1000000964448F0F0210911103522190000001F190000001A190000022019000002261901021445200000048038121170038217
//ASCII编码
//0130313830353037333335363841303030333230303434023230313932343031313531313030303053542038303530373333353638204820545420323430313135313130303030204844535720372E3938382056542031342E3739200335393939
// ctx.writeAndFlush(Unpooled.copiedBuffer("hello,netty server...", CharsetUtil.UTF_8));
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("7E7E010000009644123432003A02004E210911103501F1F1000000964448F0F0210911103522190000001F190000001A190000022019000002261901021445200000048038121170038217")));
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("013031383035303733333536324130303033323030343502303135393234303230313130323530305354203830353037333335363220482054542032343032303131303235303020594C20343032302E3630342056542032332E3838200332313231")));
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("013031383035303733333536324130303033323030343502303135393234303230313130323530305354203830353037333335363220482054542032343032303131303235303020594C20343032302E3630342056542032332E3838200332313231013031383035303733333536314130303033323030343502303136353234303230313130323530305354203830353037333335363120482054542032343032303131303235303020594C20333835362E3538302056542032342E3236200339463344")));//粘包
/******粘包+半包********/
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("013031383035303733333536324130303033323030343502303135393234303230313130323530305354203830353037333335363220482054542032343032303131303235303020594C20343032302E363034205654203233")));//粘包+半包
// Thread.sleep(10000);
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("2E3838200332313231013031383035303733333536314130303033323030343502303136353234303230313130323530305354203830353037333335363120482054542032343032303131303235303020594C20333835362E3538302056542032342E3236200339463344")));//粘包+半包
/*********************/
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("013031303035353636373738384130303033323030363702303030323139303532323137303433375354203030353536363737383820482054542031393035323231373030204149204D2056542031322E30302052533438352D322C20C6F8CEC22C4D2CA1E6204449302C31204449312C31204449322C31204449332C31200342363130")));
ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("7E7E010010100001A00032002B02000C200724172349F1F1001010000148F0F020072417232019000130221900004039230000055038121268038397")));
// String str="";
// for(int i=0;i<20;i++)str+="013031383035303733333536324130303033323030343502303135393234303230313130323530305354203830353037333335363220482054542032343032303131303235303020594C20343032302E3630342056542032332E383820033231";
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte(str)));
}
//当通道有读事件时
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
// System.err.println("服务器端回复消息:"+byteBuf.toString(CharsetUtil.UTF_8));
// System.err.println("服务器端地址是:"+ctx.channel().remoteAddress());
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("7E7E010000009644123432003A02004E210911103501F1F1000000964448F0F0210911103522190000001F190000001A190000022019000002261901021445200000048038121170038217")));
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("7EAA7EBB")));
// Thread.sleep(3000);
// ctx.writeAndFlush(Unpooled.copiedBuffer(new byte[100000000]));
byte[] bytes = ByteBufUtil.getBytes(byteBuf);
StringBuilder sb=new StringBuilder();
for(byte b:bytes){
String s = Integer.toHexString(Byte.toUnsignedInt(b));
if(s.length()==1)s='0'+s;
sb.append(s.toUpperCase());
}
System.out.println(sb.toString());
// ctx.writeAndFlush(Unpooled.copiedBuffer(strHex2Byte("3231")));
Thread.sleep(1000);
}
//当通道有异常时
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
static byte[] strHex2Byte(String hex){
char[] chars = hex.toCharArray();
int len=chars.length/2;
byte[] ret=new byte[len];
for(int i=0;i<len;i++){
char h=chars[i*2];
char l=chars[i*2+1];
int hv=0;
int lv=0;
if(h<='9'&&h>='0'){
hv=(int)(h-'0');
}
if(h<='f'&&h>='a'){
hv=(int)(h-'a')+10;
}
if(h<='F'&&h>='A'){
hv=(int)(h-'A')+10;
}
if(l<='9'&&l>='0'){
lv=(int)(l-'0');
}
if(l<='f'&&l>='a'){
lv=(int)(l-'a')+10;
}
if(l<='F'&&l>='A'){
lv=(int)(l-'A')+10;
}
ret[i]=(byte)(hv*16+lv);
}
return ret;
}
}
参考:【Netty4】netty ByteBuf(三)如何释放ByteBuf-CSDN博客