如何优雅的利用C#做协议解析
最近喜欢上了做协议解析,使用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.ToUInt32(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.ToUInt16(bytes, 39);
model.FLAC = BitConverter.ToUInt16(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.ToUInt16(bytes, 56) : model.FCELLID;
model.FMCC = BitConverter.ToUInt16(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为例了,有兴趣的朋友可以一起学习交流!