一、 自定义协议概览
下面是由终端发送到服务器端的一个自定义报文,我们由这个自定义报文来解释和阐述整个全通信链条过程中的编解码;
发送方 终端数据采集中心
接收方 中心监控服务器
数据域 | 数据类型/长度 | 例(十六进制) | 例说明 | 字节数 |
---|---|---|---|---|
数据包类型 | uint16 | 0X0A,0X00 | 0x0A=10,表示此包为签到请求(小端模式:0x000A) | 2 |
数据包总长度 | uint16 | 0X31,0X00 | 0x0A=10,表示此包为签到请求(小端模式:0x000A) | 2 |
MAC | byte16 | 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | 表示整个报文的加密 | 16 |
数据包解析结果 | uint8 | 0X00 | 00表示解析成功 | 1 |
终端id | string | 0x01 0x0C 0x32 0x30 0x31 0x36 0x30 0x39 0x30 0x39 0x30 0x30 0x30 0x31 | 0x01表示字符编码方式;将使用设置的编码方式将字符串编码到 byte 序列采集器的序列号201609090001 | 14 |
业务流水号 | uint32 | 0x05 0xD2 0x2D 0x01 | 交易流水号16进制的12DD205=19780101(小端模式) 对数据包进行标识 | 4 |
时间戳 | timestamp | 0x66 0xe4 0x33 0x58 | 时间,把日期转化为Unix时间,然后用四个字节表示 | 4 |
保留1 | uint8 | 0X0A,0X00 | 0x0A=10,表示此包为签到请求(小端模式:0x000A) | 2 |
二、 自定义协议的数据定义
整个全链条包含了以下自定义的数据类型:如下阐述
string 1 "1字节编码方式(ASCII、Unicode、UTF8等) + 1字节无符号字符串字节数 +变长字节数" 设备序列号、各类说明描述
unit8 2 1字节无符号整数 表示各种状态标记,如签到请求中的应答(允许签到、拒绝签到)、数据解析结果(解析成功、MAC校验失败、数据包格式错误、时间戳错误)
uint16 3 2字节无符号整数 软件版本号、设备型号
uint32 4 4字节无符号整数 业务流水号
float2 5 "以2字节表示的一个有符号小数,以整数形式存储和传输,数据被放大100倍取值范围在-99.99~99.99之间" "0x18FA解析如下:0x18FA以十进制表示即为 6349,此值已经被放大100倍,因此 0x18FA 表示的数值为 63.49"
float4 6 "以4字节表示的一个有符号小数,以整数形式存储和传输,数据被放大 1000倍取值范围在 -999999.999~+999999.999 之间" "0x356FD499解析如下:0x356FD499以十进制表示为 896521369,此值已经被放大1000倍,因此 0x356FD499表示的数值为 896521.369"
byte16 7 16字节的固定长度数据 随机数、工作密钥密文
byte32 8 32字节固定长度数据 随机数、工作密钥密文
timestamp 9 14字节固定长度的ASCII字符串,格式为yyyyMMddHHmmss 服务器返回给终端的服务器时间、终端上报给服务器的故障发生时间
三、 自定义数据类型封装
以float4为例进行Java编程的封装
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class BodyFieldFloat4 extends BodyField {
//定义最小值
public static final float MIN_VALUE = -999999.999F;
//定义最大值
public static final float MAX_VALUE = 999999.999F;
public static final EDataType FieldDataType = EDataType.Float4;
//解析出来的十进制值
private int value;
public int getValue(){
return value;
}
public void setValue(int value){
if(value < MIN_VALUE || value > MAX_VALUE)
throw new IllegalArgumentException("The argument value is " + value + ", while the value of float4 must between " + MIN_VALUE + " and " + MAX_VALUE);
this.value = value;
super.empty = false;
}
@Override
public Object getFieldValue(){
return this.getValue();
}
//从输入流中读取值
@Override
public void readFieldValue(InputStream is) throws IOException,
DataParseException {
byte[] buff = new byte[4];
int size = is.read(buff);
if(size != buff.length)
throw new DataParseException("Read float4 data failed, InputStream returns -1");
int value = 0;
for(int idx=0; idx<=buff.length - 1; idx++){
value = (value << 8) | (buff[idx] & 0xff);
}
this.value = value;
}
//写出到输出流中
@Override
public void writeFieldValue(OutputStream os) throws IOException,
DataWriteException {
byte[] buff = new byte[]{
(byte)((this.value >>> 24) & 0xFF),
(byte)((this.value >>> 16) & 0xFF),
(byte)((this.value >>> 8) & 0xFF),
(byte)((this.value >>> 0) & 0xFF)
};
os.write(buff);
}
static {
BodyField.registerSubclass(FieldDataType.getValue(), BodyFieldFloat4.class);
}
public BodyFieldFloat4(int fieldCode){
super(fieldCode);
}
}
四、 报文的封装
把设置的一些参数封装为字节数组
/**
*
* @Description:把设置的一些参数转化为byte数组形式
* @return byte[]
*/
public byte[] toByteArray(IMacCalculator mc,byte[] key,EMacAlgorith algorith) throws DataWriteException {
byte[] result = null;
//字节流
ByteArrayOutputStream os = null;
try{
os = new ByteArrayOutputStream();
// 终端id
EaimUtil.writeString(os, this.collectorSn, EEncodingType.ASCII);
// 业务流水号
EaimUtil.writeUint16(os, this.tradingSn);
// 时间戳
EaimUtil.writeTimeStamp(os, this.timestamp);
// 数据包体(主要包含数据域类型和组域)
this.writeBodyData(os);
byte[] data = os.toByteArray();
DisposeUtil.safeClose(os);
if (this.dataType == DataLoginReq.DATA_TYPE || this.dataType == DataLoginResp.DATA_TYPE) {
this.mac = new byte[16];
}else {
switch (algorith) {
case BaseDES:
this.mac = mc.get2ByteMac(data,key);
break;
case MD5Only:
this.mac = mc.get2ByteMac(data);
break;
default:
this.mac = new byte[16];
break;
}
}
os = new ByteArrayOutputStream(data.length + 9);
// 数据包类型 2个byte
EaimUtil.writeUint16(os, this.dataType);
// 数据包总长度2个byte
EaimUtil.writeUint16(os, data.length + 9);
//获取mac字节
byte[] result2 = EaimUtil.getSublingsByte(this.mac, 2, 6, 10, 14);
// MAC16个byte
EaimUtil.writeByte4(os, result2);
// 数据包解析结果1个byte
EaimUtil.writeUint8(os, this.dataParseResult);
// 其它数据(参与计算MAC的数据);
os.write(data);
result = os.toByteArray();
}catch(IOException ex){
logger.error(ex.getMessage(), ex);
} finally{
关闭流
}
return result;
}
五、 报文的解析
报文从字节流中解析为可读数据
//解析结果
public void parseData(byte[] fullData, IMacCalculator mc,byte[] key,EMacAlgorith algorith) throws DataParseException{
ByteArrayInputStream is = null;
try{
is = new ByteArrayInputStream(fullData);
// 数据包类型
int tmpDataType = EaimUtil.readUint16(is);
if(tmpDataType != this.dataType)
throw new DataParseException("Data type not same, this.dataType is " + this.dataType + ", and data type in bytes array is " + tmpDataType);
// 数据包总长度
this.totalSize = (int)EaimUtil.readUint16(is);
System.out.println(totalSize);
if(this.totalSize != fullData.length)
throw new DataParseException("Data length error.");
// 远程MAC
this.mac = EaimUtil.readByte4(is);
// 数据包解析结果
this.dataParseResult = EaimUtil.readUint8(is);
if(EDataParseResult.valueOf(this.dataParseResult) == null)
throw new DataParseException("The value of data parse result is unvalid.");
// 终端id
this.collectorSn = EaimUtil.readString(is);
// 计算并验证MAC
byte[] mac1 = new byte[16];//计算出字节的MAC
//TODO解析mac
byte[] result = EaimUtil.getSublingsByte(mac1, 2, 6, 10, 14);
if(EaimUtil.equals(this.mac, result) == false){
this.dataParseResult = EDataParseResult.MacCheckFailed.getValue();
return;
}
// 业务流水号
this.tradingSn = EaimUtil.readUint16(is);
// 时间戳
this.timestamp = EaimUtil.readTimeStamp(is);
// 数据包体
this.readBodyData(is);
}catch(IOException ex){
logger.error(ex.getMessage(), ex);
throw new DataParseException(ex);
}finally{
//关闭流
}
}
总结,以上为基本自定义报文的固定部分进行封装和解析。
同理如果有自定义协议构成的自定义封装类型如何进行解析和封装呢?
如何进行自定义协议的扩展呢?
在下一篇博客中进行阐述。