Работа с байтовыми массивами в С#

У меня есть массив байтов, который представляет полный пакет 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, т.е. байты 20-39 полного пакета TCP/IP, в функцию Parse без предварительного извлечения его в новый массив байтов?

В С++ я мог бы сделать следующее:

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

Есть ли что-то подобное в С#? Я хочу избежать создания и последующей сборки мусора из массива временных байтов, если это возможно.

Ответы

Ответ 1

Общую практику, которую вы можете увидеть в платформе .NET, и которую я рекомендую использовать здесь, указывает смещение и длину. Таким образом, ваша функция Parse также принимает смещение в переданном массиве и количество используемых элементов.

Конечно, те же правила применяются, как если бы вы передавали указатель, как в С++, - массив не должен быть изменен, иначе это может привести к поведению undefined, если вы не уверены, когда именно данные будут используемый. Но это не проблема, если вы больше не собираетесь изменять массив.

Ответ 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<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)

Ответ 3

Если IEnumerable<byte> допустим как вход, а не byte[], и вы используете С# 3.0, вы можете написать:

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

Обратите внимание, что это все еще выделяет экземпляры перечислений под обложками, поэтому вы не избегаете выделения вообще, и поэтому для небольшого количества байтов он может быть на самом деле медленнее, чем выделение нового массива и копирование байтов в него.

Я бы не слишком беспокоился о распределении и GC небольших временных массивов, чтобы быть честным. Собранная сборка мусора .NET чрезвычайно эффективна для этого типа шаблонов распределения, особенно если массивы недолговечны, поэтому, если вы не профилировали ее и не обнаружили, что GC является проблемой, я бы написал ее наиболее интуитивным способом и исправить проблемы с производительностью, когда вы знаете, что у вас есть.

Ответ 4

Если вам действительно нужен такой контроль, вам нужно взглянуть на функцию unsafe на С#. Это позволяет вам иметь указатель и нажимать его так, чтобы GC не двигал его:

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

Однако эта практика не рекомендуется для работы с управляемым только кодом, если нет проблем с производительностью. Вы можете передать смещение и длину, как в классе Stream.

Ответ 5

Если вы можете изменить метод parse(), измените его, чтобы принять смещение, где должна начаться обработка. TCPHeader Parse (байт [] buffer, int offset);

Ответ 6

Вы можете использовать LINQ, чтобы сделать что-то вроде:

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

Но System.Buffer.BlockCopy/System.Array.Copy, вероятно, более эффективны.

Ответ 7

Вот как я решил, что это происходит от программиста c к программисту С#. Мне нравится использовать MemoryStream, чтобы преобразовать его в поток, а затем BinaryReader, чтобы разбить двоичный блок данных. Пришлось добавить две вспомогательные функции для преобразования из сетевого порядка в маленький endian. Также для построения байта [] отправить см. Есть ли способ вернуть объект к исходному типу без указания каждого случая?, который имеет функцию, которая позволяет преобразовывать из массива объектов в байт [].

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

Ответ 8

Я не думаю, что вы можете сделать что-то подобное на С#. Вы можете либо заставить функцию Parse() использовать смещение, либо создать 3 байтовых массива для начала; один для заголовка IP, один для заголовка TCP и один для полезной нагрузки.

Ответ 9

Для этого невозможно использовать проверяемый код. Если ваш метод Parse может иметь дело с IEnumerable <byte> то вы можете использовать выражение LINQ

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

Ответ 10

Почему бы не перевернуть проблему и не создать классы, которые перекрывают буфер, чтобы вытащить биты?

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

Ответ 11

Некоторые люди, которые ответили

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 и Take, потому что, может быть, это лучше в производительности, как Стивен Роббинс ответил: "Но System.Buffer.BlockCopy/System.Array.Copy, вероятно, более эффективен", или ваша функция синтаксического анализа не может работать с IEnumerable<byte>, или вы больше привыкли к System.Buffer.Block в своем размещенном вопросе, тогда я бы рекомендовал просто просто сделать tcpbuffer не локальная, но закрытый или защищенный или общедоступный или внутренний и static или не поле (другими словами, он должен быть определен и создан снаружи, где выполняется ваш опубликованный код). Таким образом, 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;

вместо этого, то вы должны добавить в свой конструктор /s строку:

this.tcpbuffer = new byte[20];

или

tcpbuffer = new byte[20];

Вы знаете, что вам не нужно набирать this. перед tcpbuffer, это необязательно, но если вы определили его как статический, то вы не можете сделать это. Вместо этого вам нужно будет ввести имя класса, а затем точку "." Или оставить его (просто введите имя поля и все).