去年接触的一个项目中, 需要通过 TCP 与设备进行对接的, 传的是 Modbus 协议的数据, 然后后台需要可以动态配置协议解析的方式, 即寄存器的解析方式,, 配置信息有: Key, 数据 Index, 源数据类型, 数据库列类型, 数据排列方式
一开始使用的方式是, 从数据库读取出协议的配置, 然后在接收到数据的时候, 循环每个配置项根据配置 ----- 解析数据 ------ 转换类型 ---- 存临时列表,, 后来改进了一下, 配置项存缓存,, 数据库修改的时候, 同时更新缓存..
但还是慢了一点, 因为需一个配置大概是 20-30 个数据项, 每一条数据都要 for 循环 20-30 次 , 再加上还有 N 个根据配置的数据类型去做转换的 if 判断, 这么一套下来, 也很耗时间, 但待解析的数据量大的情况下,, 相对也很耗资源...
最后的觉得方案是: 利用 T4 生成 C# 的 class 源码 + 运行时编译成类, 数据直接扔 class 里直接解析出结果, 不需要循环, 也不需要 if 判断, 因为在 t4 生成源码的时候, 已经根据配置处理完了, 因此节省了很多的时间.
不过由于 T4 模板的 IDE 支持的很不好, 不过好在运行时 T4 模板在 IDE 内生成出来的类是 partial 的, 因此, 可以把大部分的代码, 放在外部的 C# 文件里. 先来看数据项的配置信息:
- public class DataItem
- {
- /// <summary>
- /// 数据项 ID
- /// </summary>
- public ObjectId DataItemID { set; get; }
- /// <summary>
- /// 偏移量
- /// </summary>
- public int Pos { set; get; }
- /// <summary>
- /// 大小
- /// </summary>
- public int Size { set; get; }
- public int BitIndex { set; get; }
- /// <summary>
- /// 数据项数据库储存类型
- /// </summary>
- public DbDataTypeEnum DbType { set; get; }
- /// <summary>
- /// 数据项协议源字节数组中的数据类型
- /// </summary>
- public DataTypeEnum SourceType { set; get; }
- /// <summary>
- /// 计算因子
- /// </summary>
- public decimal Factor { set; get; }
- public string Key { set; get; }
- }
- /// <summary>
- /// 对应的数据库字段类型
- /// </summary>
- public enum DbDataTypeEnum
- {
- Int32 = 0,
- Int64 = 1,
- Double = 2,
- DateTime = 3,
- Decimal = 4,
- Boolean = 5
- }
- public enum DataTypeEnum
- {
- Int = 0,
- Short = 1,
- Datetime = 3,
- Long = 5,
- Decimal = 6,
- UInt = 7,
- Byte = 8,
- Boolean = 9,
- Bit = 10,
- UShort = 11,
- UByte = 12
- }
- View Code
这里为什么要区分源数据和数据库数据类型呢? 主要是因为设备一般是 int,short,double,float 等类型, 但是, 对应到数据库, 有时候比如说使用 MongoDB, 之类的数据库, 不一定有完全匹配的, 因此需要区分两种数据项,
再来就是 T4 的模板 ProtocolExecuteTemplate.tt:
- <#@ template language="C#" #>
- <#@ assembly name="System.Core" #>
- <#@ assembly name="Kugar.Core.NetCore" #>
- <#@ assembly name="Kugar.Device.Service.BLL" #>
- <#@ assembly name="Kugar.Device.Service.Data" #>
- <#@ assembly name="MongoDB.Bson" #>
- <#@ import namespace="System.Linq" #>
- <#@ import namespace="System.Text" #>
- <#@ import namespace="System.Collections.Generic" #>
- <#@ import namespace="Kugar.Core.BaseStruct" #>
- <#@ import namespace="MongoDB.Bson" #>
- using System;
- using System.Text;
- using Kugar.Core.BaseStruct;
- using Kugar.Core.ExtMethod;
- using Kugar.Core.Log;
- using Kugar.Device.Service.Data.DTO;
- using Kugar.Device.Service.Data.Enums;
- using MongoDB.Bson;
- namespace Kugar.Device.Service.BLL
- {
- <#
- var className="ProtocolExecutor_" + Protocol.Version.Replace('.','_') + "_" + this.GetNextClasID();
- #>
- public class <#=className #>:IProtocolExecutor
- {
- private string _version="";
- private ObjectId _protocolID;
- private readonly DateTime _baseDt=TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1));
- public <#=className #> (ObjectId protocolID, string version)
- {
- _version=version;
- _protocolID=protocolID;
- }
- public ObjectId ProtocolID {get{return _protocolID;}}
- public BsonDocument Execute(byte[] data, int startIndex)
- {
- BsonDocument bson=new BsonDocument();
- <#
- foreach(var item in Protocol.Items){ #>
- bson["<#=item.Key #>"]= <#= DecodeConfig(item,0) #>;
- <#
- }
- #>
- return bson;
- }
- }
- }
- View Code
在直接在循环里输出解析后的语句, 并且生成的类名记得后面加多一个随机数....
然后再加一个 ProtocolExecuteTemplate.Part.cs 的部分类, 补全 T4 模板的功能, 因为在 T4 里 IDE 支持的不好,, 写代码确实难受,, 没直接写 C# 舒服:
- public partial class ProtocolExecuteTemplate
- {
- private static int _classID = 0;
- public ProtocolExecuteTemplate(DTO_ProtocolDataItem protocol)
- {
- Protocol = protocol;
- }
- public DTO_ProtocolDataItem Protocol { set; get; }
- public string DecodeConfig(DTO_ProtocolDataItem.DataItem item,int startIndex)
- {
- var str = "";
- switch (item.SourceType)
- {
- case DataTypeEnum.Int:
- str = $"BitConverter.ToInt32(data,startIndex + {startIndex + item.Pos})";
- break;
- case DataTypeEnum.UInt:
- str = $"BitConverter.ToUInt32(data,startIndex + {startIndex + item.Pos})";
- break;
- case DataTypeEnum.Short:
- str = $"BitConverter.ToInt16(data,startIndex + {startIndex + item.Pos})";
- break;
- case DataTypeEnum.Long:
- str= $"BitConverter.ToInt64(data,startIndex + {startIndex + item.Pos})";
- break;
- case DataTypeEnum.Byte:
- case DataTypeEnum.UByte:
- case DataTypeEnum.Bit:
- str = $"data[startIndex + {startIndex + item.Pos}]";
- break;
- case DataTypeEnum.UShort:
- str = $"BitConverter.ToUInt16(data,startIndex+{startIndex + item.Pos})";
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- if (item.SourceType==DataTypeEnum.Bit)
- {
- return byteToBit(str, item.BitIndex);
- }
- else
- {
- return valueTODBType(str, item.Factor, item.DbType);
- }
- }
- private string valueTODBType(string sourceValue, decimal factor, DbDataTypeEnum dbType)
- {
- switch (dbType)
- {
- case DbDataTypeEnum.Int32:
- return $"new BsonInt32({(factor> 0 ? $"(int)({factor}{getDecimalShortChar(factor)} * {sourceValue})": sourceValue)})";
- case DbDataTypeEnum.Int64:
- return $"new BsonInt64({(factor> 0 ? $"(long)({factor}{getDecimalShortChar(factor)} * {sourceValue})": sourceValue)})";
- case DbDataTypeEnum.Double:
- return $"new BsonDouble({(factor> 0 ? $"(double)({factor}{getDecimalShortChar(factor)} * {sourceValue})": $"(double){sourceValue}")})";
- case DbDataTypeEnum.DateTime:
- return $"new BsonDateTime(_baseDt.AddSeconds({sourceValue}))";
- case DbDataTypeEnum.Decimal:
- return $"new Decimal128({(factor> 0 ? $"(decimal)({factor}{getDecimalShortChar(factor)} * {sourceValue})": sourceValue)})";
- default:
- throw new ArgumentOutOfRangeException(nameof(dbType), dbType, null);
- }
- }
- private string byteToBit(string data, int index)
- {
- switch (index)
- {
- case 0:
- {
- return $"(({data} & 1) ==1)";//(data & 1) == 1;
- }
- case 1:
- {
- return $"(({data} & 2) ==1)";// (data & 2) == 2;
- }
- case 2:
- {
- return $"(({data} & 4) ==1)";//(data & 4) == 4;
- }
- case 3:
- {
- return $"(({data} & 8) ==1)";//(data & 8) == 8;
- }
- case 4:
- {
- return $"(({data} & 16) ==1)";//(data & 16) == 16;
- }
- case 5:
- {
- return $"(({data} & 32) ==1)";//(data & 32) == 32;
- }
- case 6:
- {
- return $"(({data} & 64) ==1)";//(data & 64) == 64;
- }
- case 7:
- {
- return $"(({data} & 128) ==1)";//(data & 128) == 128;
- }
- default:
- throw new ArgumentOutOfRangeException(nameof(index));
- }
- return $"(({data} & {index + 1}) ==1)";
- }
- /// <summary>
- /// 用于判断传入的 fator 是否需要使用 deciaml 进行运算, 如果有小数点的, 则是否 decimal 缩写 m,, 如果没有小数点, 则使用普通的 int 类型
- /// </summary>
- /// <param name="value"></param>
- /// <returns></returns>
- private string getDecimalShortChar(decimal value)
- {
- return (value % 1) == 0 ? "":"m";
- }
- public int GetNextClasID()
- {
- return Interlocked.Increment(ref _classID);
- }
- }
- View Code
这样, 在运行时, 即可直接生成可用于解析的类了, 而且也不需要 for 循环判断, 生成出来的类如:
- public class ProtocolExecutor_1_1_000
- {
- public BsonDocument Execute(byte[] data, int startIndex)
- {
- BsonDocument bson = new BsonDocument();
- bson["项目名 1"] = new BsonInt32(BitConverter.ToInt32(data, startIndex + 偏移量));
- bson["项目名 2"] = new BsonInt32(BitConverter.ToInt32(data, startIndex + 偏移量));
- ....... // 其他数据项
- return bson;
- }
- }
到这一步, 就可以根绝配置项生成出对应的 C# 代码了, 剩下的就是动态编译的事情了, 将该代码编译出运行时 Type, 然后传入数据 ---- 解析出数据
来源: https://www.cnblogs.com/kugar/p/10549982.html