передача структуры через сокет TCP (Sock STREAM) в C

У меня есть небольшое клиентское серверное приложение, в котором я хочу отправить всю структуру через TCP-сокет на C не c++. Предположим, что структура будет следующей:

    struct something{
int a;
char b[64];
float c;
}

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

мой вопрос в том, достаточно ли использовать только pragma pack или просто сериализацию ? Или мне нужно использовать оба?

также с serialzation процессор процесс это делает вашу производительность резко упасть, так что каков наилучший способ сериализации структуры без использования внешней библиотеки (я хотел бы пример кода/algo)?

7 ответов


вам нужно следующее Для переносимой отправки структуры по сети:

  • пакета структуры. Для gcc и совместимых компиляторов сделайте это с помощью __attribute__((packed)).

  • не используйте никакие члены, кроме целых чисел без знака фиксированного размера, других упакованных структур, удовлетворяющих этим требованиям, или массивов любого из первых. Целые числа со знаком тоже в порядке, если ваша машина не использует дополнение two представление.

  • решите, будет ли ваш протокол использовать кодировку чисел с малым или большим концом. Преобразования при чтении и записи этих чисел.

  • и не принимать указатели членов упакованной структуры, за исключением тех, у кого размер 1 или другие вложенные упакованные структуры. См.ответ.

простой пример кодирования и декодирования следующим образом. Он предполагается, что функции преобразования порядка байтов hton8(), ntoh8(), hton32() и ntoh32() доступны (первые два являются no-op, но там для согласованности).

#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>
#include <stdio.h>

// get byte order conversion functions
#include "byteorder.h"

struct packet {
    uint8_t x;
    uint32_t y;
} __attribute__((packed));

static void decode_packet (uint8_t *recv_data, size_t recv_len)
{
    // check size
    if (recv_len < sizeof(struct packet)) {
        fprintf(stderr, "received too little!");
        return;
    }

    // make pointer
    struct packet *recv_packet = (struct packet *)recv_data;

    // fix byte order
    uint8_t x = ntoh8(recv_packet->x);
    uint32_t y = ntoh32(recv_packet->y);

    printf("Decoded: x=%"PRIu8" y=%"PRIu32"\n", x, y);
}

int main (int argc, char *argv[])
{
    // build packet
    struct packet p;
    p.x = hton8(17);
    p.y = hton32(2924);

    // send packet over link....
    // on the other end, get some data (recv_data, recv_len) to decode:
    uint8_t *recv_data = (uint8_t *)&p;
    size_t recv_len = sizeof(p);

    // now decode
    decode_packet(recv_data, recv_len);

    return 0;
}

что касается функций преобразования порядка байтов, ваша система htons()/ntohs() и htonl()/ntohl() может использоваться для 16-и 32-разрядных целых чисел, соответственно, для преобразования в / из big-endian. Однако я не знаю никакой стандартной функции для 64-битных целых чисел или для преобразования в/из little endian. Вы можете использовать мои функции преобразования порядка байтов; если вы это сделаете, вы должны сказать это вашей машины!--42--> порядок байтов путем определения BADVPN_LITTLE_ENDIAN или BADVPN_BIG_ENDIAN.

что касается целых чисел со знаком, функции преобразования могут быть реализованы безопасно так же, как те, которые я написал и связал (непосредственно обмен байтами); просто измените unsigned на signed.

обновление: если вам нужен эффективный двоичный протокол, но не нравится возиться с байтами, вы можете попробовать что-то вроде Протокол Буферы (реализация C). Это позволяет описывать формат сообщений в отдельных файлах и создает исходный код, используемый для кодирования и декодирования сообщений указанного формата. Я также реализовал нечто подобное сам, но значительно упростил; см. мой генератор BProto и примеры (посмотрите в .файлы bproto, и addr.H для использования образец.)


прежде чем отправлять какие-либо данные через TCP-соединение, разработайте спецификацию протокола. Это не должен быть многостраничный документ, заполненный техническим жаргоном. Но он должен указать, кто передает что, когда и он должен указать все сообщения на уровне байтов. В нем следует указать, как устанавливаются окончания сообщений, есть ли тайм-ауты и кто их накладывает и так далее.

без спецификации, легко задать вопросы которые просто невозможны к ответ. Если что-то пойдет не так, в чем виноват? В спецификации виноват конец, который не следовал спецификации. (И если оба конца следуют спецификации, и она по-прежнему не работает, спецификация неисправна.)

Как только у вас есть спецификация, гораздо проще ответить на вопросы о том, как должен быть спроектирован тот или иной конец.

Я также настоятельно рекомендую не проектирование сетевого протокола вокруг специфики вашего аппаратура. По крайней мере, не без доказанной проблемы с производительностью.


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

struct something some;
...
if ((nbytes = write(sockfd, &some, sizeof(some)) != sizeof(some))
    ...short write or erroneous write...

и аналогично read().

однако, если есть вероятность, что системы могут отличаться, то вам нужно установить, как данные будут передаваться формально. Ты вполне можешь ... линеаризовать (сериализовать) данные - возможно, причудливо с чем-то вроде ASN.1 или, возможно, более просто с форматом, который можно легко перечитывать. Для этого текст часто полезен - его легче отлаживать, когда вы можете видеть, что происходит не так. В противном случае вам нужно определить порядок байтов, в котором int передается и убедитесь, что передача следует этому порядку, и строка, вероятно, получает количество байтов, за которым следует соответствующий объем данных (рассмотрим, следует ли передавать terminal null или нет), а затем некоторое представление float. Это более точно. Не так уж сложно написать функции сериализации и десериализации для обработки форматирования. Самое сложное-разработать (принять решение) протокол.


вы могли бы использовать union со структурой, которую вы хотите отправить и массив:

union SendSomething {
    char arr[sizeof(struct something)];
    struct something smth;
};

таким образом, вы можете отправлять и получать только arr. Конечно, вы должны заботиться о проблемах endianess и sizeof(struct something) может отличаться на разных машинах (но вы можете легко преодолеть это с помощью #pragma pack).


зачем вам это делать, когда есть хорошие и быстрые библиотеки сериализации, такие как Пакет что делать всю тяжелую работу для вас и в качестве бонуса они дают вам межъязыковая совместимость протокола сокета?

для этого используйте пакет сообщений или другую библиотеку сериализации.


обычно сериализация приносит несколько преимуществ, например, отправка битов структуры по проводу (например,fwrite).

  1. это происходит индивидуально для каждого неагрегатная атомных данных (напр. int).
  2. он точно определяет формат последовательных данных, отправленных по проводу
  3. таким образом, он имеет дело с гетерогенной архитектурой: отправляющие и получающие машины могут иметь разную длину слова и эндианность.
  4. это может быть менее хрупким, когда тип немного меняется. Поэтому, если на одной машине запущена старая версия вашего кода, она может разговаривать с машиной с более поздней версией, например с char b[80]; вместо char b[64];
  5. он может иметь дело с более сложными структурами данных-векторами переменного размера или даже хэш-таблицами-логическим способом (для хэш-таблицы передайте ассоциацию,..)

очень часто генерируются процедуры сериализации. Еще 20 лет назад RPCXDR уже существовал для этой цели, и примитивы сериализации XDR все еще находятся во многих libc.


Pragma pack используется для двоичной совместимости вашей структуры на другом конце. Потому что сервер или клиент, на который вы отправляете структуру, может быть написан на другом языке или построен с другим компилятором c или с другими параметрами компилятора C.

сериализация, как я понимаю, делает поток байтов из вас struct. Когда вы пишете, вы struct в розетку вы делаете serialiazation.