簡體   English   中英

在C#中使用字節數組

[英]Working with byte arrays in 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<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)

如果IEnumerable<byte>可接受作為輸入而不是byte[] ,並且您使用的是C#3.0,則可以編寫:

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

請注意,這仍然在幕后分配枚舉數實例,因此您不會完全逃避分配,因此對於少量字節,它實際上可能比分配新數組並將字節復制到其中要慢。

老實說,我不會太擔心小型臨時數組的分配和GC。 在這種分配模式下,.NET垃圾收集環境非常有效,尤其是在數組壽命短的情況下,因此,除非您對其進行了分析並發現GC有問題,否則我將以最直觀的方式編寫它,並且當您知道存在性能問題時,請進行修復。

如果您確實需要這些控制,則必須查看C#的unsafe功能。 它允許您擁有一個指針並將其固定,以使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);
  }

有人回答

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

做錯了。 這是一個很好的解決方案,但是代碼應如下所示:

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

您應該在主數據包上使用Skip and 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而不是“跳過並接受”,因為如Steven Robbins回答的那樣,它的性能可能更好:“但是System.Buffer.BlockCopy / System.Array.Copy可能更有效”,或者您可以解析函數無法處理IEnumerable<byte> ,或者您更習慣於已發布的問題中的System.Buffer.Block,那么我建議您使tcpbuffer 不是局部變量,而是使privateprotectedpublicinternalstatic或不是字段 (換句話說,應該在執行您發布的代碼的方法之外定義和創建)。 因此,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之前,它是可選的,但是如果您將其定義為靜態,則不能這樣做。 取而代之的是,您必須鍵入類名稱,然后輸入點“。”,或保留它(只需鍵入字段名稱,僅此而已)。

我認為您無法在C#中執行類似的操作。 您可以使Parse()函數使用偏移量,也可以創建3個字節的數組作為開始。 一種用於IP標頭,一種用於TCP標頭,另一種用於有效負載。

無法使用可驗證的代碼來執行此操作。 如果您的Parse方法可以處理IEnumerable <byte>,則可以使用LINQ表達式

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

為什么不翻轉問題並創建覆蓋緩沖區的類以提取位呢?

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM