Каков наилучший способ определить размер пакета с помощью recv ()? - PullRequest
1 голос
/ 24 января 2020

Чрезвычайно новый для программирования сокетов и C в целом. Я пытаюсь написать базовую c программу для отправки и получения данных между двумя компьютерами. Я понимаю, что recv не получит все ваши данные сразу - вам, по сути, придется l oop, пока он не прочитает все сообщение.

Вместо того, чтобы просто установить ограничение на обеих машинах, Я создал простую Message структуру на стороне клиента:

struct Message {
    size_t length;
    char contents[1024 - sizeof(size_t)];
} message; 
message.length = sizeof(struct Message);
message.contents = information_i_want_to_send;

Когда она прибывает на сервер, я recv считал в буфер: received = recv(ioSock, &buffer, 1024, 0) (что по совпадению имеет тот же размер как моя структура сообщения - но при условии, что это не было ...).

Затем я извлекаю Message.length из буфера следующим образом:

size_t messagelength;
messagelength = *((size_t *) &buffer);

Тогда я l oop recv в буфер, а received < messagelength. Это работает, но я не могу не чувствовать, что это действительно уродливо, и это кажется хакерским (Особенно, если первый вызов recv читает меньше sizeof(size_t) или машины имеют разную битовую архитектуру, в этом случае приведение size_t не будет работать ..). Есть ли лучший способ сделать это?

Ответы [ 2 ]

5 голосов
/ 24 января 2020

У вас есть сообщение фиксированного размера, поэтому вы можете использовать что-то вроде этого:

#include <errno.h>
#include <limits.h>

// Returns the number of bytes read.
// EOF was reached if the number of bytes read is less than requested.
// On error, returns -1 and sets errno.
ssize_t recv_fixed_amount(int sockfd, char *buf, size_t size) {
   if (size > SSIZE_MAX) {
      errno = EINVAL;
      return -1;
   }

   ssize_t bytes_read = 0;
   while (size > 0) {
      ssize_t rv = recv(sockfd, buf, size, 0); 
      if (rv < 0)
         return -1;
      if (rv == 0)
         return bytes_read;

      size -= rv;
      bytes_read += rv;
      buf += rv;
   }

   return bytes_read;
}

Было бы использовано что-то вроде этого:

typedef struct {
   uint32_t length;
   char contents[1020];
} Message;

Message message;

ssize_t bytes_read = recv_fixed_amount(sockfd, &(message.length), sizeof(message.length));
if (bytes_read == 0) {
   printf("EOF reached\n");
   exit(EXIT_SUCCESS);
}

if (bytes_read < 0) {
   perror("recv");
   exit(EXIT_FAILURE);
}

if (bytes_read != sizeof(message.length)) {
   fprintf(stderr, "recv: Premature EOF.\n");
   exit(EXIT_FAILURE);
}

bytes_read = recv_fixed_amount(sockfd, &(message.content), sizeof(message.content));
if (bytes_read < 0) {
   perror("recv");
   exit(EXIT_FAILURE);
}

if (bytes_read != msg_size) {
   fprintf(stderr, "recv: Premature EOF.\n");
   exit(EXIT_FAILURE);
}

Примечания:

  • size_t не будет везде одинаковым, поэтому я переключился на uint32_t.

  • Я читаю поля независимо, потому что заполнение внутри структуры может варьироваться между реализациями. Их также нужно будет отправлять таким образом.

  • Получатель заполняет message.length информацией из потока, но фактически не использует ее.

  • Злонамеренный или ошибочный отправитель может предоставить значение для message.length, которое слишком велико и может sh получатель (или хуже), если он не проверяет его. То же самое касается contents. Он может не заканчиваться NUL, если это ожидается.


Но что, если длина не была фиксированной? Тогда отправителю нужно как-то сообщить, сколько нужно прочитать читателю. Обычный подход - префикс длины.

typedef struct {
   uint32_t length;
   char contents[];
} Message;

uint32_t contents_size;
ssize_t bytes_read = recv_fixed_amount(sockfd, &contents_size, sizeof(contents_size));
if (bytes_read == 0) {
   printf("EOF reached\n");
   exit(EXIT_SUCCESS);
}

if (bytes_read < 0) {
   perror("recv");
   exit(EXIT_FAILURE);
}

if (bytes_read != sizeof(contents_size)) {
   fprintf(stderr, "recv: Premature EOF.\n");
   exit(EXIT_FAILURE);
}

Message *message = malloc(sizeof(Message)+contents_size);
if (!message) {
   perror("malloc");
   exit(EXIT_FAILURE);
}

message->length = contents_size;

bytes_read = recv_fixed_amount(sockfd, &(message->contents), contents_size);
if (bytes_read < 0) {
   perror("recv");
   exit(EXIT_FAILURE);
}

if (bytes_read != contents_size) {
   fprintf(stderr, "recv: Premature EOF.\n");
   exit(EXIT_FAILURE);
}

Примечания:

  • message->length содержит размер message->contents вместо размера структуры. Это гораздо более полезно.

Другой подход заключается в использовании значения часового. Это значение, которое сообщает читателю, что сообщение окончено. Это то, что NUL, который завершает строки C. Это сложнее, потому что вы не знаете, сколько читать заранее. Чтение за байтом слишком дорого, поэтому обычно используется буфер.

 while (1) {
     extend_buffer_if_necessary();
     recv_into_buffer();
     while (buffer_contains_a_sentinel()) {
        // This also shifts the remainder of the buffer's contents.
        extract_contents_of_buffer_up_to_sentinel();
        process_extracted_message();      
     }
 }

Преимущество использования значения Sentinel заключается в том, что ему не нужно заранее знать длину сообщения ( поэтому отправитель может начать отправлять его до того, как он будет полностью создан.)

Недостаток тот же, что и для строк C: сообщение не может содержать значение sentinel, если не используется какой-либо механизм экранирования. Между этим и сложностью читателя вы можете понять, почему префикс длины обычно предпочтительнее значения часового. :)


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

HTTP поддерживает как сообщения с префиксом длины (в форме заголовка Content-Length: <length>), так и этот подход (в форме Transfer-Encoding: chunked header ).

0 голосов
/ 24 января 2020

Есть два способа сделать это ...

1.) Использовать двоичный синхронный протокол. (Использование STX - Начало текста и ETX - Конец текста) для определения начала и конца текста.

2.) Присоедините количество байтов данных, отправляемых в начале данных. Сокет будет считывать это количество байтов и получит количество байтов, которые будут получены от сокета. Затем прочитайте все данные и получите необходимый объем данных.

Хмм ... Кажется, сложно ... ?? Позвольте мне привести пример.

Фактические данные должны быть отправлены: ABCDEFGHIJ

Новый формат данных: 0010ABCDEFGHIJ

Данные, необходимые на стороне сервера: ABCDE

Функция recv будет считывать первые 4 байта, чтобы получить количество байтов фактических данных (в l oop до тех пор, пока не получит 4 байта):

int received1= recv(ioSock, recvbuf, 4, 0);

В соответствии с вышеприведенным случаем, recvbuf будет Если 0010 преобразовать в целое число, то получится значение «10», которое может быть сохранено в некоторой целочисленной переменной. Итак, у нас есть:

int toReadVal = 10

Теперь все, что нам нужно, это прочитать эти 10 цифр в следующем вызове recv:

int received= recv(ioSock, recvbuf1, toReadVal, 0);

Наконец, мы получаем значение recvbuf1 как ABCDEFGHIG. Теперь вы можете усечь значение согласно вашему требованию.

...