#c #sockets #tcp
#c #сокеты #tcp
Вопрос:
Мне любопытно, как правильно отправлять несколько фрагментов данных произвольной длины через сокет в C. Например, если бы нужно было отправить «имя пользователя» произвольной длины, «тему» произвольной длины и «сообщение» произвольной длины, каким был бы правильный процесс для их отправки. Кроме того, данные, которые я пытаюсь отправить, не обязательно могут заканчиваться нулем, поэтому я не верю, что смогу правильно их собрать, основываясь исключительно на нулевых байтах.
Метод, который я придумал, включал бы чтение первых 4 байт входных данных, полученных на сервере, и интерпретацию их как размера первой части данных, и считывание этого объема данных из сокета и интерпретацию их как первой строки, чтение еще 4 байт и интерпретацию их как длины второй строки, затем чтение ровно такого количества байт и интерпретацию их как второй строки и так далее. Однако, похоже, что это может быть подвержено ошибкам или содержать некоторые детали реализации, которые могут привести к сбоям. Есть ли лучший способ добиться этого?
Ответ №1:
Вы хотите прочитать о сериализации. Несколько ссылок, которые могут быть полезны:
http://en.wikipedia.org/wiki/Serialization
http://en.wikipedia.org/wiki/External_Data_Representation
http://en.wikipedia.org/wiki/Protocol_buffers
И многие другие. Просто выберите свой яд. Или напишите свой собственный, как вы, кажется, пытаетесь, в качестве учебного упражнения.
Комментарии:
1. Существует разница между сериализацией и «обрамлением» сериализованных данных (что в значительной степени рассматривается в вопросе). Обычно это включает длину (и контрольную сумму).
2. Многие библиотеки сериализации также предоставляют фрейминг (например, XDR), в то время как некоторые намеренно этого не делают (например, буферы протокола). Однако его вопрос касается обоих, а не только фрейминга.
Ответ №2:
Ваше предложение — один из способов решения проблемы. Это не «подвержено ошибкам», если сокет использует надежный транспорт, такой как TCP, поскольку сетевой уровень гарантирует, что данные будут доставлены неповрежденными и в правильном порядке. Другим способом сделать это было бы отправить структуры фиксированного размера, чтобы получатель всегда считывал X байт и знал, что он получил полное сообщение. Другой способ заключается в использовании ограничителя поля, либо NUL (как вы предлагаете), либо символа новой строки; это то, что делают многие интернет-протоколы (например, HTTP, FTP). Еще одним является использование метода сериализации (как предполагает другой ответ). Все зависит от того, какого рода данные вы планируете отправлять и насколько переносимыми должны быть данные между различными типами систем.
Ответ №3:
Чтобы избежать того, чтобы ваши процедуры отправки / получения становились безумно сложными по мере усложнения ваших структур данных, я рекомендую разбить проблему на отдельные этапы:
-
Напишите процедуры, которые могут создавать фрейм и отправлять произвольный буфер из N байт по TCP-соединению. (Это включало бы отправку префикса длиной 4 байта, а затем отправку N байтов, как вы описали). Возможно, вы также захотите включить 4-байтовый заголовок type-code, который получатель может использовать для простого определения, какую из ваших структур данных должны представлять полученные байты.
-
Напишите процедуру, которая преобразует (вашу любимую структуру данных) в последовательность из N байтов, хранящихся в оперативной памяти. Затем напишите связанную процедуру, которая де-преобразует серию из N байтов, хранящихся в оперативной памяти, обратно в (вашу любимую структуру данных).
-
Повторите шаг (2) для любых других структур данных, которые вы хотите отправить по сети. Обратите внимание, что при наличии нескольких типов данных поле type-code, упомянутое в (1), облегчит получателю определение того, какую процедуру де-преобразования вызывать после того, как он полностью получит байтовый буфер.
После того, как вы выполнили вышеуказанное, вы можете использовать свой код общего назначения для отправки / получения байтовых буферов из (1) для передачи любой из ваших структур данных из (2), и, таким образом, вам не нужно писать отдельный код отправки / получения для каждой структуры данных, что является большим выигрышем.
Обратите внимание, что если вы беспокоитесь о переносимости, вам нужно обязательно преобразовать любые многобайтовые целочисленные значения или значения с плавающей запятой, которые вы хотите отправить, в big-endian (или little-endian, не имеет значения, главное, чтобы вы были последовательными) перед их отправкой, а затем де-преобразовать их обратно в собственный endian на приемнике после их получения (но прежде чем использовать их для чего-либо). (Вам также нужно будет избежать соблазна просто поместить структуры C memcpy() в байтовый буфер, поскольку разные платформы и даже разные версии компилятора могут по-разному заполнять структуры C в памяти, что приведет к катастрофическим результатам, если ваш отправитель и получатель не будут запускать один и тот же исполняемый файл)