在C#中使用字节数组

我有一个字节数组,代表一个完整的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#中有类似的东西吗? 我想尽可能避免临时字节数组的创建和后续垃圾收集。

您可以在.NET框架中看到并且我建议在此处使用的常见做法是指定偏移量和长度。 因此,使您的Parse函数也接受传递的数组中的偏移量,以及要使用的元素数。

当然,同样的规则也适用于传递像C ++一样的指针 – 不应修改数组,否则如果您不确定何时将使用数据,则可能导致未定义的行为。 但是,如果您不再修改数组,这没有问题。

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

您可以将Parse方法更改为:

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

然后你会改变对此的调用:

 // Create the array segment. ArraySegment seg = new ArraySegment(packet, 20, 20); // Call parse. TcpHeader header = Parse(seg); 

使用ArraySegment将不会复制数组,它将在构造函数中为您执行边界检查(这样您就不会指定不正确的边界)。 然后你改变你的Parse方法来处理段中指定的边界,你应该没问题。

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

 // Accepts full array. TcpHeader Parse(byte[] buffer) { // Call the overload. return Parse(new ArraySegment(buffer)); } // Changed TCPHeader to TcpHeader to adhere to public naming conventions. TcpHeader Parse(ArraySegment buffer) 

如果IEnumerable可以作为输入而不是byte[] ,并且你正在使用C#3.0,那么你可以写:

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

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

不过,我不会过分担心小型临时arrays的分配和GC。 .NET垃圾收集环境在这种类型的分配模式下非常有效,特别是如果数组是短暂的,所以除非你已经分析它并发现GC是一个问题然后我以最直观的方式写它并且当您知道有这些问题时,请修复性能问题。

如果你真的需要这种控制,你必须看看C#的unsafefunction。 它允许你有一个指针并固定它,以便GC不移动它:

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

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

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

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

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

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

这就是我解决它从ac程序员到ac#程序员的问题。 我喜欢使用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); } 

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

没有办法使用可validation的代码来执行此操作。 如果您的Parse方法可以处理IEnumerable ,那么您可以使用LINQ表达式

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

有些人回答

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

做错了。 这是一个很好的解决方案,但代码应如下所示:

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

您应该在主数据包上使用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可能更有效”,或者你的解析函数不能处理IEnumerable ,或者你在发布的问题中更习惯System.Buffer.Block,那么我建议只是让tcpbuffer 不是局部变量,而是私有受保护公共内部静态或not field (换句话说,它应该在您执行发布的代码的方法之外定义和创建)。 因此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之前,它是可选的,但是如果你将它定义为静态,那么你就不能这样做。 相反,你必须输入类名,然后输入点’。’,或者保留它(只需输入字段的名称即可)。

为什么不翻转问题并创建覆盖缓冲区的类来拉出位?

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