Работа с байтовыми массивами в C # - PullRequest
13 голосов
/ 03 января 2009

У меня есть байтовый массив, представляющий полный пакет 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 без предварительного извлечения его в новый байтовый массив?

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

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

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

Ответы [ 11 ]

24 голосов
/ 03 января 2009

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

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

22 голосов
/ 03 января 2009

Я бы в этом случае передал 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)
4 голосов
/ 03 января 2009

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

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

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

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

3 голосов
/ 03 января 2009

Если вам действительно нужен такой контроль, вам нужно посмотреть на unsafe особенность C #. Он позволяет вам иметь указатель и прикрепить его так, чтобы GC не перемещал его:

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

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

2 голосов
/ 03 января 2009

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

1 голос
/ 03 января 2009

Вот как я решил эту проблему, превратившись из программиста на c в программиста на c #. Мне нравится использовать MemoryStream для преобразования его в поток, а затем BinaryReader для разбиения двоичного блока данных. Пришлось добавить две вспомогательные функции для преобразования из сетевого порядка в little-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);
  }
1 голос
/ 03 января 2009

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

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

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

0 голосов
/ 28 апреля 2014

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

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, потому что, возможно, он лучше по производительности, как сказал Стивен Роббинс: «Но 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];

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

0 голосов
/ 03 января 2009

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

// 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 );
}
0 голосов
/ 03 января 2009

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

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