在 C# 中使用字节数组

Posted

技术标签:

【中文标题】在 C# 中使用字节数组【英文标题】:Working with byte arrays in C# 【发布时间】:2010-09-29 09:13:33 【问题描述】:

我有一个代表完整 TCP/IP 数据包的字节数组。为了清楚起见,字节数组的顺序如下:

(IP Header - 20 bytes)(TCP Header - 20 bytes)(Payload - X bytes)

我有一个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 脚本。在野外已经可以找到许多不同样式的***。 可能重复:***.com/questions/1055923/… 【参考方案1】:

您可以在 .NET 框架中看到并且我推荐在此处使用的一种常见做法是指定偏移量和长度。所以让你的 Parse 函数也接受传入数组中的偏移量,以及要使用的元素数量。

当然,与在 C++ 中传递指针的规则相同 - 不应修改数组,否则如果不确定何时使用数据,则可能导致未定义的行为。但是,如果您不再要修改数组,这没有问题。

【讨论】:

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

在这种情况下,我会传递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&lt;T&gt; 不会复制数组,它会在构造函数中为您进行边界检查(这样您就不会指定错误的边界)。然后你改变你的 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 seg = new ArraySegment(packet, 20, packet.Length-1); 哎呀! ArraySegment b2 = new ArraySegment(b1, 20, b1.Length-20); 但是...这不是为 GC 创建了一个新的类来收集,而提问者想避免这种情况吗? @mafu OP想要防止复制字节数组的段; ArraySegment 是数组的包装器,它不执行复制。它基本上为您提供了一个不允许您在这些范围之外工作的数组视图。 很想知道为什么这个答案不是首选。与上述答案相比,是否存在性能影响?【参考方案3】:

如果可以接受 IEnumerable&lt;byte&gt; 而不是 byte[] 作为输入,并且您使用的是 C# 3.0,那么您可以编写:

tcpbuffer.Skip(20).Take(20);

请注意,这仍然会在后台分配枚举器实例,因此您不会完全逃避分配,因此对于少量字节,它实际上可能比分配新数组并将字节复制到其中要慢。

老实说,我不会太担心小型临时数组的分配和 GC。 .NET 垃圾收集环境在这种类型的分配模式下非常有效,尤其是在数组寿命很短的情况下,所以除非您对其进行分析并发现 GC 是一个问题,否则我会以最直观的方式编写它并且当你知道你有性能问题时修复它们。

【讨论】:

谢谢,格雷格。事实上,我没有对其进行分析。但是常识说分配新数组并复制 20 个字节的效率低于简单地使用数组。鉴于数据包的数量,我需要尽可能高效。另外,没有分配和复制,它看起来更“整洁”。 问题中完成的数组复制比为此目的使用 Linq 更快。无论如何也解决不了创建数组副本的问题。 但是,我完全同意像这样的小数组副本不太可能导致问题。毕竟,TCP 数据包的大小和数量都比较有限。我只在一个程序中遇到过小数组分配的问题,该程序实际上创建了数十亿个小数组的副本,但除非问题是关于 ISP 的 TCP 记录器,否则我怀疑情况并非如此。【参考方案4】:

如果你真的需要这种控制,你必须看看 C# 的unsafe 特性。它允许你有一个指针并固定它,这样 GC 就不会移动它:

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

但是,如果没有性能问题,则不建议将这种做法用于仅托管代码。您可以像 Stream 类一样传递偏移量和长度。

【讨论】:

【参考方案5】:

如果您可以更改 parse() 方法,请将其更改为接受应该开始处理的偏移量。 TCPHeader Parse(byte[] buffer , int offset);

【讨论】:

【参考方案6】:

您可以使用 LINQ 执行以下操作:

tcpbuffer.Skip(20).Take(20);

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

【讨论】:

【参考方案7】:

这就是我从 c 程序员到 c# 程序员的解决方法。我喜欢使用 MemoryStream 将其转换为流,然后使用 BinaryReader 来分解二进制数据块。必须添加两个辅助函数才能从网络顺序转换为小端序。也用于构建 byte[] 发送见 Is there a way cast an object back to it original type without specifing every case? 具有允许从对象数组转换为 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 来交换字节。我可能会进行一些测试,看看哪种方法更有效。 如果性能是你所追求的,你可能想看看***.com/questions/2871,然后从类切换到结构。我也会按照网络顺序保留所有内容,仅在需要时进行转换。【参考方案8】:

我认为你不能在 C# 中做类似的事情。您可以让 Parse() 函数使用偏移量,或者创建 3 字节数组开始;一个用于 IP Header,一个用于 TCP Header,一个用于 Payload。

【讨论】:

IMO 更好的解决方案是使用 ArraySegment 为您进行边界检查,这样您就不必在任何地方复制它。【参考方案9】:

没有办法使用可验证的代码来做到这一点。如果您的 Parse 方法可以处理 IEnumerable 那么您可以使用 LINQ 表达式

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

【讨论】:

【参考方案10】:

一些回答的人

tcpbuffer.Skip(20).Take(20);

做错了。这是优秀解决方案,但代码应如下所示:

packet.Skip(20).Take(20);

您应该在主 packet 上使用 Skip 和 Take 方法,并且您发布的代码中不应存在 tcpbuffer。你也不必使用System.Buffer.BlockCopy

JaredPar 几乎是正确的,但他忘记了 Take 方法

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

但他没有弄错 tcpbuffer。 您发布的代码的最后一行应如下所示:

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

但是,如果您仍然想使用 System.Buffer.BlockCopy 而不是 Skip and Take,因为 Steven Robbins 回答的性能可能更好:“但是 System.Buffer.BlockCopy / System.Array.Copy 可能更有效” ,或者您的 Parse 函数无法处理IEnumerable&lt;byte&gt;,或者您在发布的问题中更习惯于 System.Buffer.Block,那么我建议您只需 tcpbuffer 不是本地变量,而是 privateprotectedpublicinternal 和 static 或不是 field(换句话说,它应该在您发布的代码执行的 outside 方法中定义和创建)。因此 tcpbuffer 将只创建一次,并且每次传递您在 System.Buffer.BlockCopy 行发布的代码时都会设置他的值(字节)。

这样你的代码看起来像:

class Program

    //Your defined fields, properties, methods, constructors, delegates, events and etc.
    private byte[] tcpbuffer = new byte[20];
    Your unposted method title(arguments/parameters...)
    
    //Your unposted code before your posted code
    //byte[] tcpbuffer = new byte[ 20 ]; No need anymore! this line can be removed.
    System.Buffer.BlockCopy( packet, 20, this.tcpbuffer, 0, 20 );
    TCPHeader tcp = Parse( this.tcpbuffer );
    //Your unposted code after your posted code
    
    //Your defined fields, properties, methods, constructors, delegates, events and etc.

或者只是必要的部分:

private byte[] tcpbuffer = new byte[20];
...

...
        //byte[] tcpbuffer = new byte[ 20 ]; No need anymore! This line can be removed.
        System.Buffer.BlockCopy( packet, 20, this.tcpbuffer, 0, 20 );
        TCPHeader tcp = Parse( this.tcpbuffer );
...

如果你这样做了:

private byte[] tcpbuffer;

相反,您必须在构造函数上添加以下行:

this.tcpbuffer = new byte[20];

tcpbuffer = new byte[20];

您知道您不必在 tcpbuffer 之前键入 this.,它是可选的,但如果您将其定义为静态,那么您不能这样做。相反,您必须输入类名,然后输入点“.”,或者留下它(只需输入字段的名称即可)。

【讨论】:

【参考方案11】:

为什么不解决这个问题并创建覆盖缓冲区以提取位的类?

// 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 );

【讨论】:

以上是关于在 C# 中使用字节数组的主要内容,如果未能解决你的问题,请参考以下文章

C# 使用指针将不同值类型赋值到字节数组中

java和c#的字节数组转换问题

在 C# 中组合两个或多个字节数组的最佳方法

C#如何从字节数组中提取字节?已知起始字节

使用 C# 从字节数组中解码 dtmf

C# 记录麦克风输入并将其存储在字节数组中,而不是本地存储