从字节缓冲区中提取数据包

6

我有一个长度为256的缓冲区,用于接收来自蓝牙的字节序列。我需要提取的实际数据包以字节126开始和结束。我想使用LINQ提取缓冲区中最新的数据包。

目前我的做法是检查最后一个126的索引,然后向后计数,直到找到另一个126。还有一些注意事项,例如,相邻的两个数据包会导致两个126字节相邻。

以下是缓冲区的示例:

126   6 0   5   232 125 93  126 126 69  0 
0   1   0   2   2   34  6   0   5   232 125 
93  126 126 69  0   0   1   0   2   2   34 
6   0   5   232 125 93  126 126 69  0   0 
1   0   2   2   34  6   0   5   232 125 93 
126 126 69  0   0

因此,我掌握的信息如下:

  • 数据包以字节值126开头和结尾
  • 开始索引后面的下一个字节具有值69
  • 126结束字节之前的最后3个字节是整个数据包的CRC ,我知道如何计算它,因此在提取数据包后,我可以检查这个CRC以查看是否有正确的数据包

因此,最终我想要一个包含正确数据包的数组或列表。例如:

126 69  0  0   1   0   2   2   34  6   0   5   232 125 93 126

你能给我一个快速的解决方案来从缓冲区中提取这个数据包吗?

这是我目前尝试过的方法......它失败了,因为它无法返回我要查找的正确数据包:

var data = ((byte[])msg.Obj).ToList(); //data is the buffer 

byte del = 126; //delimeter or start/end byte
var lastIndex = data.LastIndexOf(del);
var startIndex = 0;
List<byte> tos = new List<byte>(); //a new list to store the result (packet)    

//try to figure out start index                            
if(data[lastIndex - 1] != del)
{
    for(int i = lastIndex; i > 0; i--)
    {
        if(data[i] == del)
        {
            startIndex = i;
        }
    }

    //add the result in another list
    for(int i = 0; i <= lastIndex - startIndex; i++)
    {
        tos.Add(data[i]);
    }

    string shit = string.Empty;

    foreach (var b in tos)
        shit += (int)b + ", ";

   //print result in  a textbox
    AddTextToLogTextView(shit + "\r\n");
}

@KendallFrey 我加了自己的方法,但似乎无法找到正确数据包的起始和结束索引 :( - Dumbo
你能解释一下你的逻辑吗? - ehudt
这个缓冲区是否可能存在部分结果?我的意思是,起始字节已发送,但到缓冲区末尾没有结束标记,因为它是更大响应的一部分?在这种情况下,任何下面提出的解决方案都是错误的。 - Alois Kraus
@AloisKraus 是的,这是一种可能性,在这种情况下,我们只需读取下一个缓冲区。 - Dumbo
5个回答

3

解决方案

我准备了三种可能的解决方案来从输入缓冲区中取出最后一个数据包:

使用 LINQ

public static byte[] GetLastPacketUsingLINQ(byte[] input, byte delimiter)
{
    var part = input.Reverse()
                    .SkipWhile(i => i != delimiter)
                    .SkipWhile(i => i == delimiter)
                    .TakeWhile(i => i != delimiter)
                    .Reverse();

    return (new byte[] { delimiter }).Concat(part).Concat(new byte[] { delimiter }).ToArray();
}

使用 string.Split

public static byte[] GetLastPacketUsingString(byte[] input, byte delimiter)
{
    var encoding = System.Text.Encoding.GetEncoding("iso-8859-1");
    string inputString = encoding.GetString(input);
    var parts = inputString.Split(new[] { (char)delimiter }, StringSplitOptions.RemoveEmptyEntries);

    return encoding.GetBytes((char)delimiter + parts[parts.Length - 2] + (char)delimiter);
}

使用while循环和索引器
public static byte[] GetLastPacketUsingIndexers(byte[] input, byte delimiter)
{
    int end = input.Length - 1;
    while (input[end--] != delimiter) ;

    int start = end - 1;
    while (input[start--] != delimiter) ;

    var result = new byte[end - start];
    Array.Copy(input, start + 1, result, 0, result.Length);
    return result;
}

性能

我也进行了一些非常简单的性能测试。以下是结果:

LINQ version result:
126 69 0 0 1 0 2 2 34 6 0 5 232 125 93 126

String version result:
126 69 0 0 1 0 2 2 34 6 0 5 232 125 93 126

Indexers version result:
126 69 0 0 1 0 2 2 34 6 0 5 232 125 93 126

LINQ version time: 64ms (106111 ticks)
String version time: 2ms (3422 ticks)
Indexers version time: 1ms (2359 ticks)

结论

正如您所看到的,最简单的方法也是最好的方法。

您可能会认为LINQ是解决所有问题的答案,但有时手动编写更简单的解决方案比使用LINQ方法更好。


所有的任意字节数组都不能转换为字符串而不丢失一些字节。因此,您的字符串版本将无法正常工作。 - I4V
你的索引器版本如果缓冲区恰好以最后一个数据包的结尾结束,则会失败。删除示例数据的最后四个字节,你就会明白我的意思。 - pescolino
好吧,毕竟,您的索引器方法经过一些编辑后是迄今为止我得到的最佳答案。 - Dumbo

3

如果以下两个规则适用于缓冲区,则可以使用LINQ在一行代码中完成此操作:

  • 缓冲区至少包含一个被给定分隔符包围的完整数据包。
  • 每个数据包都至少包含一个字节的数据。

以下是代码:

var data = (byte[])msg.Obj;
byte delimiter = 126;

var packet = data.Reverse()
                 .SkipWhile(b => b != delimiter)
                 .SkipWhile(b => b == delimiter)
                 .TakeWhile(b => b != delimiter)
                 .Reverse();

(好的,这个内容不止一行,因为我将它拆分成多行以便更易阅读。)

编辑:删除了对Take(1)的调用,因为它总是返回一个空序列。然而,这种方式结果不包含分隔符。


以下是它的工作原理:

由于我们想找到最后一个数据包,因此可以反转数据:

var reversed = data.Reverse();

缓冲区可能以未完整的数据包结尾。因此,让我们跳过它:
reversed = reversed.SkipWhile(b => b != delimiter);

reversed现在要么是空的,要么以delimiter开头。由于我们假设缓冲区始终至少包含一个完整的数据包,因此我们可以直接取下一个字节作为结果,因为我们知道它是分隔符:

var packet = reversed.Take(1);

在这个序列中,我们现在可以跳过一个字节。如果我们找到的分隔符实际上是一个新数据包的开始,那么剩余的序列将以另一个分隔符开头,因此我们也必须跳过它:
reversed = reversed.Skip(1);
if (reversed.First() == delimiter)
{
    reversed.Skip(1);
}

既然我们知道一个数据包不能是空的,因为它包含3个字节的CRC校验码,那么我们可以这样写:

reversed = reversed.SkipWhile(b => b == delimiter);

现在是实际数据:
packet = packet.Concat(reversed.TakeWhile(b => b != delimiter));
reversed = reversed.SkipWhile(b => b != delimiter);

下一个字节是分隔符,标志着数据包的开始:
packet = packet.Concat(reversed.Take(1));

最后要做的事情是再次翻转结果:
packet = packet.Reverse();

也许你想把这个放入一个方法中:
public IEnumerable<byte> GetPacket(byte[] data, byte delimiter)
{
    yield return delimiter;

    foreach (byte value in data.Reverse()
                               .SkipWhile(b => b != delimiter)
                               .SkipWhile(b => b == delimiter)
                               .TakeWhile(b => b != delimiter))
    {
        yield return value;
    }

    yield return delimiter;
}

您需要对此方法的返回值调用Reverse方法。


如果性能很重要,您可以在底层数组上使用相同的算法。这样速度会快约20倍:

int end = data.Length - 1;
while (data[end] != delimiter)
    end--;

while (data[end] == delimiter)
    end--;

int start = end;
while (data[start] != delimiter)
    start--;

byte[] result = new byte[end - start + 2];  // +2 to include delimiters
Array.Copy(data, start, result, 0, result.Length);

谢谢您的回答。我猜我们可以以某种方式去掉“Reverse”。 - Ken Kin
1
@KenKin:我添加了一个版本,直接在底层数组上工作,而不是使用LINQ。此外,我纠正了原始答案中的一个错误:调用Take(1)会产生一个具有单个元素的序列,然后被跳过。这总是导致一个空序列。 - pescolino

1
实际上有多种方法可以解决您的问题,最简单的想法是检测双倍的126(0x7e),而不用考虑其他像CRC这样的东西。
这个概念的基本实现如下:
  • 代码要简单

    var list=new List<byte[]>();
    int i=0, j=0;
    for(; i<data.Length; ++i)
        if(i>0&&0x7e==data[i]&&0x7e==data[i-1]) {
            list.Add(data.Skip(j).Take(i-j).ToArray());
            j=i;
        }
    list.Add(data.Skip(j).Take(i-j).ToArray());
    
基于我之前的C#中的Konami代码答案,并且它甚至被用来解决这个问题:C#记录按键时输入特殊字符显示双倍字符
  • 使用序列检测器编写代码

    public partial class TestClass {
        public static void TestMethod() {
            var data=(
                new[] { 
                        126, 6, 0, 5, 232, 125, 93, 126, 
                        126, 69, 0, 0, 1, 0, 2, 2, 34, 6, 0, 5, 232, 125, 93, 126, 
                        126, 69, 0, 0, 1, 0, 2, 2, 34, 6, 0, 5, 232, 125, 93, 126, 
                        126, 69, 0, 0, 1, 0, 2, 2, 34, 6, 0, 5, 232, 125, 93, 126, 
                        126, 69, 0, 0 
                    }).Select(x => (byte)x).ToArray();
    
            var list=new List<List<byte>>();
    
            foreach(var x in data) {
                if(list.Count<1||SequenceCapturer.Captured((int)x))
                    list.Add(new List<byte>());
    
                list.Last().Add(x);
            }
    
            foreach(var byteList in list)
                Debug.Print("{0}", byteList.Select(x => x.ToString("x2")).Aggregate((a, b) => a+"\x20"+b));
        }
    }
    
    public class SequenceCapturer {
        public int Count {
            private set;
            get;
        }
    
        public int[] Sequence {
            set;
            get;
        }
    
        public bool Captures(int value) {
            for(var i=Sequence.Length; i-->0; ) {
                if(Sequence[i]!=value) {
                    if(0==i)
                        Count=0;
    
                    continue;
                }
    
                if(Count!=i)
                    continue;
    
                ++Count;
                break;
            }
    
            var x=Sequence.Length==Count;
            Count=x?0:Count;
            return x;
        }
    
        public SequenceCapturer(int[] newSequence) {
            Sequence=newSequence;
        }
    
        public SequenceCapturer()
            : this(new[] { 0x7e, 0x7e }) {
        }
    
        public static bool Captured(int value) {
            return Instance.Captures(value);
        }
    
        public static SequenceCapturer Instance=new SequenceCapturer();
    }
    
如果您想要完全使用Linq编写它,您可能需要尝试以下方法。您甚至不需要使用ListpacketArray直接为您提供了一个字节数组的数组。
这些let旨在将代码分成几行,否则它将成为一条极长的语句。如果您认为一行最好,那么我也会这样做。
  • packetArray 代码

    var packetArray=(
        from sig in new[] { new byte[] { 0x7e, 0x7e } }
        let find=new Func<byte[], int, IEnumerable<byte>>((x, i) => x.Skip(i).Take(sig.Length))
        let isMatch=new Func<IEnumerable<byte>, bool>(sig.SequenceEqual)
        let filtered=data.Select((x, i) => 0==i||isMatch(find(data, i-1))?i:~0)
        let indices=filtered.Where(i => ~0!=i).Concat(new[] { data.Length }).ToArray()
        from index in Enumerable.Range(1, indices.Length-1)
        let skipped=indices[index-1]
        select data.Skip(skipped).Take(indices[index]-skipped).ToArray()).ToArray();
    
  • 输出代码

    foreach(var byteArray in packetArray)
        Debug.Print("{0}", byteArray.Select(x => x.ToString("x2")).Aggregate((a, b) => a+"\x20"+b));
    

然而,即使在同一解决方案的概念下,如我之前提到的那样,也会有各种各样的方式。我强烈建议不要涉及额外的条件,比如关于CRC的内容,这可能会使事情更加复杂。


0

因为您正在寻找最后一个数据包,所以更容易反转byte[]并查找第一个数据包。 您的两个数据包分隔符不仅是126。 除非数据包结尾是接收到的最后一个字节,否则它们是126, 69表示开始和126,126表示结束的分隔符。

我建议使用类似于此的方法:

public static byte[] GetMessage(byte[] msg)
    {
        //Set delimiters
        byte delimit = 126;
        byte startDelimit = 69;

        //Reverse the msg so we can find the last packet
        List<byte> buf = msg.Reverse().ToList();

        //set indices to impossible values to check for failures
        int startIndex = -1;
        int endIndex = -1;
        //loop through the message
        for (int i = 0; i < buf.Count - 1; i++)
        {
            //find either a double 126, or 126 as the last byte (message just ended)
            if (buf[i] == delimit && (buf[i + 1] == delimit || i == 0))
            {
                if (i == 0)
                {
                    startIndex = i;
                    i++;
                }
                else
                {
                    startIndex = i + 1;
                    i += 2;
                }
                continue;
            }
            //Only process if we've found the start index
            if (startIndex != -1)
            {
                //check if the byte is 69 followed by 126
                if (buf[i] == startDelimit && buf[i + 1] == delimit)
                {
                    endIndex = i + 1;
                    break;
                }
            }
        }
        //make sure we've found a message
        if (!(startIndex == -1 || endIndex==-1))
        {
            //get the message and reverse it to be the original packet
            byte[] revRet = new byte[endIndex - startIndex];
            Array.Copy(buf.ToArray(), startIndex, revRet, 0, endIndex - startIndex);

            return revRet.Reverse().ToArray();
        }
        return new byte[1];
    }

我不完全确定复制的索引是否完全正确,但这应该是它的要点。


0
由于可能会收到不完整的数据,您必须存储最后一个不完整的缓冲区。
这是一个示例情况,首次接收:
126,   6, 0,   5,  232, 125, 93,  126, 126, 69,  0, 
0,   1,   0,   2,   2,   34,  6  , 0 ,  5 ,  232, 125, 
93,  126, 126, 69,  0,   0,   1 ,  0,   2,   2,   34, 
6,   0,   5,   232, 125, 93,  126, 126, 69,  0,   0 ,
1,   0,  2,   2,   34,  6,   0,   5,   232, 125, 93, 
126, 126, 69,  0,   0

第二个流:

69,  0,   0 , 1,   0,  2,   2,   34,  6,   0, 126

和代码:

    List<byte> lastBuf = new List<byte>();

    List<byte[]> Extract(byte[] data, byte delim)
    {
        List<byte[]> result = new List<byte[]>();

        for (int i = 0; i < data.Length; i++)
        {
            if (lastBuf.Count > 0)
            {
                if(data[i] == delim)
                {
                    result.Add(lastBuf.ToArray());
                    lastBuf.Clear();
                }
                else
                {
                    lastBuf.Add(data[i]);
                }
            }
            else 
            { 
                if(data[i] != 126)
                {
                    lastBuf.Add(data[i]);
                }
            }
        }

        return result;
    }

结果: 数据结果


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接