65.9K
CodeProject 正在变化。 阅读更多。
Home

消息结构库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (6投票s)

2015年1月1日

CPOL

4分钟阅读

viewsIcon

26011

downloadIcon

55

本文演示了一种创建消息结构、将字节数组转换为消息以及将消息转换回字节数组的方法。

引言

消息是一种简单的数据结构,包含类型化的字段。字段可以是基本类型,如intuintfloatdoublebytebyte array等。消息还可以包含任意嵌套的消息。我为我的需求编写了一个简单的消息结构库。我的问题是为设备模拟器构建消息结构,我想实现的是创建预定义的消息structs,填充字段值,并将它们作为字节数组发送给设备。然后,从设备接收字节数组并将其转换为消息以处理字段值。

背景

封送处理对于这类操作非常有用,它不是不安全的代码,而且速度非常快。示例如下转换:

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位的字段。我想用图来说明它

Message Fields

如上所示,Field1是3位,然后是2位保留,Field2是3位,Field3是4位,Field4是4位。

我的示例消息长度为16位,我需要一个长度为2的字节数组。我需要将所有字段定义为byte,总共需要4个字节(不包括保留字段)。而我不能使用封送处理,因为我的struct是4个字节,而传入的数据是2个字节。所以,我需要手动解析数据并分配我的字段。

结构

消息的结构如下:

Message

消息结构

报头消息可以包含“消息ID”和“消息长度”字段。

报头字段用于标识,我们可以检查其长度和类型来识别传入的数据。

尾部消息包含一个“校验和”字段。

通常,消息有一个checksum字段,该字段位于消息的最后几个字节。它是一种错误检测代码,常用于数字网络和存储设备中,以检测原始数据的意外更改。进入这些系统的数据块会附加一个简短的校验值,该值基于对其内容的多项式除法的余数;在检索时,会重复计算,如果校验值不匹配,则可以采取纠正措施,以应对假定的数据损坏。

这是消息结构库的类图

Class Diagram

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个字段。

RxTx消息的定义完全相同,因此我们可以同时使用一个消息作为RxTx消息。

这是标识和转换传入数据的消息的方法:

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截图

Screenshot 1Screenshot 2

在第一种情况下,我发送WrapAround消息并接收WrapAcknowledge消息。

在第二种情况下,我正在将预定义的字节数组数据转换为消息。解析器识别它并将其转换为LWRThreats消息。

根据我的简单基准测试结果;对于100字节的传入数据,它可以在1毫秒内解析并转换为消息。

关注点

我知道在这种情况下使用反射和泛型不是很理想,但我使用它们是为了通过提供灵活的结构来简化编码人员的工作,让他们能够应对自己具有挑战性的算法问题。如果您能修复我的代码并指导我更好的技术,我将不胜感激。

希望您喜欢,谢谢您的阅读!

历史

  • 2015年1月1日 - 初始版本
© . All rights reserved.