4. UDP发送与接收缓冲区大小探测
探测思路:把“发送长度”当作自变量
UDP 的发送缓冲区(SO_SNDBUF)和接收缓冲区(SO_RCVBUF)在内核里是有限的。应用层调用 sendto() 时,数据报先进入发送缓冲区,再由协议栈发出;对端内核收到后放入接收缓冲区,等待应用层 recvfrom() 取走。当单次发送的数据报长度逐步增大,会出现三类可观察现象:
- 正常:发送返回长度等于请求长度,服务端完整收到同样长度的数据报。
- 发送侧受限:
sendto()返回 -1,errno为EMSGSIZE(数据报超过协议栈允许上限)或ENOBUFS(缓冲区不足)。 - 接收侧受限:发送成功,但服务端
recvfrom()收到的长度小于发送长度(被截断),或返回MSG_TRUNC标志。
因此探测的关键不是“能不能发”,而是发送返回值、接收返回值、errno 三者是否一致。下面这张图给出探测过程中数据报在两端缓冲区之间的流转与判读分支。
探测程序:逐步增大发送长度
下面是一个可直接运行的最小探测程序。客户端从 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 | 判读 |
|---|---|---|---|---|
| 512 | 512 | — | 512 | 正常 |
| 1024 | 1024 | — | 1024 | 正常 |
| 2048 | 2048 | — | 2048 | 正常 |
| 4096 | 4096 | — | 4096 | 正常 |
| 8192 | 8192 | — | 8192 | 正常 |
| 16384 | 16384 | — | 16384 | 正常 |
| 32768 | 32768 | — | 32768 | 正常 |
| 65507 | 65507 | — | 65507 | 正常(接近上限) |
| 65508 | -1 | EMSGSIZE | — | 超过 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 分片,任一分片丢失则整包丢弃,表现为接收端收不到。