如何优雅的利用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.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为例了,有兴趣的朋友可以一起学习交流!
qklbishe.com区块链毕设代做网专注|以太坊fabric-计算机|java|毕业设计|代做平台 » 如何优雅的利用C#做协议解析