SL651-2014中心站协议解析简单实现

SL651协议数据通过在互联网中发送时可看成是一种应用层协议。本文用框架netty解析了该协议的中心站端的一部分,协议其余部分解析类似。

首先需要了解在SL651协议中的中心站、遥测站、遥测终端设备分别代表是什么。

 参考https://wenku.baidu.com/view/8c524e964793daef5ef7ba0d4a7302768f996f0c.html?_wkts_=1707112839329&bdQuery=%E9%81%A5%E6%B5%8B%E7%AB%99

在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博客

https://www.cnblogs.com/dafanjoy/p/15611042.html

SL651-2014全协议解析(HEX编码格式)-CSDN博客

  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值