通过网络发送不同大小数据的最佳实践

Best practice of sending data of different sizes over the network

我想通过 UDP 发送不同大小的数据。要发送的数据大小不固定。我有以下情况:

unsigned char buffer[BUFFERSIZE];
int bytes = fill_buffer(buffer, sizeof(buffer)): // Returns number of filled bytes.
sendto(socket, buffer, bytes, 0, (struct sockaddr *)&server, sizeof(server))

在上面的例子中,接收方不知道要接收多少字节。我也想到了先发送接收的字节数再发送数据。但那样的话,我不知道如果数据包乱序到达会发生什么。

发送方将是

sendto(socket, &bytes, sizeof(bytes), 0, (struct sockaddr *)&server, sizeof(server))
sendto(socket, buffer, bytes, 0, (struct sockaddr *)&server, sizeof(server))

接收方将是

recvfrom(socket, &bytes, sizeof(bytes), 0, NULL, NULL)
recvfrom(socket, buffer, bytes, 0, NULL, NULL)

但是会不会是发送的数据乱码了?

认为 如果添加消息 header.[=23=,您可以在单个数据报中发送 两者 ]

发件人仅发送其拥有的负载数据量。

接收方总是请求 最大 负载大小,但会检查 recvfrom 的 header 和 return 以确定实际长度。


这里有一些粗略的代码可以说明我的想法:

struct header {
    u32 magic_number;
    u32 seq_no;
    u32 msg_type;
    u32 payload_length;
} __attribute__((__packed__));

#define MAXPAYLOAD  1024

struct message {
    struct header info;
    unsigned char payload[MAXPAYLOAD];
} __attribute__((__packed__));

void
sendone(int sockfd,const void *buf,size_t buflen)
{
    struct message msg;
    static u32 seqno = 0;

    memcpy(&msg.payload[0],buf,buflen);
    msg.info.magic_number = 0xDEADADDE;
    msg.info.seq_no = seqno++;
    msg.info.payload_length = buflen;

    sendto(sockfd,&msg,sizeof(struct header) + buflen,...);
}

ssize_t
getone(int sockfd,void *buf,size_t buflen)
{
    struct message msg;
    ssize_t rawlen;
    ssize_t paylen;
    static u32 seqno = 0;

    rawlen = recvfrom(sockfd,&msg,sizeof(struct header) + MAXPAYLOAD,...);

    paylen = msg.info.payload_length;

    if (rawlen != (sizeof(struct header) + paylen))
        // error ...

    memcpy(buf,&msg.payload[0],paylen);

    return paylen;
}

接收方可以检查幻数和序列号以查找损坏或 missing/dropped 数据包等


事实上,使用 sendmsgrecvmsg 可能会提高效率,因为它们允许您使用 scatter/gather 列表发送单个消息。 (即)不必使用消息结构中的 memcpy 复制数据 in/out [您只需要 struct header],因此更接近于零复制缓冲。


另一种选择可能是将 MSG_PEEK 标志与 recvfrom/recvmsg 一起使用。我自己从来没有用过这个,但它会是这样的:

  1. recvmsg 长度为 sizeof(struct header) 带有 MSG_PEEK 标志
  2. 做第二个recvmsg,长度为sizeof(struct header) + msg.info.payload_length

这只是不必总是提供最大大小的缓冲区的好处。由于它涉及 两个 系统调用,因此它可能会慢一点。但是,它可能允许一些技巧,根据消息的类型 and/or length

从池中选择有效负载缓冲区

与基于流的协议 TCP 不同,这意味着对 recv 的调用并不完全对应于对发送的调用,UDP 是基于数据包的,这意味着每个 recvfrom 都完全匹配一个 sendto。这也意味着您需要注意发送的每条消息的大小。

如果您发送的 UDP 数据报大于 IP 数据包中可以包含的数据报,则 UDP 消息将被分成多个 UDP 数据包,从而增加数据丢失的可能性。这是你想要避免的事情。此外,如果您使用的是 IPv6,则在尝试发送时会收到错误消息,因为 IPv6 不支持分段。

相对于您正在做的事情,这意味着什么?这意味着,粗略地说,您的消息不应大于 1450 字节左右,因此您可以将该值用作输入缓冲区的大小。然后可以用recvfrom的return值,看看实际读取了多少字节。如果您的邮件比这大,您应该将它们分成多封邮件。

与任何基于 UDP 的协议一样,您需要考虑消息丢失并需要重新传输的情况,或者消息出现乱序的情况。

其实这个问题的答案很简单。

鉴于:

unsigned char buffer[BUFFERSIZE];
int bytes = fill_buffer(buffer, sizeof(buffer)): // Returns number of filled bytes.
sendto(socket, buffer, bytes, 0, (struct sockaddr *)&server, sizeof(server))

recvfrom 的 return 值告诉我们接收了多少字节,尽管我们进行了全读,

int bytesReceived = recvfrom(socket, buffer, sizeof(buffer), 0, NULL, NULL);
// Process bytesReceived number of bytes in the buffer