通过TCP套接字接收可变大小的数据

7
我在使用(TCP)套接字传输数据时遇到了一些小问题。以下是我的工作背景:
我正在将数据从A端发送到B端。发送的数据长度可以变化,最大长度为1096字节。
A) send(clientFd, buffer, size, NULL)

在B端,由于我不知道期望的大小是多少,所以我总是尝试接收1096字节:

B) int receivedBytes = receive(fd, msgBuff, 1096, NULL)

然而,当我这样做时:我意识到A正在发送小块数据...大约80-90字节左右。经过几次发送后,B将它们组合在一起,使接收字节数为1096。这显然会破坏数据并导致混乱。为了解决这个问题,我将我的数据分成两部分:头和数据。
struct IpcMsg
{
   long msgType;
   int devId;
   uint32_t senderId;
   uint16_t size; 
   uint8_t value[IPC_VALUES_SIZE]; 
};

在A方面:
A) send(clientFd, buffer, size, NULL)

在B端,我首先接收头部并确定要接收的载荷大小,然后接收其余的载荷。
B) int receivedBytes = receive(fd, msgBuff, sizeof(IpcMsg) - sizeof( ((IpcMsg*)0)->value ), 0);
int sizeToPoll = ((IpcMsg*)buffer)->size;
printf("Size to poll: %d\n", sizeToPoll);

if (sizeToPoll != 0)
{
        bytesRead = recv(clientFd, buffer + receivedBytes, sizeToPoll, 0); 
}

因此,对于每个有有效负载的发送,我最终会调用两次接收。这对我有用,但我想知道有没有更好的做法?


2
TCP是一种流式协议,它将数据作为流发送,这意味着当您接收数据时,您可能会收到少于您请求的数据量。如果您收到的数据量小于预期的消息大小,则必须缓冲接收到的数据并多次调用recv以接收所有数据。 - Some programmer dude
@JoachimPileborg:我不理解哨兵建议。至于另一个建议:您是在建议每个发送请求执行两次发送吗?(附注:我有很多通过的发送,我需要将它们保持最少,因为这会导致滞后/延迟) - brainydexter
1
关于如何发送/接收可变长度的数据,您可以通过在固定大小的“头”中发送实际数据大小,然后接收实际数据;或者您可以使用“哨兵”,一个不能在数据中出现的值,来标记数据的结尾。但是,无论使用哪种方法,都必须对recv进行多次调用,但在现代计算机上,多个send/recv调用的性能惩罚是可以忽略不计的(而TCP可能会将两个连续的send调用的数据放入单个数据包中)。 - Some programmer dude
@JoachimPileborg:你能否给我一个“哨兵”建议的例子?此外,我的理解是否正确——“在固定大小的标头中发送实际数据大小”——我需要进行2个发送调用——a.标头(如您所建议的)和b.实际数据? - brainydexter
1
由于Nagle算法的存在,两个连续的send调用不会引入任何网络“延迟”,因为这两个调用的数据将作为一个单独的数据包发送(如果数据量小于MTU)。而且,如果数据被作为一个单独的数据包发送,无论你进行多少次recv调用,它仍然会从单个数据包中读取。 - Some programmer dude
显示剩余2条评论
2个回答

4
你的想法是正确的,即发送一个包含以下数据基本信息的头信息,然后跟着数据本身。但是这种方式并不总是适用:
int receivedBytes = receive(fd, msgBuff, sizeof(IpcMsg) - sizeof( ((IpcMsg*)0)->value ), 0);
int sizeToPoll = ((IpcMsg*)buffer)->size;

TCP 允许根据其所应用的拥塞控制策略对头部进行分段并将其以尽可能多的块发送,这就是为什么在局域网上,您几乎总是可以在一个数据包中获取到头部,但通过互联网跨越全球时,您可能只会每次获取到较少字节数的原因。
解决方法是不直接调用TCP的“接收”(通常为recv),而是将其抽象为一个小型实用程序函数,该函数接受您需要接收的实际大小和要放入其中的缓冲区。进入循环接收和追加数据包,直到所有数据都到达或发生错误。
如果您需要异步并同时为多个客户端提供服务,则应用相同的原则,但您需要去调用“select”调用,以便在数据到达时得到通知。

2
TCP/IP是用于发送数据的“原始”接口。它确保如果字节被发送,它们都在那里并且顺序正确,但不对分块做任何保证,并且对您要发送的数据一无所知。
因此,如果要通过TCP/IP发送一个要作为“数据包”处理的数据包,必须通过以下技术之一知道何时有一个完整的数据包:
1. 固定大小的数据包。在您的情况下是1096字节。 2. 首先发送/接收已知的“标头”,它将告诉您正在发送的数据包的大小。 3. 使用某种“数据包结束”符号。
在前两种情况中,您知道要接收的字节数,因此需要缓冲接收到的任何内容,直到获得完整的消息,然后进行处理。
如果收到的数据超出了预期,即溢出到下一个数据包中,您将拆分该数据,处理完成的数据包,并保留剩余的缓冲区以供以后处理。
在后一种情况下,如果您有一个数据包结束符号,那么后面的任何内容都应该被缓冲用于下一个数据包。

如何有效地知道实际数据的大小?例如,假设我想将大小为5Mb的图像发送到服务器,如何确保5Mb(约5000000)能够直接适合x字节,以便可以向服务器发出指令:“嘿,接收到的字节数组中的前2个字节始终包含数据的长度”? - Damilola Olowookere
1
最有可能的情况是您会发送一个“头部”消息来指定您即将发送5MB的数据,然后您会发送数据。接收方将接收任意大小的数据块,但会知道为图像分配5MB,并且在字节到达时会知道图像何时已完全发送(或者如果在发送过程中连接失败)。 - CashCow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接