您的位置:首页 > 编程语言 > Java开发

Java处理协议传输编解码

2017-12-12 00:00 567 查看
在通信过程过程中,交互的基础单位是“字节”,即发送和接收的最小单位是“字节”。

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

在这个过程中,可能涉及到字节数组转十六进制字符串,十六进制字符串转字节数组,字节转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
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息