最近喜欢上了做协议解析,最近使用Java与.NET做了很多的厂家的产品的协议网关。从部标系列到第三方私有协议,通过协议解析过程中了解每款产品的特色与将来可能的应用场景。
下面我以一款第三方私有协议为例,利用C#语言进行优雅的做协议解析。
原始数据:2475201509260111002313101503464722331560113555309F00000000002D0500CB206800F064109326381A03
序号 |
名称 |
值(HEX) |
长度(Byte) |
说明 |
||||||||||||||||||||||||||||||||||
1 |
协议头 |
24 |
1 |
固定为0x24,即ASCII的”$”符. |
||||||||||||||||||||||||||||||||||
2 |
终端ID号 |
7520150926 |
5 |
终端的ID号,固定为5字节长度. 75表示JT701. |
||||||||||||||||||||||||||||||||||
3 |
协议版本号 |
01 |
1 |
01:表示协议版本号 |
||||||||||||||||||||||||||||||||||
4 |
终端类型号 |
1 |
0.5 |
1:常规可充电JT701. |
||||||||||||||||||||||||||||||||||
5 |
数据类型号 |
1 |
0.5 |
1表明常规二进制定位数据,2表示报警数据,3表示盲区常规二进制定位数据 |
||||||||||||||||||||||||||||||||||
6 |
数据长度 |
0023 |
2 |
16数据内容长度,表明后面的数据一共有35个字节长. 17数据内容长度,表明后面的数据一共有39个字节长. |
||||||||||||||||||||||||||||||||||
7 |
日期 |
131015 |
3 |
日月年表示.此处为2015年10月13号. |
||||||||||||||||||||||||||||||||||
8 |
时间 |
034647 |
3 |
时分秒表示,为国际标准时.此处表示为03:46:47. |
||||||||||||||||||||||||||||||||||
9 |
纬度 |
22331560 |
4 |
22331560,按照DDMM.MMMM格式定义,此纬度值为: 2233. 1560. |
||||||||||||||||||||||||||||||||||
10 |
经度 |
113555309 |
4.5 |
113555309,按照DDDMM.MMMM格式定义,此经度值为: 11355. 5309. |
||||||||||||||||||||||||||||||||||
11 |
位指示 |
F |
0.5 |
F = 1111,GPS定位,西经,北纬. E = 1110,非GPS定位,西经,北纬. 最右边的位为BIT0,最左边的位为BIT3. 1: BIT3为固定值. 1: BIT2表示东经,如果为0表示西经. 1: BIT1 表示北纬,如果为0表示南纬. 1: BIT0 表示定位,如果为0表示GPS不定位. |
||||||||||||||||||||||||||||||||||
12 |
速度 |
00 |
1 |
当前速度为5公里/小时. |
||||||||||||||||||||||||||||||||||
13 |
方向 |
00 |
1 |
0x98 = 152,乘以2为304,即方向在304度. |
||||||||||||||||||||||||||||||||||
14 |
里程 |
0000002D |
4 |
当前里程数为45公里.以16进制表示. |
||||||||||||||||||||||||||||||||||
15 |
GPS卫星个数 |
05 |
1 |
GPS卫星个数,若为基站定位,则GPS卫星个数为00. |
||||||||||||||||||||||||||||||||||
16 |
绑定车辆ID |
00CB2068 |
4 |
当前中心绑定的车辆ID号,以十六进制表示. |
||||||||||||||||||||||||||||||||||
17 |
终端状态 |
00F0 |
2 |
终端的各种状态及报警情况,最右边为低字节(Byte1),最左边为高字节(Byte2),详细定义如下:
|
||||||||||||||||||||||||||||||||||
18 |
电量指示 |
64 |
1 |
电量指示,为当前采集到的电量值,十六进制位表示.0x64表示剩余电量100%,显示精度为5%,若为0xFF,则表示正在USB充电中. |
||||||||||||||||||||||||||||||||||
19 |
CELL ID位置 代码 |
10932638 |
4 |
1093为CELL ID号, 2638为位置代码,即LAC. |
||||||||||||||||||||||||||||||||||
20 |
GSM信号质量 |
1A |
1 |
表明当前GSM的信号强弱,1A表明为0x1A,即信号值为26. GSM信号强度最大为31. |
||||||||||||||||||||||||||||||||||
21 |
区域报警ID |
05 |
1 |
目前区域进出报警,扩展到最多10个区域,即标识区域报警时,同时显示当前进出的区域ID,1.7版本及以后使用 |
||||||||||||||||||||||||||||||||||
22 |
设备状态3 |
01 |
1 |
具体标识含义见 4.4设备状态3说明 |
||||||||||||||||||||||||||||||||||
23 |
预留 |
0F0F |
2 |
预留。 |
||||||||||||||||||||||||||||||||||
24 |
IMEI号 |
863977039060871F |
8 |
IMEI号,前面15位是BCD码,后面补一个F。通用版本(全是0F无效)。 |
||||||||||||||||||||||||||||||||||
25 |
预留 |
|
2 |
预留 | ||||||||||||||||||||||||||||||||||
26 |
MCC |
|
2 |
国家代码,中国460 |
||||||||||||||||||||||||||||||||||
27 |
MNC |
|
1 |
运营商代码移动00 |
||||||||||||||||||||||||||||||||||
28 |
流水号 |
03 |
1 |
数据流水号,每发送一条数据,则累加1,从0x00~0xFF,终端重启流水号会清零. |
其实像这种协议大多都具有通用性,神似,只是针对每家的产品粘包问题处理方式是个需要思考的问题,不过只要有固定的包头包尾,这些还是相对比较好做的。比如808协议的7E,这份协议里面的24。
很多刚刚接触做协议解析通常喜欢用字符串截取的方式,就是把收到的数据转成16进制,然后再根据16进制截取按协议进行解析,这种方法不但性能差,代码看起来也会很杂乱,下面我就用二进制流对上面的协议进行解析。其实Netty框架里面ByteBuf也是一样的原理。多说一句,.NET也有Netty,我没怎么深入研究过,不知道是否好用,如果有时间可以去研究一下。
首先我们明确一点,无论是使用传统Socket还是netty,接收到的数据都是二进制流的方式,因为协议文档无法描述二进制流,所以会将二进制流转成16进制的方式进行描述,但是这就容易给人造成误导,认为我们接收到的数据也需要进行16进制转换。其实用二进制流做协议解析回更简单。
下面把我用C#写的一个小例子分享出来:
public static LocationProto LocationParser(byte[] bytes)
{
//定义定位数据实体类
LocationProto model = new LocationProto();
try
{
//跳过包头,然后解析设备ID
model.FAssetID = CommonClass.ByteToHexStr(bytes.Skip(1).Take(5).ToArray());
//得到数据长度
int length = BitConverter.ToInt16(bytes, 8);
//获取时间段,转成我们识别的"yyyy-MM-dd HH:mm:ss"格式
model.FGPSTime = CommonClass.GetDataTime(bytes.Skip(10).Take(6).ToArray());
//这里是数据接收时间,是我自己定义网关接收到数据的时间,因为我系统用的是格林威治时间
model.FRecvTime = DateTime.UtcNow;
//解析定位信息,经度,纬度,定位状态,做了一个方法的封装
PositioningStatus positionStatus = JT701Common.GetPositioningStatus(bytes.Skip(16).Take(9).ToArray());
model.FLatitude = positionStatus.FLatitude;//纬度
model.FLongitude = positionStatus.FLongitude;//经度
model.FLocationType = positionStatus.FLocationType;//定位状态
//解析速度
model.FSpeed = bytes[25];
//解析方向
model.FDirection = bytes[26] * 2;
//解析里程
model.FMileage = BitConverter.ToInt32(bytes, 27);//里程
//解析GSM信号值
model.FCellSignal = bytes[31];
//解析设备状态
AssetStatus assetStatus = JT701Common.GetAssetStatus(bytes.Skip(36).Take(2).ToArray(), model.FLocationType);
//解析是否基站定位(GPS定位>基站定位>不定位)
model.FLocationType = assetStatus.FLocationType;
//解析报警类型
model.FAlarmType = assetStatus.FAlarmType;
//是否需要回复终端
model.FNeedReplay = assetStatus.FNeedReplay;
//解析锁绳状态
model.FLockRope = assetStatus.FLockRope;
//解析锁状态
model.FLockStatus = assetStatus.FLockStatus;
//解析后盖状态
model.FCoverStatus = assetStatus.FCoverStatus;
//获取电量(255为充电中)
model.FBattery = bytes[38];
//解析小区码信息
model.FCELLID = BitConverter.ToInt16(bytes, 39);
model.FLAC = BitConverter.ToInt16(bytes, 41);
//解析GPS卫星个数
model.FGPSSignal = bytes[43];
//解析区域ID
model.FAreaId = bytes[44];
//得到唤醒源
model.FWakeSource = bytes[45] & 0x07;
//是否GSM信号弱报警
model.FGSMAlarm = bytes[45] & 0x40;
//得到IMEI号
model.FIMEI = CommonClass.ByteToHexStr(bytes.Skip(48).Take(8).ToArray());
model.FCELLID = model.FCELLID == 0 ? BitConverter.ToInt16(bytes, 56) : model.FCELLID;
model.FMCC = BitConverter.ToInt16(bytes, 58);
model.FMNC = bytes[60];
}
catch (Exception ex)
{
Log.Instance.Error("LocationParser:" + ex.Message);
}
return model;
}
里面用到的一些主要方法:
/// <summary>
/// 字节数组转16进制字符串:空格分隔
/// </summary>
/// <param name="byteDatas"></param>
/// <returns></returns>
public static string ByteToHexStr(byte[] byteDatas)
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < byteDatas.Length; i++)
{
builder.Append(string.Format("{0:X2}", byteDatas[i]));
}
return builder.ToString().Trim();
}
/// <summary>
/// 时间格式转换
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static DateTime GetDataTime(byte[] bytes)
{
return DateTime.ParseExact(ByteToHexStr(bytes), "ddMMyyHHmmss", System.Globalization.CultureInfo.CurrentCulture);
}
/// <summary>
/// 获取定位状态
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static PositioningStatus GetPositioningStatus(byte[] bytes)
{
try
{
PositioningStatus model = new PositioningStatus();
model.FLatitude = CommonClass.GetLatLong60(bytes.Skip(0).Take(4).ToArray());
model.FLongitude = CommonClass.GetLatLong60(bytes.Skip(4).Take(5).ToArray());
model.FLocationType = bytes[8] & 0x01;
int latStatus = bytes[8] & 0x02;
if (latStatus == 0)
{
model.FLatitude = -model.FLatitude;
}
int lonStatus = bytes[8] & 0x04;
if (lonStatus == 0)
{
model.FLongitude = -model.FLongitude;
}
return model;
}
catch (Exception ex)
{
Log.Instance.Error("GetPositioningStatus:"+ex.Message);
return null;
}
}
/// <summary>
/// 获取设备状态
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static AssetStatus GetAssetStatus(byte[] bytes,int fLocationType)
{
try
{
AssetStatus model = new AssetStatus();
//低8位
if (fLocationType == 0)
{
model.FLocationType = bytes[0] & 0x01;
} else
{
model.FLocationType = fLocationType;
}
int infenceAlarm= bytes[0] & 0x02;
if (infenceAlarm == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_9;
}
int outfenceAlarm = bytes[0] & 0x04;
if (outfenceAlarm == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_10;
}
int cutoffAlarm = bytes[0] & 0x08;
if (cutoffAlarm == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_1;
}
int shockAlarm = bytes[0] & 0x10;
if (shockAlarm == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_2;
}
else
{
model.FAlarmType = -1;
}
model.FNeedReplay = bytes[0] & 0x20;
model.FLockRope = bytes[0] & 0x40;
model.FLockStatus = bytes[0] & 0x80;
//高8位
int longTime = bytes[1] & 0x01;
if (longTime == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_3;
}
int fiveError = bytes[1] & 0x02;
if (fiveError == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_4;
}
int swipeCardAlarm = bytes[1] & 0x04;
if (swipeCardAlarm == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_5;
}
int lowPower = bytes[1] & 0x08;
if (lowPower == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_6;
}
int unCover = bytes[1] & 0x10;
if (unCover == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_7;
}
model.FCoverStatus = bytes[1] & 0x20;
int stuckAlarm = bytes[1] & 0x40;
if (stuckAlarm == 1)
{
model.FAlarmType = (int)AlarmTypeEnum.LOCK_ALARM_8;
}
return model;
}
catch (Exception ex)
{
Log.Instance.Error("GetAssetStatus:" + ex.Message);
return null;
}
}
/// <summary>
/// 经纬度计算
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static double GetLatLong60(byte[] bytes)
{
try
{
string locStr = ByteToHexStr(bytes);
if (locStr.Length < 9)
{
locStr = locStr.PadLeft(9, '0');
}
else
{
locStr = locStr.Substring(0, 9);
}
var head = Convert.ToDouble(locStr.Remove(3));
var bodyStr = locStr.Substring(3, locStr.Length - 3);
var body = Convert.ToDouble(bodyStr) / 10000;
head += body / 60;
return head;
}
catch (Exception ex)
{
// txtHelper.WriteException(ex, "locStr:" + locStr, false);
return 0;
}
}
808的解析也是类似,由于很多同行靠808的源码养家糊口,这里就不以808为例了,有兴趣的朋友可以一起学习交流!