В C, если у вас есть определенный тип пакета, вы обычно определяете некоторую структуру и преобразуете char * в указатель на структуру. После этого у вас будет прямой программный доступ ко всем полям данных в сетевом пакете. Вот так :
struct rdp_header {
int version;
char serverId[20];
};
Получив сетевой пакет, вы можете быстро сделать следующее:
char * packet;
// receive packet
rdp_header * pckt = (rdp_header * packet);
printf("Servername : %20.20s\n", pckt.serverId);
Этот метод действительно отлично работает для протоколов на основе UDP и позволяет очень быстро и очень эффективно анализировать и отправлять пакеты с использованием очень небольшого количества кода и тривиальной обработки ошибок (просто проверьте длину пакета). Есть ли аналогичный, столь же быстрый способ в Java сделать то же самое? Или вы вынуждены использовать методы, основанные на потоках?
Он также игнорирует проблемы с заполнением и выравниванием.




Прочтите свой пакет в массив байтов, а затем извлеките из него нужные биты и байты.
Вот пример без обработки исключений:
DatagramSocket s = new DatagramSocket(port);
DatagramPacket p;
byte buffer[] = new byte[4096];
while (true) {
p = new DatagramPacket(buffer, buffer.length);
s.receive(p);
// your packet is now in buffer[];
int version = buffer[0] << 24 + buffer[1] << 16 + buffer[2] < 8 + buffer[3];
byte[] serverId = new byte[20];
System.arraycopy(buffer, 4, serverId, 0, 20);
// and process the rest
}
На практике вы, вероятно, получите вспомогательные функции для извлечения полей данных в порядке сеть из массива байтов, или, как указывает Том в комментариях, вы можете использовать ByteArrayInputStream(), из которого вы можете создать DataInputStream(), у которого есть методы для чтения структурированные данные из потока:
...
while (true) {
p = new DatagramPacket(buffer, buffer.length);
s.receive(p);
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
DataInput di = new DataInputStream(bais);
int version = di.readInt();
byte[] serverId = new byte[20];
di.readFully(serverId);
...
}
Лучше, чем вспомогательные методы, напишите класс, который имеет байтовый ввод в виде поля и читает целые числа с прямым порядком байтов и тому подобное, что имеет смысл на уровне вашего сетевого формата. java.io.DataInputStream был попыткой сделать это для конкретного случая.
Я не верю, что эту технику можно реализовать на Java, если не считать использования JNI и написания обработчика протокола на C. Другой способ реализации описываемой вами техники - это вариантные записи и союзы, которых в Java также нет.
Если бы у вас был контроль над протоколом (это ваш сервер и клиент), вы могли бы использовать сериализованные объекты (включая xml), чтобы получить автоматический (но не настолько эффективный во время выполнения) синтаксический анализ данных, но это все.
В противном случае вы застрянете с синтаксическим анализом потоков или байтовых массивов (которые можно рассматривать как потоки).
Имейте в виду, что описываемая вами методика чрезвычайно подвержена ошибкам и является источником уязвимостей безопасности для любого достаточно интересного протокола, так что это не такая уж большая потеря.
Я написал кое-что, чтобы упростить такую работу. Как и большинство задач, написать инструмент было намного проще, чем пытаться все делать вручную.
Он состоял из двух классов. Вот пример его использования:
// Resulting byte array is 9 bytes long.
byte[] ba = new ByteArrayBuilder()
.writeInt(0xaaaa5555) // 4 bytes
.writeByte(0x55) // 1 byte
.writeShort(0x5A5A) // 2 bytes
.write( (new BitBuilder()) // 2 bytes---0xBA12
.write(3, 5) // 101 (3 bits value of 5)
.write(2, 3) // 11 (2 bits value of 3)
.write(3, 2) // 010 (...)
.write(2, 0) // 00
.write(2, 1) // 01
.write(4, 2) // 0002
).getBytes();
Я написал ByteArrayBuilder, чтобы просто накапливать биты. Я использовал шаблон цепочки методов (просто возвращая "this" из всех методов), чтобы упростить запись нескольких операторов вместе.
Все методы в ByteArrayBuilder были тривиальными, как одна или две строки кода (я просто написал все в поток вывода данных)
Это нужно для создания пакета, но разорвать его на части не должно быть труднее.
Единственный интересный метод в BitBuilder:
public BitBuilder write(int bitCount, int value) {
int bitMask=0xffffffff;
bitMask <<= bitCount; // If bitcount is 4, bitmask is now ffffff00
bitMask = ~bitMask; // and now it's 000000ff, a great mask
bitRegister <<= bitCount; // make room
bitRegister |= (value & bitMask); // or in the value (masked for safety)
bitsWritten += bitCount;
return this;
}
Опять же, логику можно очень легко перевернуть, чтобы прочитать пакет вместо его построения.
изменить: в этом ответе я предложил другой подход, я собираюсь опубликовать его как отдельный ответ, потому что он совершенно другой.
Это альтернативное предложение для ответа, который я оставил выше. Я предлагаю вам рассмотреть возможность его реализации, потому что он будет действовать почти так же, как решение на языке C, где вы можете выбирать поля из пакета по имени.
Вы можете начать это с внешнего текстового файла примерно так:
OneByte, 1
OneBit, .1
TenBits, .10
AlsoTenBits, 1.2
SignedInt, +4
Он может указывать всю структуру пакета, включая поля, которые могут повторяться. Язык может быть настолько простым или сложным, насколько вам нужно -
Вы бы создали такой объект:
new PacketReader packetReader("PacketStructure.txt", byte[] packet);
Ваш конструктор будет перебирать файл PacketStructure.txt и сохранять каждую строку как ключ хэш-таблицы, а точное расположение ее данных (как смещение, так и размер) в качестве данных.
После того, как вы создали объект, передав bitStructure и пакет, вы можете произвольно обращаться к данным с такими простыми операторами, как:
int x=packetReader.getInt("AlsoTenBits");
Также обратите внимание, что этот материал будет намного менее эффективным, чем структура C, но не так сильно, как вы думаете - он все же, вероятно, во много раз эффективнее, чем вам нужно. Если все будет сделано правильно, файл спецификации будет проанализирован только один раз, так что вам потребуется лишь незначительное попадание в один поиск хэша и несколько бинарных операций для каждого значения, которое вы читаете из пакета - совсем неплохо.
Исключение составляют случаи, когда вы анализируете пакеты из высокоскоростного непрерывного потока, и даже в этом случае я сомневаюсь, что быстрая сеть может затопить даже медленный процессор.
Короткий ответ, нет, вы не можете сделать это так легко.
Более длинный ответ: если вы можете использовать объекты Serializable, вы можете подключить свой InputStream к ObjectInputStream и использовать его для десериализации ваших объектов. Однако для этого требуется, чтобы у вас был некоторый контроль над протоколом. Это также будет проще, если вы используете TCP Socket. Если вы используете UDP DatagramSocket, вам нужно будет получить данные из пакета, а затем передать их в ByteArrayInputStream.
Если у вас нет контроля над протоколом, вы можете по-прежнему использовать вышеуказанный метод десериализации, но вам, вероятно, придется реализовать методы readObject() и writeObject(), а не использовать предоставленную вам реализацию по умолчанию. Если вам нужно использовать чужой протокол (скажем, потому что вам нужно взаимодействовать с собственной программой), это, вероятно, самое простое решение, которое вы найдете.
Также помните, что Java внутренне использует UTF-16 для строк, но я не уверен, что он сериализует их таким образом. В любом случае, вы должны быть очень осторожны при передаче строк туда и обратно в программы, отличные от Java.
Посмотрите на библиотеку Javolution и ее структурные классы, они сделают именно то, о чем вы просите. Фактически, у автора есть точный пример использования классов Javolution Struct для управления UDP-пакетами.
Я думаю, что это дерьмовый способ сделать это на C ... Он игнорирует порядок байтов. Я думаю, что этот тип пакетного ввода-вывода лучше всего выполнять на основе потоков или путем выполнения специальных функций сериализации / десериализации для каждого типа пакета (или команды / подпакета, что угодно).