在C#中处理字节数组

13

我有一个表示完整TCP/IP数据包的字节数组。为了澄清,该字节数组按照以下顺序排列:

(IP头 - 20个字节)(TCP头 - 20个字节)(有效负载 - X个字节)

我有一个名为Parse的函数,接受一个字节数组并返回一个TCPHeader对象。它看起来像这样:

TCPHeader Parse( byte[] buffer );

给定原始字节数组,这是我目前调用此函数的方法。

byte[] tcpbuffer = new byte[ 20 ];
System.Buffer.BlockCopy( packet, 20, tcpbuffer, 0, 20 );
TCPHeader tcp = Parse( tcpbuffer );

有没有一种方便的方法可以将TCP字节数组即完整的TCP/IP数据包的第20-39个字节传递给Parse函数,而不需要先将其提取到一个新的字节数组中?

在C++中,我可以这样做:

TCPHeader tcp = Parse( &packet[ 20 ] );

在C#中有类似的东西吗?如果可能,我想避免创建和随后垃圾回收临时字节数组。


节省时间和精力,使用现有的网络捕获/解析框架,如SharpPcap或Pcap.Net。编写TCP头解析器就像编写解析HTML的Perl脚本一样。已经有许多不同风格的轮子在野外被发现了。 - Evan Plaice
可能重复: https://dev59.com/NHNA5IYBdhLWcg3wL62x - Jon
11个回答

24

一个在.NET框架中常见的做法,也是我建议在此处使用的做法,就是指定偏移量和长度。因此,让您的解析函数还接受在传递数组时的偏移量和要使用的元素数量。

当然,如果像在C++中传递指针一样,应遵循相同的规则——如果您不确定数据何时被使用,那么数组不应被修改,否则可能导致未定义行为。但如果您不再修改该数组,则没有问题。


虽然这解决了问题,但提问者说:“有没有一种方便的方法来传递TCP字节数组...?”。@casperOne的答案似乎更适合这个问题。 - DanielCuadra

22

在这种情况下,我会传递一个 ArraySegment<byte>

您需要将您的Parse方法更改为以下内容:

// Changed TCPHeader to TcpHeader to adhere to public naming conventions.
TcpHeader Parse(ArraySegment<byte> buffer)

然后你需要将调用更改为:

// Create the array segment.
ArraySegment<byte> seg = new ArraySegment<byte>(packet, 20, 20);

// Call parse.
TcpHeader header = Parse(seg);

使用ArraySegment<T>不会复制数组,并且在构造函数中为您执行边界检查(以便您不指定不正确的边界)。然后,您可以更改Parse方法以使用段中指定的边界,这样您就可以了。

您甚至可以创建一个方便的重载,以接受完整的字节数组:

// Accepts full array.
TcpHeader Parse(byte[] buffer)
{
    // Call the overload.
    return Parse(new ArraySegment<byte>(buffer));
}

// Changed TCPHeader to TcpHeader to adhere to public naming conventions.
TcpHeader Parse(ArraySegment<byte> buffer)

ArraySegment<byte> seg = new ArraySegment<byte>(packet, 20, packet.Length-1); 数组段<byte> seg = new ArraySegment<byte>(packet,20,packet.Length-1); - gimel
哎呀!ArraySegment<byte> b2 = new ArraySegment<byte>(b1, 20, b1.Length-20); - gimel
但是...这样不会创建一个新的类供GC收集吗?而提问者想要避免这种情况。 - mafu
@mafu OP想要防止复制字节数组的段; ArraySegment是数组的包装器,它不执行复制。它基本上为您提供了一个对数组的视图,不允许您在这些边界之外工作。 - casperOne
想知道为什么这个答案不受青睐。与上面的答案相比,是否存在性能影响? - Jon
我想补充一点,由于ArraySegment是一个结构体而不是类,所以它可能不会对GC造成麻烦。 - Eilistraee

4
如果可以接受 IEnumerable<byte> 作为输入而不是 byte[],并且您使用的是 C# 3.0,则可以编写以下代码:
tcpbuffer.Skip(20).Take(20);

请注意,这仍然在内部分配枚举器实例,因此您并没有完全避免分配,因此对于少量字节,它可能比分配新数组并将字节复制到其中更慢。
说实话,我不会太担心小临时数组的分配和GC。 .NET垃圾回收环境非常高效地处理这种分配模式,特别是如果数组的生命周期很短,所以除非您已经进行了分析并发现GC成为问题,否则我会按最直观的方式编写代码,并在确定存在性能问题时进行修复。

谢谢,Greg。事实上,我还没有对其进行分析。但是常识告诉我们,分配新数组并复制20个字节比直接使用现有数组效率低。考虑到数据包的数量,我需要尽可能地提高效率。此外,不进行分配和复制看起来更加“整洁”。 - Matt Davis
问题中的数组复制比使用Linq更快。但无论如何它并没有解决创建数组副本的问题。 - mafu
然而,我完全同意这样的小数组复制不太可能引起问题。毕竟,TCP数据包的大小和数量相对有限。我只在一个程序中遇到过小数组分配的问题,该程序实际上创建了数十亿个微小数组的副本,但除非问题涉及ISP的TCP记录器,否则我认为这不是这里的情况。 - mafu

3

如果你真的需要这种类型的控制,你需要看一下 C# 的 unsafe 特性。它允许你拥有一个指针并将其固定,以便 GC 不会移动它:

fixed(byte* b = &bytes[20]) {
}

然而,如果没有性能问题,建议不要在使用仅托管代码时采用此方法。您可以像Stream类一样传递偏移量和长度。


2

如果你可以修改parse()方法,将其改为接受处理应该开始的偏移量。

TCPHeader Parse(byte[] buffer, int offset);


1
您可以使用LINQ来做类似以下的事情:
tcpbuffer.Skip(20).Take(20);

但是 System.Buffer.BlockCopy / System.Array.Copy 可能更有效率。


1
这是我从C程序员转变为C#程序员时解决问题的方法。我喜欢使用MemoryStream将其转换为流,然后使用BinaryReader来分解二进制数据块。必须添加两个辅助函数以将网络顺序转换为小端序。此外,要构建一个byte[]以发送,请参见有没有一种方法可以将对象强制转换回其原始类型而不指定每种情况?,其中有一个函数允许从对象数组转换为byte[]。
  Hashtable parse(byte[] buf, int offset )
  {

     Hashtable tcpheader = new Hashtable();

     if(buf.Length < (20+offset)) return tcpheader;

     System.IO.MemoryStream stm = new System.IO.MemoryStream( buf, offset, buf.Length-offset );
     System.IO.BinaryReader rdr = new System.IO.BinaryReader( stm );

     tcpheader["SourcePort"]    = ReadUInt16BigEndian(rdr);
     tcpheader["DestPort"]      = ReadUInt16BigEndian(rdr);
     tcpheader["SeqNum"]        = ReadUInt32BigEndian(rdr);
     tcpheader["AckNum"]        = ReadUInt32BigEndian(rdr);
     tcpheader["Offset"]        = rdr.ReadByte() >> 4;
     tcpheader["Flags"]         = rdr.ReadByte() & 0x3f;
     tcpheader["Window"]        = ReadUInt16BigEndian(rdr);
     tcpheader["Checksum"]      = ReadUInt16BigEndian(rdr);
     tcpheader["UrgentPointer"] = ReadUInt16BigEndian(rdr);

     // ignoring tcp options in header might be dangerous

     return tcpheader;
  } 

  UInt16 ReadUInt16BigEndian(BinaryReader rdr)
  {
     UInt16 res = (UInt16)(rdr.ReadByte());
     res <<= 8;
     res |= rdr.ReadByte();
     return(res);
  }

  UInt32 ReadUInt32BigEndian(BinaryReader rdr)
  {
     UInt32 res = (UInt32)(rdr.ReadByte());
     res <<= 8;
     res |= rdr.ReadByte();
     res <<= 8;
     res |= rdr.ReadByte();
     res <<= 8;
     res |= rdr.ReadByte();
     return(res);
  }

这绝对是一种简单、优雅的方法。我已经为IP、TCP和UDP头定义了类。在内部,我使用BitConverter函数来提取值,并使用IPAddress.NetworkToHostOrder来交换字节顺序。我可能会运行一些测试来看哪种方法更有效。 - Matt Davis
如果你追求性能,你可能想看一下https://dev59.com/3XVD5IYBdhLWcg3wXaYd,并从类切换到结构体。我还建议保持所有数据以网络顺序存储,只在需要时进行转换。 - Rex Logan

0

我认为在C#中你不能做那样的事情。你可以让Parse()函数使用一个偏移量,或者一开始就创建三个字节数组;一个用于IP头,一个用于TCP头,一个用于有效载荷。


我认为更好的解决方案是使用ArraySegment<T>,它可以为您执行边界检查,这样您就不必在每个地方都重复它。 - casperOne

0

没有可验证的代码可以做到这一点。如果您的解析方法可以处理IEnumerable<byte>,则可以使用LINQ表达式。

TCPHeader tcp = Parse(packet.Skip(20));

0
为什么不改变思路,创建覆盖缓冲区以从其中提取位的类呢?
// member variables
IPHeader ipHeader = new IPHeader();
TCPHeader tcpHeader = new TCPHeader();

// passing in the buffer, an offset and a length allows you
// to move the header over the buffer
ipHeader.SetBuffer( buffer, 0, 20 );

if( ipHeader.Protocol == TCP )
{
    tcpHeader.SetBuffer( buffer, ipHeader.ProtocolOffset, 20 );
}

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