C raw UDP socket环回地址下recvfrom()无法接收发送数据问题
Raw UDP Socket 通信故障修复方案
你的代码存在raw socket使用逻辑错误、参数未初始化、协议字段填充错误三类核心问题,以下是具体修改点位和通用编码规范问题。
必须修改的核心点位
客户端(发送端)修复
你当前的核心问题是缓冲区结构和socket行为不匹配:你要求内核自动生成IP头,但却在缓冲区开头预留了IP头的空位,导致内核将你填充的0值区域识别为IP头,直接丢弃非法数据包。
- 移除缓冲区开头预留的IP头空间:不设置
IP_HDRINCL选项时,传给sendto()的缓冲区从第一个字节开始就应该是UDP头,不需要为内核生成的IP头预留位置。因此UDP头指针直接指向缓冲区起始地址,载荷指针紧跟UDP头之后。 - 修正UDP头长度字段:
udphdr->len为UDP头+UDP载荷的总长度,不是整个缓冲区的长度,你当前载荷为4字节,因此应赋值为htons(sizeof(struct udphdr) + 4),必须转网络字节序。 - 移除硬编码的校验和:将
check字段设为0即可,内核会自动计算并填充合法的UDP校验和,硬编码4会导致校验和非法,数据包被网络栈丢弃。
服务端(接收端)修复
你当前的收包逻辑存在参数未初始化、返回值未校验的致命错误,导致recvfrom()调用直接失败:
- 初始化
recvfrom()的长度参数:len是传入传出参数,调用前必须赋值为sizeof(struct sockaddr_in),未初始化时为栈上随机值,会导致recvfrom()直接返回错误。 - 校验
recvfrom()返回值:你设置了非阻塞socket,没有数据包到达时recvfrom()会立刻返回-1并置errno为EAGAIN,必须判断返回值大于0时再解析缓冲区内容,否则会读到全0的无效数据。如果不需要非阻塞逻辑,可以直接移除fcntl设置非阻塞的代码,用阻塞模式接收更简单。 - 建议绑定环回地址:调用
bind()将socket绑定到127.0.0.1,避免收到其他接口的无关流量,也能解决部分内核版本下环回流量不递交给未绑定raw socket的问题。
修复后参考代码
服务端代码
int main() { // 创建raw socket,需要root权限运行 int sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); if (sock == -1) { perror("socket create failed"); return 1; } // 绑定环回地址,仅接收lo口UDP流量 struct sockaddr_in bind_addr = {0}; bind_addr.sin_family = AF_INET; bind_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (bind(sock, (struct sockaddr*)&bind_addr, sizeof(bind_addr)) == -1) { perror("bind failed"); return 1; } // 若需要阻塞模式,删除以下fcntl相关代码即可 int fcntl_ret = fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK); if (fcntl_ret == -1) { perror("fcntl set nonblock failed"); return 1; } char packet[128] = {0}; // raw socket收包时,缓冲区前sizeof(struct iphdr)为内核填充的IP头,之后是UDP头和载荷 struct udphdr *header = (struct udphdr*)(packet + sizeof(struct iphdr)); char *body = packet + sizeof(struct iphdr) + sizeof(struct udphdr); struct sockaddr_in peer_addr; socklen_t addr_len = sizeof(peer_addr); // 必须初始化长度 ssize_t recv_bytes; while (1) { memset(packet, 0, sizeof(packet)); recv_bytes = recvfrom(sock, packet, sizeof(packet), 0, (struct sockaddr*)&peer_addr, &addr_len); if (recv_bytes > 0) { printf("Received %zd bytes\nChecksum: %u\nBody: %s\n", recv_bytes, ntohs(header->check), body); } sleep(1); } close(sock); return 0; }
客户端代码
int main() { // 创建raw socket,需要root权限运行 int sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); if (sock == -1) { perror("socket create failed"); return -1; } struct sockaddr_in dest_addr = {0}; dest_addr.sin_family = AF_INET; dest_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 注意:自行构造UDP头时,sockaddr_in中的sin_port不生效,端口以UDP头中填充的为准 char packet[128] = {0}; // 不设置IP_HDRINCL时,缓冲区直接从UDP头开始,不需要预留IP头空间 struct udphdr *header = (struct udphdr*)packet; char *body = packet + sizeof(struct udphdr); memcpy(body, "test", 4); // 填充UDP头 header->source = htons(25252); header->dest = htons(80); header->len = htons(sizeof(struct udphdr) + 4); // UDP头+载荷总长度,转网络字节序 header->check = 0; // 置0由内核自动计算校验和 ssize_t send_bytes; while (1) { send_bytes = sendto(sock, packet, ntohs(header->len), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr)); if (send_bytes == -1) { perror("send failed"); } else { printf("Sent %zd bytes\n", send_bytes); } sleep(1); } close(sock); return 0; }
通用编码问题
- 缺少错误详情打印:所有系统调用失败时仅打印固定提示,没有通过
perror()输出errno对应的具体错误原因,极大增加调试难度。 - 未校验系统调用返回值:原代码中
sendto()/recvfrom()返回失败时仍会执行后续打印、解析逻辑,导致无效输出和内存访问。 - 协议字段字节序错误:原代码中UDP长度字段直接赋值主机序数值,没有通过
htons()转为网络字节序,导致协议解析错误。 - 资源未释放:socket使用完后没有调用
close()释放,虽然进程退出时内核会回收,但属于不规范写法。 - 硬编码长度值:原代码中多处直接写128作为缓冲区长度,没有用
sizeof()计算,后续修改缓冲区大小时容易出现长度不匹配的问题。 - 缺少权限提示:raw socket创建需要root权限,原代码没有相关提示,普通用户运行时会直接创建socket失败但无法快速定位原因。
注意:两个程序都必须以root权限运行,否则raw socket创建、收发都会失败。
内容的提问来源于stack exchange,提问作者castile
相关产品推荐
相关产品推荐

