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

4

11 回答 11

24

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

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

于 2009-01-03T16:05:17.327 回答
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)
于 2009-01-03T16:06:46.247 回答
4

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

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

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

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

于 2009-01-03T16:05:01.647 回答
3

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

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

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

于 2009-01-03T16:06:06.293 回答
2

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

于 2009-01-03T16:11:44.113 回答
1

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

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

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

于 2009-01-03T16:09:14.090 回答
1

这就是我从 ac 程序员到 ac# 程序员的解决方法。我喜欢使用 MemoryStream 将其转换为流,然后使用 BinaryReader 来分解二进制数据块。必须添加两个辅助函数才能从网络顺序转换为小端序。也用于构建一个 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);
  }
于 2009-01-03T18:16:54.987 回答
0

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

于 2009-01-03T16:03:43.383 回答
0

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

TCPHeader tcp = Parse(packet.Skip(20));
于 2009-01-03T16:06:22.643 回答
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 );
}
于 2009-01-03T16:42:03.803 回答
0

一些回答的人

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

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

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

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

于 2014-04-28T16:17:33.553 回答