Передача структуры через сокет TCP (SOCK_STREAM) в C - PullRequest
8 голосов
/ 03 ноября 2011

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

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

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

Мой вопрос:достаточно ли использовать JUST Pragma Pack или просто сериализацию?Или мне нужно использовать оба?

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

Ответы [ 8 ]

15 голосов
/ 04 ноября 2011

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

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

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

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

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

Ниже приведен простой пример кодирования и декодирования.Предполагается, что функции преобразования порядка байтов hton8(), ntoh8(), hton32() и ntoh32() доступны (первые две не используются, но для согласованности).

#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-разрядных целых чисел, соответственно, для преобразования в / из большихобратный порядок байт.Тем не менее, я не знаю какой-либо стандартной функции для 64-битных целых чисел или для преобразования в / из прямым порядком байтов.Вы можете использовать мои функции преобразования порядка байтов ;если вы сделаете это, вы должны указать порядок байтов вашей машины, определив BADVPN_LITTLE_ENDIAN или BADVPN_BIG_ENDIAN.

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

ОБНОВЛЕНИЕ : если вы хотите эффективный двоичный протокол, но не любите возиться с байтами, вы можете попробовать что-то вроде Protocol Buffers ( C реализация ).Это позволяет вам описывать формат ваших сообщений в отдельных файлах и генерирует исходный код, который вы используете для кодирования и декодирования сообщений указанного вами формата.Я тоже реализовал нечто подобное, но значительно упростил;см. мой генератор BProto и некоторые примеры (см. файлы .bproto и addr.h для примера использования).

3 голосов
/ 03 ноября 2011

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

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

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

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

2 голосов
/ 03 ноября 2011

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

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

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

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

1 голос
/ 03 ноября 2011

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

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

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

1 голос
/ 03 ноября 2011

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

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

1 голос
/ 03 ноября 2011

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

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

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

0 голосов
/ 22 января 2019

Google Protocol Buffer предлагает отличное решение этой проблемы. См. Здесь Google Protobol Buffer - C Реализация

Создайте файл .proto на основе структуры вашей полезной нагрузки и сохраните его как payload.proto

syntax="proto3"

message Payload {
     int32 age = 1;
     string name = 2;
} . 

Скомпилируйте файл .proto, используя

protoc --c_out=. payload.proto

Это создаст заголовочный файл payload.pb-c.h и соответствующий ему payload.pb-c.c в вашем каталоге.

Создайте файл server.c и включите заголовочные файлы protobuf-c

#include<stdio.h>
#include"payload.pb.c.h"

int main()
{
   Payload pload = PLOAD__INIT;
   pload.name = "Adam";
   pload.age = 1300000;

   int len = payload__get_packed_size(&pload);

   uint8_t buffer[len];

   payload__pack(&pload, buffer);

   // Now send this buffer to the client via socket. 
}

На принимающей стороне client.c

....
int main()
{
   uint8_t buffer[MAX_SIZE]; // load this buffer with the socket data. 
   size_t buffer_len; // Length of the buffer obtain via read()
   Payload *pload = payload_unpack(NULL, buffer_len, buffer);

   printf("Age : %d Name : %s", pload->age, pload->name);
}

Убедитесь, что вы компилируете свои программы с -lprotobuf-c flag

gcc server.c payload.pb-c.c -lprotobuf-c -o server.out
gcc client.c payload.pb-c.c -lprotobuf-c -o client.out
0 голосов
/ 03 ноября 2011

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

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

...