4. UDP发送与接收缓冲区大小探测

UDP发送与接收缓冲区大小探测 学习卡片

探测思路:把“发送长度”当作自变量

UDP 的发送缓冲区(SO_SNDBUF)和接收缓冲区(SO_RCVBUF)在内核里是有限的。应用层调用 sendto() 时,数据报先进入发送缓冲区,再由协议栈发出;对端内核收到后放入接收缓冲区,等待应用层 recvfrom() 取走。当单次发送的数据报长度逐步增大,会出现三类可观察现象:

  • 正常:发送返回长度等于请求长度,服务端完整收到同样长度的数据报。
  • 发送侧受限:sendto() 返回 -1,errno 为 EMSGSIZE(数据报超过协议栈允许上限)或 ENOBUFS(缓冲区不足)。
  • 接收侧受限:发送成功,但服务端 recvfrom() 收到的长度小于发送长度(被截断),或返回 MSG_TRUNC 标志。

因此探测的关键不是“能不能发”,而是发送返回值、接收返回值、errno 三者是否一致。下面这张图给出探测过程中数据报在两端缓冲区之间的流转与判读分支。

客户端 应用层 sendto(len) 内核发送缓冲区 SO_SNDBUF 默认约 212992 B sendto 返回值 = len 或 -1 UDP 数据报 保留消息边界 无连接、不重传 超 MTU 触发 IP 分片 分片丢失 → 整包丢弃 服务端 应用层 recvfrom(buf, N) 内核接收缓冲区 SO_RCVBUF 默认约 212992 B recvfrom 返回值 = 实际收到长度 发送 投递 判读: sendto=-1 & EMSGSIZE → 超协议栈上限 sendto=-1 & ENOBUFS → 发送缓冲区不足 recv < send → 接收缓冲区截断 正常路径 send 返回 len,recv 返回 len

探测程序:逐步增大发送长度

下面是一个可直接运行的最小探测程序。客户端从 512 字节开始,每次翻倍,直到 sendto() 失败或达到 65507 字节(UDP 载荷理论上限)。服务端用足够大的缓冲区接收,并打印实际收到的字节数。

// probe_server.c —— 接收端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(void) {
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) { perror("socket"); return 1; }

    // 显式设置接收缓冲区为 256 KB,便于观察截断
    int rcvbuf = 256 * 1024;
    setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9000);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind"); return 1;
    }

    char buf[70000];
    for (;;) {
        struct sockaddr_in cli; socklen_t clen = sizeof(cli);
        ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
                             (struct sockaddr *)&cli, &clen);
        if (n < 0) { perror("recvfrom"); continue; }
        printf("recv %zd bytes from %s:%d\n", n,
               inet_ntoa(cli.sin_addr), ntohs(cli.sin_port));
    }
    return 0;
}
// probe_client.c —— 发送端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(int argc, char **argv) {
    const char *ip = argc > 1 ? argv[1] : "127.0.0.1";
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) { perror("socket"); return 1; }

    // 显式设置发送缓冲区为 256 KB
    int sndbuf = 256 * 1024;
    setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));

    struct sockaddr_in dst = {0};
    dst.sin_family = AF_INET;
    dst.sin_port = htons(9000);
    inet_pton(AF_INET, ip, &dst.sin_addr);

    char *buf = malloc(70000);
    memset(buf, 'A', 70000);

    for (size_t len = 512; len <= 65507; len *= 2) {
        ssize_t n = sendto(fd, buf, len, 0,
                           (struct sockaddr *)&dst, sizeof(dst));
        if (n < 0) {
            printf("len=%zu  sendto FAILED  errno=%d (%s)\n",
                   len, errno, strerror(errno));
            if (errno == EMSGSIZE || errno == ENOBUFS) break;
        } else {
            printf("len=%zu  sendto OK  sent=%zd\n", len, n);
        }
        usleep(100000); // 100 ms 间隔,避免瞬时拥塞
    }
    free(buf);
    close(fd);
    return 0;
}

结果判读与参数对照

在 Linux 上运行上述程序,典型输出如下(示例数据,实际值随内核版本和系统配置变化):

发送长度sendto 返回errno服务端 recvfrom判读
512512—512正常
10241024—1024正常
20482048—2048正常
40964096—4096正常
81928192—8192正常
1638416384—16384正常
3276832768—32768正常
6550765507—65507正常(接近上限)
65508-1EMSGSIZE—超过 UDP 载荷上限
关键观察点:如果服务端 recvfrom() 返回的长度小于客户端 sendto() 的返回值,说明接收缓冲区不足以容纳整个数据报,多余部分被内核丢弃。此时应增大服务端的 SO_RCVBUF,或减小单次发送长度。
注意:SO_SNDBUF 和 SO_RCVBUF 的实际生效值通常是设置值的 2 倍(内核会预留管理开销),可通过 getsockopt() 读回确认。不要假设设置 256 KB 就一定能缓存 256 KB 的净载荷。

快速复查清单

  • 发送端:sendto() 返回 -1 时,先看 errno 是 EMSGSIZE 还是 ENOBUFS。
  • 接收端:recvfrom() 返回值是否等于发送端报告的长度。
  • 缓冲区:用 getsockopt(SO_SNDBUF/SO_RCVBUF) 读回实际生效值。
  • 上限:UDP 应用层载荷最大 65507 字节(IPv4),超过必然 EMSGSIZE。
  • 分片:超过路径 MTU 会触发 IP 分片,任一分片丢失则整包丢弃,表现为接收端收不到。