Data transfer over sockets[TCP] how to pack multiple integer in c/c++ and transfer the data with send() recv()?

前端 未结 4 920
感情败类
感情败类 2021-01-21 19:17

I\'m making a small client/server based game, on linux in c/c++ and I need to send the player turn to the server.

Here is my problem.

I want to send two integer

4条回答
  •  时光说笑
    2021-01-21 19:47

    On many hardware architectures, integers and other types have alignment requirements. The compiler normally takes care of this, but when in a buffer, unaligned accesses can be an issue. Furthermore, the server and the client might not use the same byte order.

    Here is a set of inline helper functions you can use to pack and unpack integer types to/from a buffer:

    /* SPDX-License-Identifier: CC0-1.0 */
    
    #ifndef   PACKING_H
    #define   PACKING_H
    #include 
    
    /* 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 */
    

    When packed, these values are in two's complement little-endian byte order.

    pack_uN() and unpack_uN() work with unsigned integers from 0 to 2N-1, inclusive.

    pack_iN() and unpack_iN() work with signed integers from -2N-1 to 2N-1-1, inclusive.

    Let's consider a simple binary protocol, where each message starts with two bytes: first one the total length of this message, and the second one identifying the type of the message.

    This has the nice feature that if something odd happens, it is always possible to resynchronize by sending at least 256 zeroes. Each zero is an invalid length for the message, so they should just be skipped by the receiver. You probably won't need this, but it may come in handy someday.

    To receive a message of this form, we can use the following function:

    /* 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];
    }
    

    In your own code, you can use the above like this:

        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 */
        }
    

    where I randomly chose 0 as the abort-game message type, and 4 as the coordinate message type.

    Instead of scattering such statements here and there in your client, put it in a function. You could also consider using a finite-state machine to represent the game state.

    To send messages, you can use a helper function like

    /* 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;
    }
    

    so that sending an abort game (type 0) message would be

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

    and sending a coordinate (type 4) message would be e.g.

    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);
    }
    

    If the game is not turn-based, you won't want to block in the sends or receives, like the above functions do.

    Nonblocking I/O is the way to go. Essentially, you'll have something like

    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 */
    

    and you set the server_fd nonblocking using e.g. fcntl(server_fd, F_SETFL, O_NONBLOCK);.

    A communicator function will try to send and receive as much data as possible. It will return 1 if it sent anything, 2 if it received anything, 3 if both, 0 if neither, and -1 if an error occurred:

    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;
    }
    

    When there is nothing to do, and you want to wait for a short while (some milliseconds), but interrupt the wait whenever more I/O can be done, use

    /* 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();
    }
    

    To process messages received thus far, you use a loop:

        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];
        }
    

    Then you recalculate game state, update the display, communicate some more, and repeat.

提交回复
热议问题