Java处理协议传输编解码

在通信过程过程中,交互的基础单位是“字节”,即发送和接收的最小单位是“字节”。

而在多数通信协议中,我们定义的源码很可能是无符号的十六进制字符串。随后转为字节数组进行传输。

在这个过程中,可能涉及到字节数组转十六进制字符串,十六进制字符串转字节数组,字节转8位、16位、32位整数,字符串转字节数组、字节数组转字符串等等转换操作。

1、Java处理协议传输常见问题

1.1、有符号数和无符号数,大端序和小端序

1.1.1、有符号数和无符号数

有符号数和无符号数的差别在于:有符号数最高位为符号位。同样位数的类型无符号的byte取值范围是0~255,而有符号位的取值范围是 -128~127。

Java中所有的整数类型(byte、short、int、long)都以有符号数的形式存储(即用补码表示二进制数),其二进制形式的第一个二进制位为符号位:0表示正数,1表示负数。

  • 补码:
    • 正数的补码为其本身
    • 负数补码为其绝对值各位取反加1
    • 例如:
      • +21,其二进制表示形式是00010101,则其补码同样为00010101。
      • -21,按照概念其绝对值为00010101,各位取反为11101010,再加1为11101011,即-21的二进制表示形式为11101011。
      • 而同样是11101011的无符号数为235

注意,==在协议传输中我们一般使用无符号数,而Java并未提供无符号类型==。

1.1.2、大端序和小端序

如果数据都是单字节的,其存储顺序就没有所谓,但是对于多字节数据,比如int,double等,就要考虑存储的顺序了。

  • Big-endian(大端序):数据的高位字节存放在地址的低端,低位字节存放在地址高端。
  • Little-endian(小端序):数据的高位字节存放在地址的高端,低位字节存放在地址低端。 不同的机器中,数据的存储方式可能会有差异,这就导致了在C系列的语言中,如果在不同的机器间进行通信,就需要对不同的“序”进行额外的处理。

所幸Java运行在JVM上,而JVM屏蔽了机器底层的端序差异,无需考虑跨平台的问题。在Java中,永远都是大端序。如果使用Java编写通信程序,就==无需考虑端序的问题==。

1.2、数字运算

1.2.1、有无符号数带来的问题

  • 由于在Java中数字以有符号数存储,而在协议传输过程中,数字以无符号数存储。这就导致:
    • 加减乘除等基于值的==四则运算==,==必须要保证值不丢失才能正确计算==。
    • ==直接基于二进制位的位运算则不受影响== 比如:
    public static void main(String[] args) {
        ByteBuf byteBuf = Unpooled.buffer();
        byteBuf.writeByte(255);
        byteBuf.writeByte(133);
        byteBuf.writeByte(255);
        byteBuf.writeByte(133);
        // 读取两个一个字节的有符号数
        byte b1 = byteBuf.readByte();
        byte b2 = byteBuf.readByte();
        // 读取两个一个字节的无符号数
        short i1 = byteBuf.readUnsignedByte();
        short i2 = byteBuf.readUnsignedByte();
        // 虽然打印的结果不同,其底层的二进制数相同
        System.out.println(b1);
        System.out.println(i1);
        // 如果进行位运算
        // 虽然打印的结果不同,其底层的二进制数相同
        System.out.println(b1&b2);
        System.out.println(i1&i2);
        // 但是如果进行四则运算
        // 两种结果完全不同
        System.out.println(b1+b2);
        System.out.println(i1+i2);
    }
    
    
  • 在Java中处理无符号数:
    • 位运算:无所谓有符号无符号,只要不对数字进行更改,直接进行运算即可。
    • 四则运算:使用“高一级”的 类型替代。比如用short接收无符号的byte,用int 接收无符号short等等。

1.2.2、常见校验码计算方法

  • 异或校验:从消息头开始,同后一字节异或,直到校验码前一个字节
    • 示例代码:
        /**
     * 对字节数组计算异或检验码,并在某位添加,占一个字节
     * @param bytes
     */
    private static void addCheckByte(List<Byte> bytes) {
        if (bytes.size()>=2){
            //计算校验码
            Iterator<Byte> iterator = bytes.iterator();
            byte checkByte = iterator.next();
            while (iterator.hasNext()){
                checkByte ^= iterator.next();
            }
            //添加到结尾
            bytes.add(checkByte);
        }
    }
    
  • 无进位累加和:

2、数字编码解码方法

2.1、Java原生的转化方法

  • Java本身提供了一些转化方法,但是不太直观并且不方便使用,比如:
    • Java整数的包装类都提供了解析对应进制有符号数(即范围仍然是有符号数的范围)的方法,即valueOf(String value,int radix,其中第二个参数表示进制。但是注意,valueOf方法中,负数不再使用补码,而直接使用负号。例如:

      public static void main(String[] args) {
          String positiveBinaryValue = Integer.toBinaryString(255);
          //正数,正常输出255
          System.out.println(Integer.valueOf(positiveBinaryValue,2));
          String negativeBinaryValue = Integer.toBinaryString(-255);
          //负数,会报错,不能识别补码
          //System.out.println(Integer.valueOf(negativeBinaryValue));
          //添加负号实现负数,输出-255
          System.out.println(Integer.valueOf("-"+positiveBinaryValue,2));
      }
      

      在多数通信协议中,我们都使用无符号数。

      这就导致了直接使用Java包装类的valueOf方法不能很好地解析传输的内容。比如Byte.valueOf("ff",16),就会报NumberFormatException: Value out of range异常。

  • 所以这里推荐使用Netty提供的转化方法。

2.2、Netty提供的转化方法

2.2.1、编码

  • Netty的ByteBuf类提供了针对不同数据类型的一系列编码方法
    • writeByte(int num),向流中写入一个字节
    • writeShort(int num),向流中写入两个字节
    • writeMedium(int num),向流中写入三个字节
    • writeInt(int num),向流中写入四个字节
    • writeLong(long num),向流中写入八个字节
    • 注意:
      • Netty提供的writeXX方法,其原理都是截短固定长度的二进制位,比如:
      ByteBuf byteBuf = Unpooled.buffer();
      byteBuf.writeByte(256);
      // 结果是0,因为256的二进制编码为1 0000 0000
      System.out.println(byteBuf.readByte());
      
      • 对于writeByte,writeShortwriteInt,这三个方法都接收int类型参数
        • 对于无符号的byte和short,这样并没有问题,只要不超出量程(byte:0~255,short:0~65535),即可正确写入
        • 但是对于无符号的int,Java的int类型无法正确表达无符号数,我们需要特殊处理:
        /**
         * 按大端序写无符号整型
         * 大端序:先传递高 24 位,然后传递高 16 位,再传递高八位,最后传递低八位。
         * @param num
         * @return
         */
        public static byte[] writeUnsignedInt(long num){
            byte[] bytes = new byte[4];
            for (int i = 3; i >= 0; i--) {
                bytes[3-i] = (byte)(num >> (8 * i));
            }
            return bytes;
        }
        
        • 如果有需要使用超出量程的writeLong,需要类似处理

2.2.1、解码

  • Netty的ByteBuf类提供了针对不同数据类型的两个系列的解码方法
    • get系列
      • get系列大体和read相同,唯一不同之处在于其只读取readIndex所在的位置的数据,而不移动readIndex指针
    • read系列
      • 按照有符号数读取
        // 读取一个字节
        byte b = byteBuf.readByte();
        // 读取两个个字节
        short s = byteBuf.readShort();
        // 读取三个字节
        int i1 = byteBuf.readMedium();
        // 读取四个字节
        int i2 = byteBuf.readInt();
        // 读取八个字节
        long l = byteBuf.readLong();
        
      • 按照无符号数读取
        // 读取一个字节
        short s = byteBuf.readUnsignedByte();
        // 读取两个个字节
        int i1 = byteBuf.readUnsignedShort();
        // 读取三个字节
        int i2 = byteBuf.readUnsignedMedium();
        // 读取四个字节
        long l1 = byteBuf.readUnsignedInt();
        
      • 注意:
        • 无符号数版本返回的类型要“高”一级
        • 无符号数版本没有读取无符号Long的方法,因为Java没有对应的基本类型
        • 虽然类型返回的类型不同(无符号数返回的类型要“高”一级),但是按照有符号数读取和按照无符号数读取,两种方式读取的字节数是一样的

3、特殊格式编码方法

3.1、8421 BCD码

  • 在通信协议中,我们有时候还会用一种特殊的编码:8421 BCD码。
    • 二进制编码的十进制数,简称BCD码(Binarycoded Decimal)。
    • 这种方法是用4位二进制码的组合代表十进制数的0,1,2,3,4,5,6 ,7,8,9 十个数符。4位二进制数码有16种组合,原则上可任选其中的10种作为代码,分别代表十进制中的0,1,2,3,4,5,6,7,8,9 这十个数符。
    • 最常用的BCD码称为8421BCD码,8.4.2.1 分别是4位二进数的位取值。
    • 注意:
      • 8421 BCD码转化的数字字符串要比原数字字符串占据的长度短一半,是一种良好的压缩方式
      • 对应数值本身不用于计算,基本类型难以存储的数字字符串(比如手机号码),协议中常用BCD码
      • 对于偶数位数的数字,BCD码可以良好表达,而对于奇数位数的数字,比如12345,则需要一些处理,其和123456一般使用三个字节表达,差别在于最后一个字节只有后四个位有意义。
      • 小技巧:数字的8421 BCD码和数字转16进制后逐字节拼接结果相同,可用于快速实现BCD码的编码
    • 代码示例:
    
    /**
     * 解析BCD码,获得解码后的字符串
     * @param BCDSize BCD数组的size
     */
    public static String getBCDString(ByteBuf msg,int BCDSize) {
        StringBuilder startTimeBuilder = new StringBuilder();
        for (int i = 0; i < BCDSize; i++) {
            startTimeBuilder.append(FromByteUtil.decodeBCDNum(msg.readByte(), false));
        }
        return startTimeBuilder.toString();
    }
    
    
    /**
     * 解析BCD码
     * @param b
     * @param isHalf 标注是否只有后四个位有数字,前四个位填充0
     * @return
     */
    public static String decodeBCDNum(byte b,boolean isHalf){
        int i1 = b&0xff;
        String num = String.format("%02X",i1);
        if (isHalf){
            num = num.substring(0, 1);
        }
        return num;
    }
    
    /**
     * 将BCD码转为字节数组
     * @param size 字节数组的大小,而不是字符串长度
     * @param BCDCode
     * @return byte[]
     */
    public static byte[] BCDStringToBytes(String BCDCode,int size) {
        if (BCDCode == null || BCDCode.equals("")) {
            return new byte[size];
        }
        int BCDSize = BCDCode.length() / 2;
        char[] hexChars = BCDCode.toCharArray();
        byte[] d = new byte[size];
        int n = Math.min(size, BCDSize);
        for (int i = 0; i < n; i++) {
            int pos = i * 2;
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
        }
        //如果不是偶数,说明最后还有一个单独的数字
        if ((BCDCode.length() & 0x01)==1){
            d[BCDSize] = (byte) (charToByte(hexChars[BCDSize * 2]) << 4 | 0);
            BCDSize ++;
        }
        //不足位数,在最后补零
        for (int i = 0; i < size - BCDSize; i++) {
            d[BCDSize+i] = 0x00;
        }
        return d;
    }
    
    /**
     * Convert char to byte
     * @param c char
     * @return byte
     */
    private static byte charToByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }
    
    

3.2、一般字符串

  • 一般字符串的编码、解码都可通过Java本身的API实现,注意前后encoding相同即可
    • 示例代码:
    /**
     * 将字符串转为指定长度的数组,不足位数,在前面补0
     * @param string
     * @param byteSize
     * @return
     */
    public static byte[] StringToByte(String string,int byteSize) throws UnsupportedEncodingException {
        //如果传入的字符串为null,直接返回指定长度的byte数据,全部填充0x00
        if (string == null){
            return new byte[byteSize];
        }
        byte[] strBytes = string.getBytes("GBK");
        int strLength = strBytes.length;
        if (strLength <byteSize){
            byte[] bytes = new byte[byteSize];
            //先补0
            int diff = byteSize - strLength;
            for (int i = 0; i < diff; i++) {
                bytes[i] = 0;
            }
            //再写字符串
            for (int i = 0; i < strLength; i++) {
                bytes[diff+i] = strBytes[i];
            }
            return bytes;
        }else if (strLength>byteSize){
            //截短过长的字符串
            return Arrays.copyOf(strBytes, byteSize);
        }else{
            return strBytes;
        }
    }
    
    
    /**
     * 从ByteBuf中读取指定字节长度的字符串
     * 并且输出时去除头尾的空格符
     * @param msg
     * @param byteSize
     * @param charset
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String getString(ByteBuf msg,int byteSize,String charset) throws UnsupportedEncodingException {
        if (msg.readableBytes()<byteSize){
            throw new IllegalArgumentException("数据长度不够!");
        }
        byte[] bytes = new byte[byteSize];
        msg.readBytes(bytes);
        String s = new String(bytes, charset);
        return s.trim();
    }
    
    

3.3、按位取值

  • 有时候我们需要判断某一个或多个字节某一个位是否为1
    • 一般可以通过&运算来进行判断,比如要判断第一个位是否为1,可以:
    int targetNum = 55;
    Integer ref = Integer.valueOf("0001", 2);
    boolean b = (targetNum & ref) == ref;
    
    • 一般我们可能需要定义一个枚举类,但是写大串的01过于冗长,并且容易出错,可以利用二进制数和十六进制数的小技巧实现: 二进制数 | 十六进制数 ---|--- 0000 0001 | 0x01 0000 0010 | 0x02 0000 0100 | 0x04 0000 1000 | 0x08 0001 0000 | 0x10 0010 0000 | 0x20 0100 0000 | 0x40 1000 0000 | 0x80 ... 依此类推更高位,1248循环|
    • 示例枚举:
    /**
     * Created by 蔡志杰 on 2017/8/7.
     * 文本标识
     */
    public enum TextFlag {
        /**
         * 紧急
         */
        EMERGENCY(0x01),
        /**
         * 终端显示器显示
         */
        LCD(0x04),
        /**
         * 终端TTS播读
         */
        TTS(0x08),
        /**
         * 广告屏显示
         */
        ADVERTISE_LCD(0x10),
        /**
         * 1:CAN故障码信息或,0:中心导航信息
         */
        CAN_FAILURE_INFO(0x20);
    
    
    
        private int code;
        TextFlag(int code){
            this.code = code;
        }
        /**
         * 根据码获取命令标识对象
         * @param code
         * @return
         */
        public static TextFlag getTextFlag(int code){
            for (TextFlag textFlag :
                    TextFlag.values()) {
                if (textFlag.code == code) {
                    return textFlag;
                }
            }
            throw new IllegalArgumentException();
        }
        public long getCode(){
            return this.code;
        }
    }
    
    
    • 示例使用:
    // 判断第1、3位是否均为1
    int num = 55;
    long ref = TextFlag.EMERGENCY.getCode() | TextFlag.TTS.getCode();
    boolean b = (55 & ref) == ref;
    

5、注意事项

5.1、Java中的数字比较问题

Java中的整数以有符号数的形式存储,并且不提供无符号类型。但是大多数的通信协议中,使用的都是无符号数。这就导致在解析的时候,我们需要注意有符号数、无符号数的转换。

注意:

  • 不超出范围(Java中整数范围)的有符号正整数、无符号正整数并没有任何区别。
    • 比如有符号的byte = 126,和无符号的byte = 126,其二进制码一模一样。
  • 但是需要注意,如果使用包装类的进行比较:
    • 包装类型和基本类型进行比较没有问题
    • 基本类型和基本类型进行比较同样没有问题
    • 但是需要注意包装类型和包装类型之间的比较
    public static void main(String[] args) {
        Integer i1 = 128;
        Integer i2 = 128;
        int i3 = 128;
        System.out.println(i1==i2); // 返回false
        System.out.println(i1==i3); // 返回true
        // 当数字大小在-128~127之间时,包装类对象存在同一个区域,返回true
        Integer i4 = 127;
        Integer i5 = 127;
        int i6 = 127;
        System.out.println(i4==i5); // 返回true
        System.out.println(i4==i6); // 返回true
    }
    

转载于:https://my.oschina.net/pierrecai/blog/1588560

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值