【问题标题】:Extracting Values Across Byte Boundaries With Arbitrary Bit Positions and Lengths In C#在 C# 中使用任意位位置和长度跨字节边界提取值
【发布时间】:2011-07-11 17:43:49
【问题描述】:

我目前正在开发一种网络工具,该工具需要对特定协议进行解码/编码,该协议将字段打包到任意位置的密集位数组中。例如,协议的一部分使用 3 个字节来表示多个不同的字段:

Bit Position(s)  Length (In Bits)    Type
0                1                   bool
1-5              5                   int
6-13             8                   int
14-22            9                   uint
23               1                   bool

如您所见,其中几个字段跨越多个字节。许多(大多数)也比可能用于表示它们的内置类型短,例如第一个 int 字段只有 5 位长。在这些情况下,目标类型(例如 Int32 或 Int16)的最高有效位应用 0 填充以弥补差异。

我的问题是我很难处理这种数据。具体来说,我很难弄清楚如何有效地获取任意长度的位数组,用源缓冲区中的适当位填充它们,填充它们以匹配目标类型,并将填充的位数组转换为目标类型。在理想情况下,我可以使用上面示例中的 byte[3] 并调用像 GetInt32(byte[] bytes, int startBit, int length) 这样的方法。

我发现的最接近的东西是BitStream 类,但它似乎希望各个值在字节/字边界上排列(以及半流式/半索引访问约定)类使它有点混乱)。

我自己的第一次尝试是使用BitArray 类,但事实证明这有点笨拙。将缓冲区中的所有位填充到一个大的BitArray 中很容易,只将您想要的那些从源BitArray 传输到一个新的临时BitArray,然后将其转换为目标值......但是这似乎是错误的,而且非常耗时。

我现在正在考虑一个类似下面的类,它引用(或创建)一个源/目标 byte[] 缓冲区以及偏移量,并为某些目标类型提供 get 和 set 方法。棘手的部分是获取/设置值可能跨越多个字节。

class BitField
{
    private readonly byte[] _bytes;
    private readonly int _offset;

    public BitField(byte[] bytes)
        : this(bytes, 0)
    {
    }

    public BitField(byte[] bytes, int offset)
    {
        _bytes = bytes;
        _offset = offset;
    }

    public BitField(int size)
        : this(new byte[size], 0)
    {
    }

    public bool this[int bit]
    {
        get { return IsSet(bit); }
        set { if (value) Set(bit); else Clear(bit); }
    }

    public bool IsSet(int bit)
    {
        return (_bytes[_offset + (bit / 8)] & (1 << (bit % 8))) != 0;
    }

    public void Set(int bit)
    {
        _bytes[_offset + (bit / 8)] |= unchecked((byte)(1 << (bit % 8)));
    }

    public void Clear(int bit)
    {
        _bytes[_offset + (bit / 8)] &= unchecked((byte)~(1 << (bit % 8)));
    }

    //startIndex = the index of the bit at which to start fetching the value
    //length = the number of bits to include - may be less than 32 in which case
    //the most significant bits of the target type should be padded with 0
    public int GetInt32(int startIndex, int length)
    {
        //NEED CODE HERE
    }

    //startIndex = the index of the bit at which to start storing the value
    //length = the number of bits to use, if less than the number of bits required
    //for the source type, precision may be lost
    //value = the value to store
    public void SetValue(int startIndex, int length, int value)
    {
        //NEED CODE HERE
    }

    //Other Get.../Set... methods go here
}

我正在寻找这方面的任何指导,例如第三方库、在跨越多个字节的任意位位置获取/设置值的算法、对我的方法的反馈等。我将上面的课程包括在内以进行澄清,我不一定要寻找代码来填充它(尽管我不会争论是否有人想解决它!)。

【问题讨论】:

  • 一些人提到了字节序,这当然是一个问题。因为我正在处理网络数据,所以我假设原始缓冲区是大端(或网络顺序)。我计划使用来自MiscUtils 的优秀EndianBitConverter 将所有内容转换为Get... 方法中的本地顺序。
  • 我还在解决这个问题。对于这种情况,下面的答案都不是完全正确的(特别是涉及任意位置和跨越字节边界的方面)。我决定完全实现问题中提出的 BitField 类,并且一直很幸运。因为它可能有很多用途,特别是对于位域通常密集的网络处理,一旦完成,我将在接下来的几天发布完成的课程作为附加答案。我将继续投票支持解决该问题的其他有用答案。

标签: c# networking binary bit-manipulation


【解决方案1】:

正如所承诺的,这是我最终为此目的创建的类。它将在可选指定的索引处包装任意字节数组,并允许在位级别进行读/写。它提供了从其他字节数组读取/写入任意位块或读取/写入具有用户定义的偏移和长度的原始值的方法。它非常适合我的情况,并解决了我上面提出的确切问题。但是,它确实有几个缺点。首先是它显然没有大量记录 -​​ 我只是没有时间。第二个是没有界限或其他检查。它目前还需要MiscUtil 库来提供字节序转换。综上所述,希望这可以帮助解决或作为其他有类似用例的人的起点。

internal class BitField
{
    private readonly byte[] _bytes;
    private readonly int _offset;
    private EndianBitConverter _bitConverter = EndianBitConverter.Big;

    public BitField(byte[] bytes)
        : this(bytes, 0)
    {
    }

    //offset = the offset (in bytes) into the wrapped byte array
    public BitField(byte[] bytes, int offset)
    {
        _bytes = bytes;
        _offset = offset;
    }

    public BitField(int size)
        : this(new byte[size], 0)
    {
    }

    //fill == true = initially set all bits to 1
    public BitField(int size, bool fill)
        : this(new byte[size], 0)
    {
        if (!fill) return;
        for(int i = 0 ; i < size ; i++)
        {
            _bytes[i] = 0xff;
        }
    }

    public byte[] Bytes
    {
        get { return _bytes; }
    }

    public int Offset
    {
        get { return _offset; }
    }

    public EndianBitConverter BitConverter
    {
        get { return _bitConverter; }
        set { _bitConverter = value; }
    }

    public bool this[int bit]
    {
        get { return IsBitSet(bit); }
        set { if (value) SetBit(bit); else ClearBit(bit); }
    }

    public bool IsBitSet(int bit)
    {
        return (_bytes[_offset + (bit / 8)] & (1 << (7 - (bit % 8)))) != 0;
    }

    public void SetBit(int bit)
    {
        _bytes[_offset + (bit / 8)] |= unchecked((byte)(1 << (7 - (bit % 8))));
    }

    public void ClearBit(int bit)
    {
        _bytes[_offset + (bit / 8)] &= unchecked((byte)~(1 << (7 - (bit % 8))));
    }

    //index = the index of the source BitField at which to start getting bits
    //length = the number of bits to get
    //size = the total number of bytes required (0 for arbitrary length return array)
    //fill == true = set all padding bits to 1
    public byte[] GetBytes(int index, int length, int size, bool fill)
    {
        if(size == 0) size = (length + 7) / 8;
        BitField bitField = new BitField(size, fill);
        for(int s = index, d = (size * 8) - length ; s < index + length && d < (size * 8) ; s++, d++)
        {
            bitField[d] = IsBitSet(s);
        }
        return bitField._bytes;
    }

    public byte[] GetBytes(int index, int length, int size)
    {
        return GetBytes(index, length, size, false);
    }

    public byte[] GetBytes(int index, int length)
    {
        return GetBytes(index, length, 0, false);
    }

    //bytesIndex = the index (in bits) into the bytes array at which to start copying
    //index = the index (in bits) in this BitField at which to put the value
    //length = the number of bits to copy from the bytes array
    public void SetBytes(byte[] bytes, int bytesIndex, int index, int length)
    {
        BitField bitField = new BitField(bytes);
        for (int i = 0; i < length; i++)
        {
            this[index + i] = bitField[bytesIndex + i];
        }
    }

    public void SetBytes(byte[] bytes, int index, int length)
    {
        SetBytes(bytes, 0, index, length);
    }

    public void SetBytes(byte[] bytes, int index)
    {
        SetBytes(bytes, 0, index, bytes.Length * 8);
    }

    //UInt16

    //index = the index (in bits) at which to start getting the value
    //length = the number of bits to use for the value, if less than required the value is padded with 0
    public ushort GetUInt16(int index, int length)
    {
        return _bitConverter.ToUInt16(GetBytes(index, length, 2), 0);
    }

    public ushort GetUInt16(int index)
    {
        return GetUInt16(index, 16);
    }

    //valueIndex = the index (in bits) of the value at which to start copying
    //index = the index (in bits) in this BitField at which to put the value
    //length = the number of bits to copy from the value
    public void Set(ushort value, int valueIndex, int index, int length)
    {
        SetBytes(_bitConverter.GetBytes(value), valueIndex, index, length);
    }

    public void Set(ushort value, int index)
    {
        Set(value, 0, index, 16);
    }

    //UInt32

    public uint GetUInt32(int index, int length)
    {
        return _bitConverter.ToUInt32(GetBytes(index, length, 4), 0);
    }

    public uint GetUInt32(int index)
    {
        return GetUInt32(index, 32);
    }

    public void Set(uint value, int valueIndex, int index, int length)
    {
        SetBytes(_bitConverter.GetBytes(value), valueIndex, index, length);
    }

    public void Set(uint value, int index)
    {
        Set(value, 0, index, 32);
    }

    //UInt64

    public ulong GetUInt64(int index, int length)
    {
        return _bitConverter.ToUInt64(GetBytes(index, length, 8), 0);
    }

    public ulong GetUInt64(int index)
    {
        return GetUInt64(index, 64);
    }

    public void Set(ulong value, int valueIndex, int index, int length)
    {
        SetBytes(_bitConverter.GetBytes(value), valueIndex, index, length);
    }

    public void Set(ulong value, int index)
    {
        Set(value, 0, index, 64);
    }

    //Int16

    public short GetInt16(int index, int length)
    {
        return _bitConverter.ToInt16(GetBytes(index, length, 2, IsBitSet(index)), 0);
    }

    public short GetInt16(int index)
    {
        return GetInt16(index, 16);
    }

    public void Set(short value, int valueIndex, int index, int length)
    {
        SetBytes(_bitConverter.GetBytes(value), valueIndex, index, length);
    }

    public void Set(short value, int index)
    {
        Set(value, 0, index, 16);
    }

    //Int32

    public int GetInt32(int index, int length)
    {
        return _bitConverter.ToInt32(GetBytes(index, length, 4, IsBitSet(index)), 0);
    }

    public int GetInt32(int index)
    {
        return GetInt32(index, 32);
    }

    public void Set(int value, int valueIndex, int index, int length)
    {
        SetBytes(_bitConverter.GetBytes(value), valueIndex, index, length);
    }

    public void Set(int value, int index)
    {
        Set(value, 0, index, 32);
    }

    //Int64

    public long GetInt64(int index, int length)
    {
        return _bitConverter.ToInt64(GetBytes(index, length, 8, IsBitSet(index)), 0);
    }

    public long GetInt64(int index)
    {
        return GetInt64(index, 64);
    }

    public void Set(long value, int valueIndex, int index, int length)
    {
        SetBytes(_bitConverter.GetBytes(value), valueIndex, index, length);
    }

    public void Set(long value, int index)
    {
        Set(value, 0, index, 64);
    }

    //Char

    public char GetChar(int index, int length)
    {
        return _bitConverter.ToChar(GetBytes(index, length, 2), 0);
    }

    public char GetChar(int index)
    {
        return GetChar(index, 16);
    }

    public void Set(char value, int valueIndex, int index, int length)
    {
        SetBytes(_bitConverter.GetBytes(value), valueIndex, index, length);
    }

    public void Set(char value, int index)
    {
        Set(value, 0, index, 16);
    }

    //Bool

    public bool GetBool(int index, int length)
    {
        return _bitConverter.ToBoolean(GetBytes(index, length, 1), 0);
    }

    public bool GetBool(int index)
    {
        return GetBool(index, 8);
    }

    public void Set(bool value, int valueIndex, int index, int length)
    {
        SetBytes(_bitConverter.GetBytes(value), valueIndex, index, length);
    }

    public void Set(bool value, int index)
    {
        Set(value, 0, index, 8);
    }

    //Single and double precision floating point values must always use the correct number of bits
    public float GetSingle(int index)
    {
        return _bitConverter.ToSingle(GetBytes(index, 32, 4), 0);
    }

    public void SetSingle(float value, int index)
    {
        SetBytes(_bitConverter.GetBytes(value), 0, index, 32);
    }

    public double GetDouble(int index)
    {
        return _bitConverter.ToDouble(GetBytes(index, 64, 8), 0);
    }

    public void SetDouble(double value, int index)
    {
        SetBytes(_bitConverter.GetBytes(value), 0, index, 64);
    }
}

【讨论】:

  • 您能否将其发布为对原始问题的修改?使它更容易找到。也谢谢你!
  • @Benjamin 的答案不应作为对问题的编辑发布。如果您希望某些内容易于查找,可以将其添加为书签..
【解决方案2】:

如果您的数据包始终小于 8 或 4 个字节,则将每个数据包存储在 Int32Int64 中会更容易。字节数组只会使事情复杂化。您必须注意 High-Endian 与 Low-Endian 存储。

然后,对于一个 3 字节的包:

public static void SetValue(Int32 message, int startIndex, int length, int value)
{
   // we want lengthx1
   int mask = (1 << length) - 1;     
   value = value & mask;  // or check and throw

   int offset = 24 - startIndex - length;   // 24 = 3 * 8
   message = message | (value << offset);
}

【讨论】:

  • 不幸的是,数据包的长度变化很大,通常比单个 Int64 可以容纳的要大很多。我查看了新的BigInteger 结构,想知道它是否可以与您提出的相同概念一起使用,但我现在被困在 3.5 上,所以我并没有在这条路上走得太远。也许我需要再看一下 - 这些算法是否适用于 BigInteger(它确实支持按位运算符)?
  • 嗯,BigInteger 似乎有移位运算符,它可能会为您节省一些工作。但不要指望超快的速度。
  • 刚发现这个并在.Net5中测试它,这不起作用,因为左移BigInteger会导致BigInteger容量增加,它不会在原始边界上截断。 RightShift 按预期工作。如果 byte[0] = 0,前导零字节也会被截断,因此第 8 位变为第 0 位。
【解决方案3】:

首先,您似乎用System.Collections.BitArray 类重新发明了轮子。至于实际找到特定位字段的值,我认为可以通过以下伪代码的一点数学魔法轻松完成:

  1. 从选择中最远的数字开始(startIndex + 长度)。
  2. 如果已设置,则添加 2^(与数字的距离)。在这种情况下,它将是 0 (mostDistance - self = 0)。所以加 2^0 (1)。
  3. 向左移动一位。
  4. 以您想要的长度重复每个数字。

在那种情况下,如果你有这样的位数组:

10001010

如果你想要数字 0-3 的值,你会得到类似的结果:

[Index 3]   [Index 2]   [Index 1]   [Index 0]
(3 - 3)     (3 - 2)     (3 - 1)     (3 - 0)
=============================================
(0 * 2^0) + (0 * 2^1) + (0 * 2^2) + (1 * 2^3) = 8

因为 1000(二进制)== 8,所以数学很有效。

【讨论】:

    【解决方案4】:

    仅使用简单的位移来获取您的值有什么问题?

    int data = Convert.ToInt32( "110000000010000000100001", 2 );
    
    bool v1 = ( data & 1 ) == 1; // True
    int v2 = ( data >> 1 ) & 0x1F; // 16
    int v3 = ( data >> 6 ) & 0xFF; // 128
    uint v4 = (uint )( data >> 14 ) & 0x1FF; // 256
    bool v5 = ( data >> 23 ) == 1; // True
    

    This 是一篇很好的文章,涵盖了这个主题。它在 C 中,但同样的概念仍然适用。

    【讨论】:

    • 对于我的具体问题,这些字段有很多,我不确定我是否要手动为每个字段编写访问器和设置器。事实上,在某些情况下,在其他部分被解码之前,我可能并不确切知道这些模式。我认为在许多情况下,该问题的通用解决方案会很有用。我不反对使用位操作、掩码、移位等——我只是想要让我使用任意起点和长度来完成它的算法。
    猜你喜欢
    • 2012-05-20
    • 2016-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-21
    • 2022-11-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多