Передача данных через сокеты [TCP], как упаковать несколько целых чисел в c / c ++ и передать данные с помощью send () recv ()? - PullRequest
2 голосов
/ 13 июля 2020

Я делаю небольшую клиент-серверную игру на linux в c / c ++, и мне нужно отправить ход игрока на сервер.

Вот моя проблема.

Я хочу отправить два целых числа на сервер, и иногда он работает отлично, но иногда сервер получает как целое число в первом recv (), так и его зависание.

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

Теоретически -> ввод игрока будет похож на int column = 4 и второй int row = 1, а я пакет сообщение как 4 | 1 или что-то вроде этого. Затем я отправляю от клиента к серверу и кодирую его на сервере. Пример был бы отличным или, может быть, несколько советов, как, вероятно, обрабатывать подобные вещи. Я все еще новичок в программировании сокетов.

Вот как выглядит моя функция:

Клиент:

#define BUFFER 512

void send_turn_to_server(int sock, int row, int column)
{
   // sends  row to server from player turn

   char char_row[BUFFER];
   sprintf(char_row, "%d", row);
   char *message_from_client = char_row;
   int len, bytes_sent_row;
   len = strlen(message_from_client);

   if (sendall(sock, message_from_client, &len) == -1)
   {
      perror("sendall");
      printf("We only sent %d bytes because of the error!\n", len);
   }


   char char_column[BUFFER];
   int bytes_sent_column;
   //sends column from player turn
   //sprintf converts the int to char
   sprintf(char_column, "%d", column);
   char *column_from_client = char_column;
   len = strlen(column_from_client);

   if (sendall(sock, column_from_client, &len) == -1)
   {
      perror("sendall");
      printf("We only sent %d bytes because of the error!\n", len);
   }
   cout << "send_turn_to_server_complete" << endl;
}

Здесь я использую функцию из Руководства Beej по сетевому программированию, поэтому я могу быть уверен, что будет отправлен весь буфер.

Клиент:

int sendall(int s, char *buf, int *len)
{
   int total = 0;        // how many bytes we've sent
   int bytesleft = *len; // how many we have left to send
   int n;

   while (total < *len)
   {
      n = send(s, buf + total, bytesleft, 0);
      if (n == -1)
      {
         break;
      }
      total += n;
      bytesleft -= n;
   }

   *len = total; // return number actually sent here

   return n == -1 ? -1 : 0; // return -1 on failure, 0 on success
}

Сервер:

int receive_player_turn(int sock, int &int_row, int &int_column)
{
   int byte_count;
   char buf[BUFFER];
   byte_count = recv(sock, buf, sizeof buf, 0);
   cout << "The row from player: " << buf << endl;
   //The C library function int atoi(const char *str) converts the string argument str to an integer (type int).
   int_row = atoi(buf);
   //cleans the buffer
   bzero(buf, sizeof(buf));

   byte_count = recv(sock, buf, sizeof buf, 0);
   cout << buf << endl;
   cout << "The column from player: " << buf << endl;

   //converts the char string to an int
   int_column = atoi(buf);

   cout << endl

        << "receive player turn worked" << endl
        << "players turn was in the row " << int_row << " and in the column " << int_column + 1 << endl;

   return int_row, int_column;
}

правильный вывод с сервера:

Player connected: SchleichsSalaticus
The row from player: 7
4
The column from player: 4

receive player turn worked
players turn was in the row 7 and in the column 5  
7 4

неверный вывод с сервера:

Player connected: SchleichsSalaticus
The row from player: 74

Ответы [ 4 ]

2 голосов
/ 13 июля 2020

Проблема в том, что TCP является непрерывным потоком , без понятия начала или конца «сообщения» , потому что он не основан на сообщениях.

В большинстве случаев люди используют очень простой «протокол кадрирования» , при котором вы всегда отправляете 4-байтовый заголовок при каждой передаче, который сообщает получателю, сколько байтов нужно прочитать, затем вы отправляете это количество байтов в качестве своего сообщение.

Используйте htonl() для отправки 4-байтового заголовка в сетевом порядке байтов, тогда вы будете совместимы.

1 голос
/ 14 июля 2020

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

Вот набор встроенных вспомогательных функций, которые вы можете использовать для упаковки и распаковки целочисленных типов в / из буфера:

/* SPDX-License-Identifier: CC0-1.0 */

#ifndef   PACKING_H
#define   PACKING_H
#include <stdint.h>

/* Packing and unpacking unsigned and signed integers in
   little-endian byte order.
   Works on all architectures and OSes when compiled
   using a standards-conforming C implementation, C99 or later.
*/

static inline void pack_u8(unsigned char *dst, uint8_t val)
{
    dst[0] =  val & 255;
}

static inline void pack_u16(unsigned char *dst, uint16_t val)
{
    dst[0] =  val       & 255;
    dst[1] = (val >> 8) & 255;
}

static inline void pack_u24(unsigned char *dst, uint32_t val)
{
    dst[0] =  val        & 255;
    dst[1] = (val >> 8)  & 255;
    dst[2] = (val >> 16) & 255;
}

static inline void pack_u32(unsigned char *dst, uint32_t val)
{
    dst[0] =  val        & 255;
    dst[1] = (val >> 8)  & 255;
    dst[2] = (val >> 16) & 255;
    dst[3] = (val >> 24) & 255;
}

static inline void pack_u40(unsigned char *dst, uint64_t val)
{
    dst[0] =  val        & 255;
    dst[1] = (val >> 8)  & 255;
    dst[2] = (val >> 16) & 255;
    dst[3] = (val >> 24) & 255;
    dst[4] = (val >> 32) & 255;
}

static inline void pack_u48(unsigned char *dst, uint64_t val)
{
    dst[0] =  val        & 255;
    dst[1] = (val >> 8)  & 255;
    dst[2] = (val >> 16) & 255;
    dst[3] = (val >> 24) & 255;
    dst[4] = (val >> 32) & 255;
    dst[5] = (val >> 40) & 255;
}

static inline void pack_u56(unsigned char *dst, uint64_t val)
{
    dst[0] =  val        & 255;
    dst[1] = (val >> 8)  & 255;
    dst[2] = (val >> 16) & 255;
    dst[3] = (val >> 24) & 255;
    dst[4] = (val >> 32) & 255;
    dst[5] = (val >> 40) & 255;
    dst[6] = (val >> 48) & 255;
}

static inline void pack_u64(unsigned char *dst, uint64_t val)
{
    dst[0] =  val        & 255;
    dst[1] = (val >> 8)  & 255;
    dst[2] = (val >> 16) & 255;
    dst[3] = (val >> 24) & 255;
    dst[4] = (val >> 32) & 255;
    dst[5] = (val >> 40) & 255;
    dst[6] = (val >> 48) & 255;
    dst[7] = (val >> 56) & 255;
}

static inline void pack_i8(unsigned char *dst, int8_t val)
{
    pack_u8((uint8_t)val);
}

static inline void pack_i16(unsigned char *dst, int16_t val)
{
    pack_u16((uint16_t)val);
}

static inline void pack_i24(unsigned char *dst, int32_t val)
{
    pack_u24((uint32_t)val);
}

static inline void pack_i32(unsigned char *dst, int32_t val)
{
    pack_u32((uint32_t)val);
}

static inline void pack_i40(unsigned char *dst, int64_t val)
{
    pack_u40((uint64_t)val);
}

static inline void pack_i48(unsigned char *dst, int64_t val)
{
    pack_u48((uint64_t)val);
}

static inline void pack_i56(unsigned char *dst, int64_t val)
{
    pack_u56((uint64_t)val);
}

static inline void pack_i64(unsigned char *dst, int64_t val)
{
    pack_u64((uint64_t)val);
}

static inline uint8_t unpack_u8(const unsigned char *src)
{
    return (uint_fast8_t)(src[0] & 255);
}

static inline uint16_t unpack_u16(const unsigned char *src)
{
    return  (uint_fast16_t)(src[0] & 255)
         | ((uint_fast16_t)(src[1] & 255) << 8);
}

static inline uint32_t unpack_u24(const unsigned char *src)
{
    return  (uint_fast32_t)(src[0] & 255)
         | ((uint_fast32_t)(src[1] & 255) << 8)
         | ((uint_fast32_t)(src[2] & 255) << 16);
}

static inline uint32_t unpack_u32(const unsigned char *src)
{
    return  (uint_fast32_t)(src[0] & 255)
         | ((uint_fast32_t)(src[1] & 255) << 8)
         | ((uint_fast32_t)(src[2] & 255) << 16)
         | ((uint_fast32_t)(src[3] & 255) << 24);
}

static inline uint64_t unpack_u40(const unsigned char *src)
{
    return  (uint_fast64_t)(src[0] & 255)
         | ((uint_fast64_t)(src[1] & 255) << 8)
         | ((uint_fast64_t)(src[2] & 255) << 16)
         | ((uint_fast64_t)(src[3] & 255) << 24)
         | ((uint_fast64_t)(src[4] & 255) << 32);
}

static inline uint64_t unpack_u48(const unsigned char *src)
{
    return  (uint_fast64_t)(src[0] & 255)
         | ((uint_fast64_t)(src[1] & 255) << 8)
         | ((uint_fast64_t)(src[2] & 255) << 16)
         | ((uint_fast64_t)(src[3] & 255) << 24)
         | ((uint_fast64_t)(src[4] & 255) << 32)
         | ((uint_fast64_t)(src[5] & 255) << 40);
}

static inline uint64_t unpack_u56(const unsigned char *src)
{
    return  (uint_fast64_t)(src[0] & 255)
         | ((uint_fast64_t)(src[1] & 255) << 8)
         | ((uint_fast64_t)(src[2] & 255) << 16)
         | ((uint_fast64_t)(src[3] & 255) << 24)
         | ((uint_fast64_t)(src[4] & 255) << 32)
         | ((uint_fast64_t)(src[5] & 255) << 40)
         | ((uint_fast64_t)(src[6] & 255) << 48);
}

static inline uint64_t unpack_u64(const unsigned char *src)
{
    return  (uint_fast64_t)(src[0] & 255)
         | ((uint_fast64_t)(src[1] & 255) << 8)
         | ((uint_fast64_t)(src[2] & 255) << 16)
         | ((uint_fast64_t)(src[3] & 255) << 24)
         | ((uint_fast64_t)(src[4] & 255) << 32)
         | ((uint_fast64_t)(src[5] & 255) << 40)
         | ((uint_fast64_t)(src[6] & 255) << 48)
         | ((uint_fast64_t)(src[7] & 255) << 56);
}

static inline int8_t unpack_i8(const unsigned char *src)
{
    return (int8_t)(src[0] & 255);
}

static inline int16_t unpack_i16(const unsigned char *src)
{
    return (int16_t)unpack_u16(src);
}

static inline int32_t unpack_i24(const unsigned char *src)
{
    uint_fast32_t u = unpack_u24(src);
    /* Sign extend to 32 bits */
    if (u & 0x800000)
        u |= 0xFF000000;
    return (int32_t)u;
}

static inline int32_t unpack_i32(const unsigned char *src)
{
    return (int32_t)unpack_u32(src);
}

static inline int64_t unpack_i40(const unsigned char *src)
{
    uint_fast64_t u = unpack_u40(src);
    /* Sign extend to 64 bits */
    if (u & UINT64_C(0x0000008000000000))
        u |= UINT64_C(0xFFFFFF0000000000);
    return (int64_t)u;
}

static inline int64_t unpack_i48(const unsigned char *src)
{
    uint_fast64_t u = unpack_i48(src);
    /* Sign extend to 64 bits */
    if (u & UINT64_C(0x0000800000000000))
        u |= UINT64_C(0xFFFF000000000000);
    return (int64_t)u;
}

static inline int64_t unpack_i56(const unsigned char *src)
{
    uint_fast64_t u = unpack_u56(src);
    /* Sign extend to 64 bits */
    if (u & UINT64_C(0x0080000000000000))
        u |= UINT64_C(0xFF00000000000000);
    return (int64_t)u;
}

static inline int64_t unpack_i64(const unsigned char *src)
{
    return (int64_t)unpack_u64(src);
}

#endif /* PACKING_H */

В упакованном виде эти значения находятся в порядке следования байтов с прямым порядком байтов дополнения до двух.

pack_uN() и unpack_uN() работают с целыми числами без знака от 0 до 2 N -1 включительно .

pack_iN() и unpack_iN() работают с целыми числами со знаком от -2 N-1 до 2 N-1 -1 включительно.

Давайте рассмотрим простой двоичный протокол, в котором каждое сообщение начинается с двух байтов: первый - это общая длина этого сообщения, а второй, определяющий тип сообщения.

У этого есть замечательная особенность: если происходит что-то странное, всегда можно выполнить повторную синхронизацию, отправив не менее 256 нулей. Каждый ноль является недопустимой длиной для сообщения, поэтому получатель должен просто пропустить их. Возможно, вам это не понадобится, но когда-нибудь это может пригодиться.

Чтобы получить сообщение этой формы, мы можем использовать следующую функцию:

/* Receive a single message.
   'fd' is the socket descriptor, and
   'msg' is a buffer of at least 255 chars.
   Returns -1 with errno set if an error occurs,
   or the message type (0 to 255, inclusive) if success.
*/
int recv_message(const int fd, unsigned char *msg)
{
    ssize_t  n;

    msg[0] = 0;
    msg[1] = 0;

    /* Loop to skip zero bytes. */
    do {

        do {
            n = read(fd, msg, 1);
        } while (n == -1 && errno == EINTR);
        if (n == -1) {
            /* Error; errno already set. */
            return -1;
        } else
        if (n == 0) {
            /* Other end closed the socket. */
            errno = EPIPE;
            return -1;
        } else
        if (n != 1) {
            errno = EIO;
            return -1;
        }

    } while (msg[0] == 0);

    /* Read the rest of the message. */        
    {
        unsigned char *const end = msg + msg[0];
        unsigned char       *ptr = msg + 1;

        while (ptr < end) {
            n = read(fd, ptr, (size_t)(end - ptr));
            if (n > 0) {
                ptr += n;
            } else
            if (n == 0) {
                /* Other end closed socket */
                errno = EPIPE;
                return -1;
            } else
            if (n != -1) {
                errno = EIO;
                return -1;
            } else
            if (errno != EINTR) {
                /* Error; errno already set */
                return -1;
            }
        }
    }

    /* Success, return message type. */
    return msg[1];
}

В вашем собственном коде , вы можете использовать приведенное выше, например:

    unsigned char buffer[256];

    switch(receive_message(fd, buffer)) {
    case -1:
        if (errno == EPIPE) {
            /* The other end closed the connection */
        } else {
            /* Other error; see strerror(errno). */
        }
        break or return or abort;

    case 0: /* Exit/cancel game */
        break or return or abort;

    case 4: /* Coordinate message */
        int x = unpack_i16(buffer + 2);
        int y = unpack_i16(buffer + 4);
        
        /* x,y is the coordinate pair; do something */

        break;

    default:
        /* Ignore all other message types */
    }

, где я случайно выбрал 0 в качестве типа сообщения об отмене игры и 4 в качестве типа сообщения с координатами.

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

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

/* Send one or more messages; does not verify contents.
   Returns 0 if success, -1 with errno set if an error occurs.
*/
int send_message(const int fd, const void *msg, const size_t len)
{
    const unsigned char *const end = (const unsigned char *)msg + len;
    const unsigned char       *ptr = (const unsigned char *)msg;
    ssize_t                    n;

    while (ptr < end) {
        n = write(fd, ptr, (size_t)(end - ptr));
        if (n > 0) {
            ptr += n;

        } else
        if (n != -1) {
            /* C library bug, should not occur */
            errno = EIO;
            return -1;
        } else
        if (errno != EINTR) {
            /* Other error */
            return -1;
        }
    }

    return 0;
}

, чтобы отправка сообщения об отмене игры (тип 0) будет иметь вид

int send_abort_message(const int fd)
{
    unsigned char buffer[2] = { 1, 0 };
    return send_message(fd, buffer, 2);
}

, а отправка сообщения с координатами (тип 4) будет выглядеть, например,

int send_coordinates(const int fd, const int x, const int y)
{
    unsigned char buffer[2 + 2 + 2];
    buffer[0] = 6;  /* Length in bytes/chars */
    buffer[1] = 4;  /* Type */
    pack_i16(buffer + 2, x);
    pack_i16(buffer + 4, y);
    return send_message(fd, buffer, 6);
}

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

Неблокирующий ввод-вывод - это путь к go. По сути, у вас будет что-то вроде

static int            server_fd = -1;

static size_t         send_size = 0;
static unsigned char *send_data = NULL;
static size_t         send_next = 0;    /* First unsent byte */
static size_t         send_ends = 0;    /* End of buffered data */

static size_t         recv_size = 0;
static unsigned char *recv_data = NULL;
static size_t         recv_next = 0;    /* Start of next message */
static size_t         recv_ends = 0;    /* End of buffered data */

, и вы установите server_fd неблокирование, используя, например, fcntl(server_fd, F_SETFL, O_NONBLOCK);.

Функция коммуникатора попытается отправить и получить столько данных, сколько возможно. Он вернет 1, если что-то отправил, 2, если что-то получил, 3, если оба, 0, если ни то, ни другое, и -1, если произошла ошибка:

int communicate(void) {
    int      retval = 0;
    ssize_t  n;

    while (send_next < send_ends) {
        n = write(server_fd, send_data + send_next, send_ends - send_next);
        if (n > 0) {
            send_next += n;
            retval |= 1;
        } else
        if (n != -1) {
            /* errno already set */
            return -1;
        } else
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            /* Cannot send more without blocking */
            break;
        } else
        if (errno != EINTR) {
            /* Error, errno set */
            return -1;
        }
    }

    /* If send buffer became empty, reset it. */
    if (send_next >= send_ends) {
        send_next = 0;
        send_ends = 0;
    }

    /* If receive buffer is empty, reset it. */
    if (recv_next >= recv_ends) {
        recv_next = 0;
        recv_ends = 0;
    }

    /* Receive loop. */
    while (1) {

        /* Receive buffer full? */
        if (recv_ends + 256 > recv_ends) {
            /* First try to repack. */
            if (recv_next > 0) {
                memmove(recv_data, recv_data + recv_next, recv_ends - recv_next);
                recv_ends -= recv_next;
                recv_next = 0;
            }
            if (recv_ends + 256 > recv_ends) {
                /* Allocate 16k more (256 messages!) */
                size_t         new_size = recv_size + 16384;
                unsigned char *new_data;
                
                new_data = realloc(recv_data, new_size);
                if (!new_data) {
                    errno = ENOMEM;
                    return -1;
                }
                
                recv_data = new_data;
                recv_size = new_size;
            }
        }

        /* Try to receive incoming data. */
        n = read(server_fd, recv_data + recv_ends, recv_size - recv_ends);
        if (n > 0) {
            recv_ends += n;
            retval |= 2;
        } else
        if (n == 0) {
            /* Other end closed the connection. */
            errno = EPIPE;
            return -1;
        } else
        if (n != -1) {
            errno = EIO;
            return -1;
        } else
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            break;
        } else
        if (errno != EINTR) {
            return -1;
        }
    }

    return retval;
}

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

/* Wait for max 'ms' milliseconds for communication to occur.
   Returns 1 if data received, 2 if sent, 3 if both, 0 if neither
   (having waited for 'ms' milliseconds), or -1 if an error occurs.
*/
int communicate_wait(int ms)
{
    struct pollfd  fds[1];
    int            retval;

    /* Zero timeout is "forever", and we don't want that. */
    if (ms < 1)
        ms = 1;

    /* We try communicating right now. */
    retval = communicate();
    if (retval)
        return retval;

    /* Poll until I/O possible. */
    fds[0].fd = server_fd;
    if (send_ends > send_next)
        fds[0].events = POLLIN | POLLOUT;
    else
        fds[0].events = POLLIN;
    fds[0].revents = 0;
    poll(fds, 1, ms);

    /* We retry I/O now. */
    return communicate();
}

Для обработки сообщений, полученных на данный момент, вы используете al oop:

    while (recv_next < recv_ends && recv_next + recv_data[recv_next] <= recv_ends) {
        if (recv_data[recv_next] == 0) {
            recv_next++;
            continue;
        }

        /* recv_data[recv_next+0] is the length of the message,
           recv_data[recv_next+1] is the type of the message. */

        switch (recv_data[recv_next + 1]) {

        case 4: /* Coordinate message */
            if (recv_data[recv_next] >= 6) {
                int  x = unpack_i16(recv_data + recv_next + 2);
                int  y = unpack_i16(recv_data + recv_next + 4);

                /* Do something with x and y ... */

            }
            break;

        /* Handle other message types ... */

        }

        recv_next += recv_data[recv_next];
    }

Затем вы пересчитываете состояние игры, обновляете дисплей, еще раз общаетесь и повторяете.

1 голос
/ 13 июля 2020

Если предполагается, что все ваши сообщения будут иметь одинаковую длину, тогда вам не нужен заголовок сообщения. Что-то вроде приведенного ниже должно работать нормально. В общем, вы должны быть готовы к получению меньшего или большего количества сообщений, чем вы ожидаете, а также к тому, что одно сообщение будет разделено на несколько приемов. они имеют в виду, а другой интерпретирует их. Тогда первый может применяться более широко.

Следующее следует рассматривать только как псевдокод. не тестировался.

   // use a buffer length double of MESSAGE_LENGTH.
   static int offset = 0; // not thread safe.
   // loop to receive a message.
   while(offset < MESSAGE_LENGTH) {
       byte_count = recv(sock, &buf[offset],  (sizeof(buf)-offset), 0);
       if(byte_count > 0) {
          offset += byte_count;
       }
       else {
          // add error handling here. close socket.
          break out of loop
       }      
   }
   //  process buf here, but do not clear it.
   //  received message always starts at buf[0].
   
   if(no receive error above) {
       process_received_message(buf); // 
   }

   // move part of next message (if any) to start of buffer.
   if(offset > MESSAGE_LENGTH) {
        // copy the start of next message to start of buffer.
        // and remember the new offset to avoid overwriting them.
        char* pSrc = &buf[MESSAGE_LENGTH];
        char* pSrcEnd = &buf[offset];
        char* pDest = buf;
        while(pSrc < pSrcEnd){
           *pDest++ = *pSrc++;
        } //or memcpy.    
        offset -= MESSAGE_LENGTH;
   } 
   else {
        offset = 0;    
   }
1 голос
/ 13 июля 2020

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

[длина вашего сообщения 4 байта] [2 байта для первого игрока] [2 байта для второго], а на стороне сервера вы должны сначала в функции rcv получить 4 байта и извлеките длину полученного сообщения и на основе длины приема (L) снова вызовите функцию rcv с размером L, после чего вы должны проанализировать полученные сообщения и извлечь очередь каждого игрока.

...