Вопрос:
У меня есть небольшое клиентское серверное приложение, в котором я хочу отправить всю структуру через TCP-сокет в C не С++. Предположим, что struct будет следующей:
struct something{ int a; char b[64]; float c; }
Я нашел много сообщений о том, что мне нужно использовать пакет pragma или сериализовать данные перед отправкой и получением.
Мой вопрос: достаточно ли использовать JUST pragma pack или просто сериал? Или мне нужно использовать оба?
Кроме того, поскольку сериализация является процессом с интенсивным процессором, это значительно снижает эффективность вашей работы, поэтому как лучше всего сериализовать структуру БЕЗ использования внешней библиотеки (я бы хотел попробовать образец кода/алгоритма)?
Лучший ответ:
Для переносимости структуры по сети вам потребуется следующее:
-
Упакуйте структуру. Для gcc и совместимых компиляторов выполните это с помощью __attribute__((packed)).
-
Не используйте никакие элементы, кроме целых чисел без знака фиксированного размера, других упакованных структур, удовлетворяющих этим требованиям, или массивов любого из них. Подписанные целые числа тоже ОК, если только ваша машина не использует представление с двумя дополнениями.
-
Решите, будет ли ваш протокол использовать кодировку с большим или большим числом целых чисел. Делайте преобразования при чтении и записи этих целых чисел.
-
Кроме того, не принимают указатели элементов упакованной структуры, кроме тех, которые имеют размер 1 или другие вложенные упакованные структуры. См. этот ответ.
Ниже приведен простой пример кодирования и декодирования. Он предполагает, что доступны функции преобразования порядка байтов hton8(), ntoh8(), hton32() и ntoh32() (первые два являются не-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-разрядных целых чисел соответственно для преобразования в/из большой обратный порядок байт. Тем не менее, я не знаю никакой стандартной функции для 64-битных целых чисел или для преобразования в /from little endian. Вы можете использовать мои функции преобразования байтов; если вы это сделаете, вы должны указать порядок байтов вашего компьютера, указав BADVPN_LITTLE_ENDIAN или BADVPN_BIG_ENDIAN.
Что касается целых чисел со знаком, функции преобразования могут быть реализованы безопасно таким же образом, как и те, которые я написал и связал (напрямую меняя байты); просто замените unsigned на подписанный.
UPDATE: если вы хотите использовать эффективный двоичный протокол, но не любите возиться с байтами, вы можете попробовать что-то вроде Буферы протокола (C-реализация). Это позволяет вам описывать формат ваших сообщений в отдельных файлах и генерировать исходный код, который вы используете для кодирования и декодирования сообщений указанного вами формата. Я также реализовал нечто подобное себе, но значительно упрощен; см. мой генератор BProto и несколько примеров (смотрите .bproto и addr.h для примера использования).
Ответ №1
Перед отправкой любых данных по TCP-соединению определите спецификацию протокола. Это не должен быть многостраничный документ, заполненный техническим жаргоном. Но он должен указать, кто передает то, что и когда он должен указывать все сообщения на уровне байта. Он должен указать, как устанавливаются концы сообщений, есть ли тайм-ауты и кто их навязывает, и т.д.
Без спецификации легко задавать вопросы, на которые просто невозможно ответить. Если что-то пойдет не так, какой конец виноват? Со спецификацией ошибка, не соответствующая спецификации, виновата. (И если оба конца соответствуют спецификации и все еще не работают, спецификация виновата.)
Как только у вас есть спецификация, гораздо легче ответить на вопросы о том, как один конец или другой должен быть разработан.
Я также настоятельно рекомендую не разрабатывать сетевой протокол по специфике вашего оборудования. По крайней мере, не без доказанной проблемы с производительностью.
Ответ №2
Это зависит от того, можете ли вы быть уверены, что ваши системы на обоих концах соединения однородны или нет. Если вы уверены, на все время (что большинство из нас не может быть), вы можете взять несколько ярлыков – но вы должны знать, что они являются ярлыками.
struct something some; … if ((nbytes = write(sockfd, &some, sizeof(some)) != sizeof(some)) …short write or erroneous write…
и аналогичных read().
Однако, если есть вероятность, что системы могут быть разными, вам необходимо установить, как данные будут переданы формально. Вы можете линеаризовать (сериализовать) данные – возможно, с любовью с чем-то вроде ASN.1 или, возможно, более просто с форматом, который можно легко перечитать. Для этого текст часто выгоден – его легче отлаживать, когда вы можете видеть, что происходит не так. В противном случае вам нужно определить порядок байтов, в котором передается int, и убедитесь, что передача соответствует этому порядку, и строка, вероятно, получает количество байтов, за которым следует соответствующий объем данных (подумайте, следует ли передавать терминал null или нет), а затем некоторое представление поплавка. Это более странно. Нелегко писать функции сериализации и десериализации для обработки форматирования. Трудная часть – это разработка (принятие решения) протокола.
Ответ №3
Вы можете использовать union со структурой, которую хотите отправить, и массивом:
union SendSomething { char arr[sizeof(struct something)]; struct something smth; };
Таким образом вы можете отправлять и получать только обр. Конечно, вы должны заботиться о проблемах с endianess, а sizeof(struct something) может различаться в разных машинах (но вы можете легко преодолеть это с помощью #pragma pack).
Ответ №4
Зачем вам это делать, когда есть хорошие и быстрые библиотеки сериализации, такие как Message Pack, которые делают всю тяжелую работу для вас, и в качестве бонуса они предоставляют вам кросс-язычную совместимость вашего протокола сокетов?
Для этого используйте Message Pack или другую библиотеку сериализации.
Ответ №5
Обычно сериализация приносит несколько преимуществ, например, посылая биты структуры по проводу (например, fwrite).
- Это происходит индивидуально для каждой неагрегатной атомной информации (например, int).
- Он точно определяет формат последовательных данных, отправленный по кабелю
- Таким образом, он имеет дело с гетерогенной архитектурой: отправка и получение машин могут иметь разную длину и конечную длину слова.
- Он может быть менее хрупким, если тип немного изменится. Поэтому, если на одной машине установлена старая версия вашего кода, она может разговаривать с машиной с более новой версией, например. один из которых имеет char b[80]; вместо char b[64];
- Он может иметь дело с более сложными структурами данных – переменными размерами векторов или даже хэш-таблицами – логическим способом (для хеш-таблицы, передать ассоциацию,..)
Очень часто генерируются процедуры сериализации. Еще 20 лет назад RPCXDR уже существовал для этой цели, а примитивы сериализации XDR по-прежнему во многих libc.
Ответ №6
Пакет Pragma используется для двоичной совместимости вашей структуры на другом конце.
Поскольку сервер или клиент, которому вы отправляете структуру, могут быть записаны на другом языке или созданы с помощью другого компилятора c или с другими параметрами компилятора c.
Сериализация, как я понимаю, делает поток байтов из вашей структуры. Когда вы пишете структуру в сокете, вы делаете сериализацию.
Ответ №7
Если вам нужна переносимость, тогда вы должны сериализовать каждый элемент отдельно из-за завершения ввода строки и структуры.
Вот пример использования Binn:
binn *obj; // create a new object obj = binn_object(); // add values to it binn_object_set_int32(obj, «id», 123); binn_object_set_str(obj, «name», «Samsung Galaxy Charger»); binn_object_set_double(obj, «price», 12.50); binn_object_set_blob(obj, «picture», picptr, piclen); // send over the network send(sock, binn_ptr(obj), binn_size(obj)); // release the buffer binn_free(obj);
Это всего лишь 2 файла (binn.c и binn.h), поэтому его можно скомпилировать с помощью проекта вместо использования в качестве общей библиотеки.
Возможно, вы также должны использовать фреймворк сообщений (также известный как кадрирование с префиксом длины) в потоках сокетов.
Ответ №8
Протокол Google 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-ch и соответствующий ему payload.pb-cc в вашем каталоге.
Создайте свой файл 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
gcc server.c payload.pb-c.c -lprotobuf-c -o server.out gcc client.c payload.pb-c.c -lprotobuf-c -o client.out