Должны ли данные полезной нагрузки сетевого пакета быть выровнены по соответствующим границам?


Если у вас есть следующий класс в качестве полезной нагрузки сетевого пакета:

Полезная Нагрузка Класса { char field0; int field1; голец поле2; int field3; };

Оставляет ли использование класса, подобного полезной нагрузке, получателя данных восприимчивым к проблемам выравнивания при получении данных через сокет? Я думаю, что класс должен быть либо переупорядочен, либо добавить отступ для обеспечения выравнивания.

Либо изменить порядок:

class Payload
{
    int  field1;
    int  field3;
    char field0;
    char field2;
};

Или добавить отступ:

class Payload
{
    char  field0;
    char  pad[3];
    int   field1;
    char  field2;
    char  pad[3];
    int   field3; 
};

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

Каков ваш опыт работы с такими проблемами выравнивания сетевых данных?

6 4

6 ответов:

Вы должны посмотреть в буферы протокола Google, или Boost::serialize, как сказал другой плакат.

Если вы хотите свернуть свой собственный, пожалуйста, сделайте это правильно.

Если вы используете типы из stdint.h (то есть: uint32_t, int8_t, и т. д.), и убедитесь, что каждая переменная имеет "собственное выравнивание" (то есть ее адрес делится равномерно на ее размер (int8_ts находятся где угодно, uint16_ts находятся на четных адресах, uint32_ts находятся на адресах, делимых на 4), Вам не придется беспокоиться о выравнивании или упаковке.

При a в предыдущем задании все структуры, передаваемые через нашу базу данных (ethernet или CANbus, byteflight или последовательные порты), были определены в XML. Был парсер, который проверял выравнивание по переменным внутри структур (предупреждая вас, если кто-то написал плохой XML), а затем генерировал заголовочные файлы для различных платформ и языков, чтобы отправлять и получать структуры. Это работало действительно хорошо для нас, мы никогда не должны были беспокоиться о рукописном коде для разбора или упаковки сообщений, и это было гарантировано, что все платформы не будут иметь глупых маленьких ошибок кодирования. Некоторые из наших уровней канала передачи данных были довольно ограничены пропускной способностью, поэтому мы реализовали такие вещи, как bitfields, с помощью парсера, генерирующего соответствующий код для каждой платформы. У нас также были перечисления, что было очень приятно (вы удивитесь, как легко для человека испортить кодирование битовых полей на перечислениях вручную).

Если только вам не нужно беспокоиться о том, что он работает на 8051s и HC11s с C, или через слои каналов передачи данных это очень ограниченная пропускная способность, вы не собираетесь придумать что-то лучше, чем буферы протокола, вы просто потратите много времени, пытаясь быть на одном уровне с ними.

Правильно, слепое игнорирование выравнивания может вызвать проблемы. Даже в одной операционной системе, если 2 компонента были скомпилированы с разными компиляторами или разными версиями компилятора.

Так лучше...
1) передайте ваши данные через некоторый вид процесса сериализации.
2) или передайте каждый из ваших примитивов по отдельности, все еще обращая внимание на порядок байтов = =Endianness

Хорошим местом для начала было бы повысить сериализацию.

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

  1. тщательное определение конкретных типов разрядности на основе среды компиляции(typedef unsigned int uint32_t)
  2. вставка соответствующих прагм, специфичных для компилятора, для указания плотной упаковки элементов структуры
  3. требуя, чтобы все было в одном байтовом порядке (использовать сеть или порядок большого конца)
  4. тщательное написание как серверного, так и клиентского кода
Если вы только начинаете, я бы посоветовал вам пропустить всю эту путаницу, пытаясь представить то, что находится на проводе со структурами. Просто сериализуйте каждый элемент примитива отдельно. Если вы решили не использовать существующую библиотеку, такую как Boost Serialize, или промежуточное программное обеспечение, такое как TibCo, то избавьте себя от головной боли, написав абстракцию вокруг двоичного буфера, который скрывает детали вашего метод сериализации. Стремитесь к такому интерфейсу, как:
class ByteBuffer {
public:
    ByteBuffer(uint8_t *bytes, size_t numBytes) {
        buffer_.assign(&bytes[0], &bytes[numBytes]);
    }
    void encode8Bits(uint8_t n);
    void encode16Bits(uint16_t n);
    //...
    void overwrite8BitsAt(unsigned offset, uint8_t n);
    void overwrite16BitsAt(unsigned offset, uint16_t n);
    //...
    void encodeString(std::string const& s);
    void encodeString(std::wstring const& s);

    uint8_t decode8BitsFrom(unsigned offset) const;
    uint16_t decode16BitsFrom(unsigned offset) const;
    //...
private:
    std::vector<uint8_t> buffer_;
};

Каждый из ваших пакетных классов будет иметь метод для сериализации в ByteBuffer или десериализации из ByteBuffer и смещения. Это одна из тех вещей, которые я абсолютно хочу, чтобы я мог вернуться назад во времени и исправить. Я не могу сосчитать, сколько раз я потратил время на отладку проблемы, вызванной тем, что забыл поменять местами байты или не упаковал struct.

Другая ловушка, которой следует избегать, - это использование union, чтобы представляют собой байт или memcpyть к unsigned буфер случайной работы, чтобы извлечь байт. Если вы всегда используете Big-Endian на проводе, то вы можете использовать простой код для записи байтов в буфер и не беспокоиться о htonl материале:

void ByteBuffer::encode8Bits(uint8_t n) {
    buffer_.push_back(n);
}
void ByteBuffer::encode16Bits(uint16_t n) {
    encode8Bits(uint8_t((n & 0xff00) >> 8));
    encode8Bits(uint8_t((n & 0x00ff)     ));
}
void ByteBuffer::encode32Bits(uint32_t n) {
    encode16Bits(uint16_t((n & 0xffff0000) >> 16));
    encode16Bits(uint16_t((n & 0x0000ffff)      ));
}
void ByteBuffer::encode64Bits(uint64_t n) {
    encode32Bits(uint32_t((n & 0xffffffff00000000) >> 32));
    encode32Bits(uint32_t((n & 0x00000000ffffffff)      ));
}

Это остается хорошим агностиком платформы, так как численное представление всегда логически является большим концом. Этот код также очень хорошо подходит для использования шаблонов, основанных на размере примитивного типа (think encode<sizeof(val)>((unsigned char const*)&val))... не очень красиво, но очень, очень легко писать и поддерживать.

Мой опыт показывает, что следующие подходы должны быть предпочтительными (в порядке предпочтения):

  1. Используйте высокоуровневую платформу, такую как Tibco, CORBA, DCOM или что-то еще, что будет управлять всеми этими проблемами для вас.

  2. Напишите свои собственные библиотеки на обеих сторонах соединения, которые знают о упаковке, порядке байтов и других проблемах.

  3. Общайтесь только с помощью строковых данных.

Попытка отправить необработанные двоичные данные без какого-либо посредничества почти наверняка вызовет массу проблем.

Вы практически не можете использовать класс или структуру для этого, если вам нужна какая-либо переносимость. В вашем примере ints может быть 32-разрядной или 64-разрядной в зависимости от вашей системы. Скорее всего, вы используете маленькую эндианскую машину, но старые Apple Mac-это большие эндианы. Компилятор также может свободно прокладывать, как ему нравится.

В общем случае вам понадобится метод, который записывает каждое поле в буфер по байту за раз, после того, как вы убедитесь, что вы получаете правильный порядок байтов с n2hll, n2hl или n2hs.

Если у вас нет естественного выравнивания в структурах, компиляторы обычно вставляют отступы, чтобы выравнивание было правильным. Однако если вы используете прагмы для "упаковки" структур (удаления обивки), могут возникнуть очень вредные побочные эффекты. На PowerPCs, не выровненные поплавки генерируют исключение. Если вы работаете со встроенной системой, которая не обрабатывает это исключение, вы получите сброс. Если существует процедура для обработки этого прерывания, она может резко замедлиться ваш код, потому что он будет использовать программную процедуру, чтобы обойти рассогласование, которое будет молча калечить вашу производительность.