Java处理协议传输编解码
2017-12-12 00:00
567 查看
在通信过程过程中,交互的基础单位是“字节”,即发送和接收的最小单位是“字节”。
而在多数通信协议中,我们定义的源码很可能是无符号的十六进制字符串。随后转为字节数组进行传输。
在这个过程中,可能涉及到字节数组转十六进制字符串,十六进制字符串转字节数组,字节转8位、16位、32位整数,字符串转字节数组、字节数组转字符串等等转换操作。
Java中所有的整数类型(byte、short、int、long)都以有符号数的形式存储(即用补码表示二进制数),其二进制形式的第一个二进制位为符号位:0表示正数,1表示负数。
补码:
正数的补码为其本身
负数补码为其绝对值各位取反加1
例如:
+21,其二进制表示形式是00010101,则其补码同样为00010101。
-21,按照概念其绝对值为00010101,各位取反为11101010,再加1为11101011,即-21的二进制表示形式为11101011。
而同样是11101011的无符号数为235
注意,==在协议传输中我们一般使用无符号数,而Java并未提供无符号类型==。
Big-endian(大端序):数据的高位字节存放在地址的低端,低位字节存放在地址高端。
Little-endian(小端序):数据的高位字节存放在地址的高端,低位字节存放在地址低端。
不同的机器中,数据的存储方式可能会有差异,这就导致了在C系列的语言中,如果在不同的机器间进行通信,就需要对不同的“序”进行额外的处理。
所幸Java运行在JVM上,而JVM屏蔽了机器底层的端序差异,无需考虑跨平台的问题。在Java中,永远都是大端序。如果使用Java编写通信程序,就==无需考虑端序的问题==。
加减乘除等基于值的==四则运算==,==必须要保证值不丢失才能正确计算==。
==直接基于二进制位的位运算则不受影响==
比如:
在Java中处理无符号数:
位运算:无所谓有符号无符号,只要不对数字进行更改,直接进行运算即可。
四则运算:使用“高一级”的
类型替代。比如用short接收无符号的byte,用int
接收无符号short等等。
示例代码:
无进位累加和:
Java整数的包装类都提供了解析对应进制有符号数(即范围仍然是有符号数的范围)的方法,即
在多数通信协议中,我们都使用无符号数。
这就导致了直接使用Java包装类的valueOf方法不能很好地解析传输的内容。比如Byte.valueOf("ff",16),就会报
所以这里推荐使用Netty提供的转化方法。
注意:
Netty提供的
对于
对于无符号的byte和short,这样并没有问题,只要不超出量程(byte:0~255,short:0~65535),即可正确写入
但是对于无符号的int,Java的int类型无法正确表达无符号数,我们需要特殊处理:
如果有需要使用超出量程的writeLong,需要类似处理
按照有符号数读取
按照无符号数读取
注意:
无符号数版本返回的类型要“高”一级
无符号数版本没有读取无符号Long的方法,因为Java没有对应的基本类型
虽然类型返回的类型不同(无符号数返回的类型要“高”一级),但是按照有符号数读取和按照无符号数读取,两种方式读取的字节数是一样的
二进制编码的十进制数,简称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码可以良好表达,而对于奇数位数的数字,比如
小技巧:数字的8421 BCD码和数字转16进制后逐字节拼接结果相同,可用于快速实现BCD码的编码
代码示例:
示例代码:
一般可以通过
一般我们可能需要定义一个枚举类,但是写大串的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循环|
示例枚举:
示例使用:
注意:
不超出范围(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& 3ff0 ;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,
writeShort和
writeInt,这三个方法都接收
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 }
相关文章推荐
- jsch使用之Java实现ssh sftp协议传输
- Mina Codec Filter对应协议实现编解码处理
- 基于JAVA中Jersey处理Http协议中的Multipart的详解
- Java 网络编程(二) 两类传输协议:TCP UDP
- Java处理http协议相关初步(三)——线程池的使用分析
- 用spring搭建微信公众号开发者模式下服务器处理用户消息的加密传输构架(java)
- Java中的编码解码处理
- 如何处理java相关网络协议占内存的问题啊?为何时间越长,内存占用越大?
- java中TCP传输协议
- java网络编程之传输协议
- Android与Java 服务器使用Socket协议实现Json数据传输
- Java 网络编程(二) 两类传输协议:TCP UDP
- java base64 编码 解码, HTTP传送解决+号 \n\r 问题,查询处理
- 使用FFmpeg解码私有传输协议标准H264流(1)
- mina的编码和解码以及断包的处理,发送自己定义协议,仿qq聊天,发送xml或json
- Java网络编程之传输控制协议
- java 网络编程 [网络传输] [协议] [UDP与TCP] [套接字] [URL与URI]
- java实现UDP协议传输DatagramSocket
- JAVA基础再回首(二十八)——网络编程概述、IP地址、端口号、TCP和UDP协议、Socket、UDP传输、多线程UDP聊天
- java在线支付---09,10,11,12_在线支付_分析易宝支付网关的应答协议与处理代码,完成用于处理支付响应的Servlet的初步编写和调试,完成处理支付网关响应结果的Servlet,支付实现