通过套接字[TCP]传输数据 如何在C / C ++中打包多个整数并使用send() recv()传输数据

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

本文关键字:数据 传输 recv 包多个 整数 send TCP 套接字      更新时间:2023-10-16

我正在制作一个基于客户端/服务器的小型游戏,在 c/c++ 的 linux 上,我需要将玩家转移到服务器。

这是我的问题。

我想向服务器发送两个整数,有时它工作得很好,但有时服务器在第一个 recv() 中同时收到整数并卡住。

我知道最好的方法是打包消息。 问题是我不知道语法应该是什么样子。

理论上 ->玩家输入就像一个 int 列 = 4,第二个 int 行 = 1,我将消息打包为 4|1 或类似的东西。然后我从客户端发送到服务器并在服务器上对其进行编码。 一个例子会很棒,或者可能是一些建议如何处理这样的事情。 我对套接字编程仍然很陌生。

这是我的函数的样子:

客户:

#define 缓冲液 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

问题是TCP是一个连续的流,没有"消息">的开始或结束的概念,因为它不是基于消息的。

大多数时候,人们使用一个非常简单的"成帧协议">,即你总是在每次传输时发送一个4字节的标头,告诉收件人要读取多少字节,然后你发送多少字节作为你的消息。

使用htonl()按网络字节顺序发送 4 字节标头,然后您将可以互操作。这里有一个非常相似的例子。

一种可能的解决方案是为客户端发送到服务器的消息定义格式。例如,您可以按如下方式定义协议:

[消息的 4 字节长度][第一个玩家为 2 字节][第二个玩家为 2 字节]在服务器端,您应该首先在 rcv 函数中获取 4 个字节并提取到达消息的长度,并根据接收长度(L)再次调用大小为 L 的 rcv 函数,之后您应该解析收到的消息并提取每个玩家的轮次。

如果所有邮件的长度都预计相同,则不需要邮件头。 下面给出的类似的东西应该可以正常工作。通常,您应该准备好接收少于或超过预期的消息,以及将一条消息拆分到多个接收中。

此外,我推荐一个函数,它接收字节而不假设它们的含义,另一个函数解释它们。然后第一个可以更广泛地应用。

仅将以下内容视为伪代码。 未测试。

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

在许多硬件体系结构上,整数和其他类型具有对齐要求。 编译器通常会处理此问题,但是在缓冲区中时,未对齐的访问可能是一个问题。 此外,服务器和客户端可能不使用相同的字节顺序。

下面是一组内联帮助程序函数,可用于将整数类型打包到缓冲区/从缓冲区打包和解压缩整数类型:

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

打包时,这些值按 2 的补码小端字节顺序排列。

pack_uN()unpack_uN()

处理从 0 到 2 N-1(包括 0 到 2N-1)的无符号整数。

pack_iN()unpack_iN()处理从 -2N-1到 2N-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);
}

如果游戏不是回合制的,你不会像上述函数那样在发送或接收中阻止。

非阻塞 I/O 是必经之路。 从本质上讲,你会有类似的东西

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

当无事可做,并且您想等待一小段时间(几毫秒)但每当可以完成更多 I/O 时中断等待时,请使用

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

要处理到目前为止收到的消息,请使用循环:

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

然后,重新计算游戏状态,更新显示,再交流一些,然后重复。