Работа с массивами байтов в C#
-
03-07-2019 - |
Вопрос
У меня есть массив байтов, который представляет собой полный пакет 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#?Я хочу, если это возможно, избежать создания и последующей сборки мусора временного массива байтов.
Решение
Распространенная практика, которую вы можете увидеть в платформе .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);
Обратите внимание, что при этом экземпляры перечислителя по-прежнему выделяются скрыто, поэтому вы не можете полностью избежать выделения, и поэтому для небольшого количества байтов это может фактически быть медленнее, чем выделение нового массива и копирование байтов в него.
Честно говоря, я бы не стал слишком беспокоиться о распределении и сборке небольших временных массивов.Среда сбора мусора .NET чрезвычайно эффективна при таком типе распределения, особенно если массивы недолговечны, поэтому, если вы не профилировали ее и не обнаружили, что GC является проблемой, я бы написал ее наиболее интуитивно понятным способом и устраняйте проблемы с производительностью, если вы знаете, что они у вас есть.
Если вам действительно нужен такой контроль, вам нужно посмотреть unsafe
особенность C#.Это позволяет вам иметь указатель и закрепить его, чтобы GC не перемещал его:
fixed(byte* b = &bytes[20]) {
}
Однако эта практика не рекомендуется для работы только с управляемым кодом, если нет проблем с производительностью.Вы можете передать смещение и длину, как в Stream
сорт.
Если вы можете изменить метод parse(), измените его, чтобы принять смещение, с которого должна начаться обработка.TCPHeader Parse (буфер байта [], смещение целого числа);
Вы можете использовать LINQ, чтобы сделать что-то вроде:
tcpbuffer.Skip(20).Take(20);
Но System.Buffer.BlockCopy/System.Array.Copy, вероятно, более эффективны.
Вот как я решил эту проблему, пройдя путь от программиста C до программиста C#.Мне нравится использовать 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);
}
Я не думаю, что вы можете сделать что-то подобное на C#.Вы можете либо заставить функцию Parse() использовать смещение, либо создать для начала 3-байтовые массивы;один для заголовка IP, один для заголовка TCP и один для полезной нагрузки.
Для этого невозможно использовать проверяемый код.Если ваш метод Parse может работать с IEnumerable<byte>, вы можете использовать выражение LINQ.
TCPHeader tcp = Parse(packet.Skip(20));
Некоторые люди, ответившие
tcpbuffer.Skip(20).Take(20);
сделал это неправильно.Это отличный решение, но код должен выглядеть так:
packet.Skip(20).Take(20);
Вы должны использовать методы Skip и Take на своем основном пакет, и tcpbuffer не должно существовать в опубликованном вами коде.Также вам не нужно использовать then System.Buffer.BlockCopy
.
ДжаредПар было почти правильно, но он забыл метод 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];
Вы знаете, что вам не нужно печатать этот. перед 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 );
}