消息结构库






4.33/5 (6投票s)
本文演示了一种创建消息结构、将字节数组转换为消息以及将消息转换回字节数组的方法。
引言
消息
是一种简单的数据结构,包含类型化的字段。字段可以是基本类型,如int
、uint
、float
、double
、byte
、byte array
等。消息还可以包含任意嵌套的消息。我为我的需求编写了一个简单的消息结构库。我的问题是为设备模拟器构建消息结构,我想实现的是创建预定义的消息struct
s,填充字段值,并将它们作为字节数组发送给设备。然后,从设备接收字节数组并将其转换为消息以处理字段值。
背景
封送处理对于这类操作非常有用,它不是不安全的代码,而且速度非常快。示例如下转换:
byte[] GetBytes(Message msg)
{
int size = Marshal.SizeOf(msg);
byte[] arr = new byte[size];
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(msg, ptr, true);
Marshal.Copy(ptr, arr, 0, size);
Marshal.FreeHGlobal(ptr);
return arr;
}
Message GetMessage(byte[] arr)
{
Message msg = new Message();
int size = Marshal.SizeOf(msg);
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.Copy(arr, 0, ptr, size);
msg = (Message)Marshal.PtrToStructure(ptr, msg.GetType());
Marshal.FreeHGlobal(ptr);
return msg;
}
为什么我不能用这个?
因为我的字段不总是byte
对齐。我的意思是,我可能有一个只有3位的字段。我想用图来说明它
如上所示,Field1
是3位,然后是2位保留,Field2
是3位,Field3
是4位,Field4
是4位。
我的示例消息长度为16位,我需要一个长度为2的字节数组。我需要将所有字段定义为byte
,总共需要4个字节(不包括保留字段)。而我不能使用封送处理,因为我的struct
是4个字节,而传入的数据是2个字节。所以,我需要手动解析数据并分配我的字段。
结构
消息的结构如下:
报头消息可以包含“消息ID
”和“消息长度
”字段。
报头字段用于标识,我们可以检查其长度和类型来识别传入的数据。
尾部消息包含一个“校验和
”字段。
通常,消息有一个checksum
字段,该字段位于消息的最后几个字节。它是一种错误检测代码,常用于数字网络和存储设备中,以检测原始数据的意外更改。进入这些系统的数据块会附加一个简短的校验值,该值基于对其内容的多项式除法的余数;在检索时,会重复计算,如果校验值不匹配,则可以采取纠正措施,以应对假定的数据损坏。
这是消息结构库的类图:
IMessage
是一个接口,必须由Message
类实现。
/// <summary>
/// Message interface.
/// </summary>
public interface IMessage
{
/// <summary>
/// Gets fields.
/// </summary>
List<Field> Fields { get; }
/// <summary>
/// Returns byte array representation of message.
/// </summary>
/// <returns>
/// The <see cref="byte[]"/>.
/// Byte array representation of message.
/// </returns>
byte[] GetMessage();
/// <summary>
/// Fills fields of message with input byte array.
/// </summary>
/// <param name="data">
/// The byte array data.
/// </param>
void SetMessage(byte[] data);
}
Field
是一个保存消息字段所有必需数据的类。
public class Field
{
#region ctor
/// <summary>
/// Initializes a new instance of the <see cref="Field"/> class.
/// </summary>
/// <param name="name">
/// The name.
/// </param>
/// <param name="length">
/// The length.
/// </param>
public Field(string name, int length)
{
this.Name = name;
this.Length = length;
this.Resolution = 1;
this.DataType = DataTypes.BYTE;
var byteCount = (int)Math.Ceiling(length / 8f);
this.Data = new byte[byteCount];
}
/// <summary>
/// Initializes a new instance of the <see cref="Field"/> class.
/// </summary>
/// <param name="name">
/// The name.
/// </param>
/// <param name="length">
/// The length.
/// </param>
/// <param name="type">
/// The type.
/// </param>
public Field(string name, int length, DataTypes type)
: this(name, length)
{
this.DataType = type;
}
/// <summary>
/// Initializes a new instance of the <see cref="Field"/> class.
/// </summary>
/// <param name="name">
/// The name.
/// </param>
/// <param name="length">
/// The length.
/// </param>
/// <param name="type">
/// The type.
/// </param>
/// <param name="resolution">
/// The resolution.
/// </param>
public Field(string name, int length, DataTypes type, int resolution)
: this(name, length, type)
{
this.Resolution = resolution;
}
/// <summary>
/// Initializes a new instance of the <see cref="Field"/> class.
/// </summary>
/// <param name="length">
/// The length.
/// </param>
/// <param name="type">
/// The type.
/// </param>
/// <param name="resolution">
/// The resolution.
/// </param>
public Field(int length, DataTypes type, int resolution)
: this(string.Empty, length, type, resolution)
{
this.Resolution = resolution;
}
/// <summary>
/// Initializes a new instance of the <see cref="Field"/> class.
/// </summary>
/// <param name="length">
/// The length.
/// </param>
/// <param name="type">
/// The type.
/// </param>
public Field(int length, DataTypes type)
: this(string.Empty, length, type)
{
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the value updated.
/// </summary>
public Action ValueUpdated { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the length.
/// </summary>
public int Length { get; set; }
/// <summary>
/// Gets or sets the resolution.
/// </summary>
public double Resolution { get; set; }
/// <summary>
/// Gets or sets the byte array data.
/// </summary>
public byte[] Data { get; set; }
/// <summary>
/// Gets or sets the data type.
/// </summary>
public DataTypes DataType { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Sets value of field.
/// </summary>
/// <param name="value">
/// The value.
/// </param>
/// <exception cref="Exception">
/// </exception>
public void SetValue(object value)
{
switch (this.DataType)
{
case DataTypes.INT:
this.SetValue((int)value);
break;
case DataTypes.UINT:
this.SetValue((uint)value);
break;
case DataTypes.SHORT:
this.SetValue((short)value);
break;
case DataTypes.USHORT:
this.SetValue((ushort)value);
break;
case DataTypes.FLOAT:
this.SetValue((float)value);
break;
case DataTypes.DOUBLE:
this.SetValue((double)value);
break;
case DataTypes.BYTE:
this.SetValue((byte)value);
break;
case DataTypes.BYTEARRAY:
this.SetValue((byte[])value);
break;
default:
throw new Exception("Hata");
}
}
/// <summary>
/// The set value.
/// </summary>
/// <param name="value">
/// The value.
/// </param>
/// <typeparam name="T">
/// Available types: int, uint, short, ushort, float, double, byte, char, byte[]
/// </typeparam>
public void SetValue<T>(T value)
{
var len = (int)Math.Ceiling(this.Length / 8f);
if (typeof(T) == typeof(int))
{
var intvalue = (int)(Convert.ToInt32(value) / this.Resolution);
this.Data = BitConverter.GetBytes(intvalue).Reverse().ToArray().PadTrimArray(len);
}
else if (typeof(T) == typeof(uint))
{
var uintvalue = (uint)(Convert.ToUInt32(value) / this.Resolution);
this.Data = BitConverter.GetBytes(uintvalue).Reverse().ToArray().PadTrimArray(len);
}
else if (typeof(T) == typeof(short))
{
var shortvalue = (short)(Convert.ToInt16(value) / this.Resolution);
this.Data = BitConverter.GetBytes(shortvalue).Reverse().ToArray();
}
else if (typeof(T) == typeof(ushort))
{
var ushortvalue = (ushort)(Convert.ToUInt16(value) / this.Resolution);
this.Data = BitConverter.GetBytes(ushortvalue).Reverse().ToArray().PadTrimArray(len);
}
else if (typeof(T) == typeof(float))
{
var floatvalue = (float)(Convert.ToSingle(value) / this.Resolution);
this.Data = BitConverter.GetBytes(floatvalue).PadTrimArray(len).Reverse().ToArray();
}
else if (typeof(T) == typeof(double))
{
double doublevalue = Convert.ToDouble(value) / this.Resolution;
this.Data = BitConverter.GetBytes(doublevalue).Reverse().ToArray().PadTrimArray(len);
}
else if (typeof(T) == typeof(byte))
{
var bytevalue = (byte)(Convert.ToByte(value) / this.Resolution);
this.Data = BitConverter.GetBytes(bytevalue).Reverse().ToArray().PadTrimArray(len);
}
else if (typeof(T) == typeof(char))
{
var charvalue = (char)Convert.ToChar(value);
this.Data = BitConverter.GetBytes(charvalue).Reverse().ToArray().PadTrimArray(len);
}
else if (typeof(T) == typeof(byte[]))
{
this.Data = (byte[])(object)value;
}
else
{
throw new ArgumentException("value", "Invalid type.");
}
if (this.ValueUpdated != null)
{
this.ValueUpdated();
}
}
/// <summary>
/// The get value.
/// </summary>
/// <returns>
/// The <see cref="string"/>.
/// </returns>
public string GetValue()
{
return this.GetValue(this.DataType);
}
/// <summary>
/// The get value.
/// </summary>
/// <typeparam name="T">
/// Available types: int, uint, short, ushort, float, double, byte, char, byte[]
/// </typeparam>
/// <returns>
/// The <see cref="T"/>.
/// Returns value after converted to selected type.
/// </returns>
public T GetValue<T>()
{
if (typeof(T) == typeof(int))
{
var arr = this.Data.PadTrimArray(4);
var value = (int)(BitConverter.ToInt32(arr.Reverse().ToArray(), 0) * this.Resolution);
return (T)Convert.ChangeType(value, typeof(T));
}
if (typeof(T) == typeof(uint))
{
var arr = this.Data.PadTrimArray(4);
var value = (uint)(BitConverter.ToUInt32(arr.Reverse().ToArray(), 0) * this.Resolution);
return (T)Convert.ChangeType(value, typeof(T));
}
if (typeof(T) == typeof(short))
{
var arr = this.Data.PadTrimArray(2);
var value = (short)(BitConverter.ToInt16(arr.Reverse().ToArray(), 0) * this.Resolution);
return (T)Convert.ChangeType(value, typeof(T));
}
if (typeof(T) == typeof(ushort))
{
var arr = this.Data.PadTrimArray(2);
var value = (ushort)(BitConverter.ToUInt16(arr.Reverse().ToArray(), 0) * this.Resolution);
return (T)Convert.ChangeType(value, typeof(T));
}
if (typeof(T) == typeof(float))
{
var arr = this.Data.PadTrimArray(4);
var value = (float)(BitConverter.ToSingle(arr.Reverse().ToArray(), 0) * this.Resolution);
return (T)Convert.ChangeType(value, typeof(T));
}
if (typeof(T) == typeof(double))
{
var arr = this.Data.PadTrimArray(4);
var value = BitConverter.ToDouble(arr.Reverse().ToArray(), 0) * this.Resolution;
return (T)Convert.ChangeType(value, typeof(T));
}
if (typeof(T) == typeof(byte))
{
var value = (byte)(this.Data[0] * this.Resolution);
return (T)Convert.ChangeType(value, typeof(T));
}
if (typeof(T) == typeof(char))
{
var value = (char)this.Data[0];
return (T)Convert.ChangeType(value, typeof(T));
}
if (typeof(T) == typeof(byte[]))
{
return (T)Convert.ChangeType(this.Data, typeof(T));
}
return default(T);
}
/// <summary>
/// The to string.
/// </summary>
/// <returns>
/// The <see cref="string"/>.
/// </returns>
public override string ToString()
{
return this.Name;
}
#endregion
#region Private Methods
/// <summary>
/// Returns string representation of value of field.
/// </summary>
/// <param name="dataType">
/// The data type.
/// </param>
/// <returns>
/// The <see cref="string"/>.
/// String representation of field.
/// </returns>
private string GetValue(DataTypes dataType)
{
switch (dataType)
{
case DataTypes.INT:
return this.GetValue<int>().ToString();
case DataTypes.UINT:
return this.GetValue<uint>().ToString();
case DataTypes.SHORT:
return this.GetValue<short>().ToString();
case DataTypes.USHORT:
return this.GetValue<ushort>().ToString();
case DataTypes.FLOAT:
return this.GetValue<float>().ToString();
case DataTypes.DOUBLE:
return this.GetValue<double>().ToString();
case DataTypes.BYTE:
return this.GetValue<byte>().ToString();
case DataTypes.CHAR:
return this.GetValue<char>().ToString();
case DataTypes.BYTEARRAY:
return BitConverter.ToString(this.GetValue<byte[]>());
default:
return null;
}
}
#endregion
}
Message
类保存所有字段、报头和尾部消息。
public abstract class Message : IMessage
{
#region Fields
/// <summary>
/// The checksum field.
/// </summary>
protected Field CheckSumField;
/// <summary>
/// Fields of message.
/// </summary>
private List<Field> fields;
/// <summary>
/// Fields of message without checksum field.
/// </summary>
private List<Field> fieldsWoChecksum;
#endregion
#region Properties
/// <summary>
/// Gets or sets the header message.
/// </summary>
public Message HeaderMessage { get; set; }
/// <summary>
/// Gets or sets the footer message.
/// </summary>
public Message FooterMessage { get; set; }
/// <summary>
/// Gets or sets a value indicating whether is checksum exists.
/// </summary>
public bool IsChecksumExists { get; set; }
/// <summary>
/// Gets the fields.
/// </summary>
public virtual List<Field> Fields
{
get
{
if (this.fields == null)
{
this.GatherFields();
return this.fields;
}
return this.fields;
}
}
#endregion
#region Public Methods
/// <summary>
/// Fills fields of message with input byte array.
/// </summary>
/// <param name="data">
/// The byte array data.
/// </param>
public virtual void SetMessage(byte[] data)
{
this.Fill(data);
}
/// <summary>
/// Returns byte array representation of message.
/// </summary>
/// <returns>
/// The <see cref="byte[]"/>.
/// Byte array representation of message.
/// </returns>
public virtual byte[] GetMessage()
{
this.ReCalculateCheckSum();
return this.ToByteArray();
}
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>
/// A string that represents the current object.
/// </returns>
/// <filterpriority>2</filterpriority>
public override string ToString()
{
if (this.fields == null)
{
this.GatherFields();
}
try
{
return string.Join(
Environment.NewLine,
this.fields.Select(p => p.Name +
":\t" + (p.GetValue() == "\0" ?
"Null Character" : p.GetValue())));
}
catch
{
return string.Empty;
}
}
/// <summary>
/// Initializes fields of message.
/// </summary>
protected void InitFields()
{
this.GatherFields();
var fieldProperties =
this.GetType()
.GetProperties()
.Where(p => p.PropertyType == typeof (Field))
.ToList();
foreach (var a in fieldProperties)
{
object o = a.GetValue(this, null);
if (o == null)
{
continue;
}
var f = o as Field;
if (f != null && f.Name == string.Empty)
{
f.Name = Utils.SplitCamelCase(a.Name);
}
}
}
/// <summary>
/// Returns calculated checksum value.
/// </summary>
/// <returns>
/// The <see cref="byte"/>.
/// </returns>
protected virtual byte GetCheckSum()
{
return Utils.CalculateChecksum(this.Fields.ToByteArray());
}
/// <summary>
/// Calculates checksum and sets checksum field.
/// </summary>
protected void ReCalculateCheckSum()
{
if (this.fields == null)
{
this.GatherFields();
}
if (this.IsChecksumExists && this.CheckSumField != null)
{
this.CheckSumField.SetValue
(Utils.CalculateChecksum(this.fieldsWoChecksum.ToByteArray()));
}
}
#endregion
#region Private Methods
/// <summary>
/// Gathers all fields.
/// </summary>
private void GatherFields()
{
var fieldProperties = this.GetType().GetProperties().ToList();
this.fields = new List<Field>();
foreach (var a in fieldProperties)
{
object o = a.GetValue(this, null);
if (o == null)
{
continue;
}
if (o is Field)
{
fields.Add(o as Field);
}
else if (o is IEnumerable<Message>)
{
var y = o as IEnumerable<Message>;
fields.AddRange(y.SelectMany(p => p.Fields));
}
}
if (this.HeaderMessage != null)
{
this.fields.InsertRange(0, this.HeaderMessage.Fields);
}
if (this.FooterMessage != null)
{
this.fields.AddRange(this.FooterMessage.Fields);
}
if (this.IsChecksumExists)
{
this.CheckSumField = this.fields.Last();
if (this.fieldsWoChecksum == null)
{
this.fieldsWoChecksum = this.Fields.Except
(new List<Field> { this.CheckSumField }).ToList();
}
this.fieldsWoChecksum.ForEach
(p => p.ValueUpdated = this.ReCalculateCheckSum);
}
}
#endregion
}
Using the Code
我准备了一个示例项目来使用这个库,我定义了一些Rx
(传入)和Tx
(传出)消息,并带有任意字段。并编写了一些方法来解析传入数据进行标识。
一个示例Rx
消息,WrapAcknowledge
public class WrapAcknowledge : Message
{
public Field Version { get; set; }
public Field OutgoingData { get; set; }
public WrapAcknowledge()
{
this.IsChecksumExists = true;
this.HeaderMessage = new MyHeader(0x01, 0x01, 0x0F);
this.FooterMessage = new MyFooter();
this.Version = new Field(16, DataTypes.USHORT);
this.OutgoingData = new Field(64, DataTypes.BYTEARRAY);
this.InitFields();
}
}
我们需要告诉类是否有checksum
字段,并在存在时定义我们的报头和尾部消息。我们需要调用InitFields
基类方法,让消息通过反射收集所有自身字段。所有字段都有一个name属性,可以使其可读地打印出来,如果我们为字段属性提供清晰的驼峰命名声明,我们就不必担心为字段定义名称。我们可以再次通过反射解析它们。
一个示例Tx
消息,WrapAround
public enum WrapAroundType
{
Type1 = 0,
Type2
};
public class WrapAround : Message
{
public Field Version { get; set; }
public Field OutgoingData { get; set; }
public WrapAround()
{
this.IsChecksumExists = true;
this.HeaderMessage = new MyHeader(0x01, 0x01, 0x0F);
this.FooterMessage = new MyFooter();
this.Version = new Field(16, DataTypes.USHORT);
this.OutgoingData = new Field(64, DataTypes.BYTEARRAY);
this.InitFields();
}
public WrapAround(WrapAroundType type)
: this()
{
if (type == WrapAroundType.Type1)
{
this.OutgoingData.SetValue(new byte[]
{ 0x51, 0x45, 0xA0, 0x11, 0x00, 0x00, 0xFF, 0xFF });
}
else if (type == WrapAroundType.Type2)
{
this.OutgoingData.SetValue(new byte[]
{ 0x3A, 0xBA, 0x02, 0x0F, 0x34, 0xA5, 0xF0, 0xF0 });
}
}
}
一个更复杂的消息,LWRThreats
:
public class LWRThreats : Message
{
public Field Foo { get; set; }
public List<Threat> Treats{ get; set; }
public LWRThreats()
{
this.IsChecksumExists = true;
this.HeaderMessage = new MyHeader(0x01, 0x05, 0x36);
this.FooterMessage = new MyFooter();
this.Foo = new Field(8, DataTypes.BYTE);
this.Treats = new List<Threat>().SetCapacity(3);
this.InitFields();
}
}
此消息依次包含:
- 一个报头消息(3个字段)
- 其自身的字段Foo(1个字段)
- 消息的威胁列表,长度为3(每个威胁消息有8个字段,总共24个字段)
- 一个尾部消息(1个字段)
因此,此消息按此顺序总共有29个字段。
Rx
和Tx
消息的定义完全相同,因此我们可以同时使用一个消息作为Rx
和Tx
消息。
这是标识和转换传入数据的消息的方法:
public static Message DetermineMessage(byte[] data)
{
var header = new MyHeader();
header.SetMessage(data);
return ParseWithHeader(header, data);
}
private static Message ParseWithHeader(MyHeader header, byte[] data)
{
var len = header.MessageLength.GetValue<ushort>();
if (len != data.Length)
{
return null;
}
if (Utils.CalculateChecksum(data) != 0x00)
{
return null;
}
var tip = header.MessageType.GetValue<byte>();
switch (tip)
{
case 0x01:
Message mesaj = new WrapAcknowledge();
mesaj.SetMessage(data);
return mesaj;
case 0x03:
mesaj = new SystemStatus();
mesaj.SetMessage(data);
return mesaj;
case 0x05:
mesaj = new LWRThreats();
mesaj.SetMessage(data);
return mesaj;
default:
return null;
}
}
首先,我将传入数据转换为报头消息,通过这种方式我可以检索其标识值。如果长度字段的值不等于传入数据的长度,我们需要丢弃此数据。
第二次检查是checksum
,我们需要计算消息的checksum
并检查它是否等于零。如果失败,我们需要丢弃此数据。
最后一步是使用像消息类型字段这样的switch
case跳转流来查找可用的消息类型。如果没有定义的消息具有此消息类型值,我们需要丢弃此数据。否则,我们可以将数据转换为消息并将其返回给调用者。
GUI截图
在第一种情况下,我发送WrapAround
消息并接收WrapAcknowledge
消息。
在第二种情况下,我正在将预定义的字节数组数据转换为消息。解析器识别它并将其转换为LWRThreats
消息。
根据我的简单基准测试结果;对于100字节的传入数据,它可以在1毫秒内解析并转换为消息。
关注点
我知道在这种情况下使用反射和泛型不是很理想,但我使用它们是为了通过提供灵活的结构来简化编码人员的工作,让他们能够应对自己具有挑战性的算法问题。如果您能修复我的代码并指导我更好的技术,我将不胜感激。
希望您喜欢,谢谢您的阅读!
历史
- 2015年1月1日 - 初始版本