Linux服务器编程实践45-UDP数据读写:recvfrom与sendto函数的使用实例
1. 前言:UDP协议与数据读写的特殊性
在Linux网络编程中,UDP(用户数据报协议)作为无连接、不可靠的传输层协议,与面向连接的TCP在数据读写方式上有显著差异。TCP通过建立持久连接,使用recv
/send
即可完成数据交互;而UDP因无连接特性,每次通信都需明确指定目标地址,因此内核提供了专门的recvfrom
和sendto
函数处理UDP数据报。
本文将从函数原理、参数解析、使用场景出发,结合完整代码示例,详解UDP数据读写的实现逻辑,并通过JavaScript可视化工具展示数据交互流程,帮助开发者快速掌握UDP编程的核心要点。
注意:UDP不保证数据的有序性和完整性,也不进行重传,因此在需要可靠通信的场景(如文件传输)中需上层协议自行实现确认机制;但UDP的无连接特性使其在低延迟、高并发场景(如实时通信、DNS查询)中性能更优。
2. UDP数据读写核心函数:recvfrom与sendto
Linux内核在sys/socket.h
头文件中定义了recvfrom
和sendto
函数,二者均支持UDP数据报的读写,且可灵活适配IPv4/IPv6地址格式。
2.1 函数原型与参数解析
#include <sys/types.h> #include <sys/socket.h>// 读取UDP数据报 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);// 发送UDP数据报 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
2.2 关键参数详解
参数 | 含义(recvfrom) | 含义(sendto) |
---|---|---|
sockfd | UDP类型的socket文件描述符(通过socket(PF_INET, SOCK_DGRAM, 0) 创建) | |
buf | 接收数据的缓冲区地址(用户空间) | 发送数据的缓冲区地址(用户空间) |
len | 接收缓冲区的最大长度(避免缓冲区溢出) | 发送数据的实际长度(字节数) |
flags | 数据读写的控制标志(常见值:0表示默认行为;MSG_DONTWAIT 表示非阻塞读写;MSG_PEEK 表示窥探数据不清除缓冲区) | |
src_addr | 输出参数,存储发送端的socket地址(IPv4用sockaddr_in ,IPv6用sockaddr_in6 ) | -(无意义,传NULL) |
dest_addr | -(无意义,传NULL) | 输入参数,指定接收端的socket地址 |
addrlen | 输入输出参数,传入时为src_addr 的长度,返回时为实际地址长度 | 输入参数,dest_addr 的固定长度 |
2.3 返回值说明
- 成功:返回实际读写的字节数(
ssize_t
类型,支持负数表示错误) - 失败:返回-1,并设置
errno
(常见错误:EBADF
表示socket无效;EAGAIN
表示非阻塞模式下无数据可读;EINVAL
表示地址格式错误) - 特殊情况:
recvfrom
返回0表示对方关闭连接(UDP无连接,实际极少出现,通常因网络异常导致)
3. 可视化:UDP数据交互流程
以下UDP客户端与服务器的数据流交互图,展示recvfrom
和sendto
的调用时机与数据流向。
从流程图可见,UDP服务器必须先通过bind
绑定端口(让客户端知道目标地址),而客户端可省略bind
(内核自动分配临时端口);每次数据交互时,客户端通过sendto
指定服务器地址,服务器通过recvfrom
获取客户端地址并回复。
4. 实战:UDP回射服务器与客户端实现
下面通过“回射服务”实例(客户端发送数据,服务器原样返回),完整展示recvfrom
和sendto
的使用方式,包含IPv4地址处理、错误处理、非阻塞模式等关键细节。
4.1 UDP服务器实现(echo_server.c)
功能说明:绑定8888端口,接收客户端数据后原样返回,支持多客户端并发(UDP无连接,天然支持并发)
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h>#define PORT 8888 #define BUF_SIZE 1024int main() {// 1. 创建UDP socketint sockfd = socket(PF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket create failed");exit(EXIT_FAILURE);}// 2. 配置服务器地址(IPv4)struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET; // IPv4协议族server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡server_addr.sin_port = htons(PORT); // 端口号(主机字节序转网络字节序)// 3. 绑定地址与端口if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}printf("UDP echo server started, port: %d\n", PORT);// 4. 循环处理客户端请求char buf[BUF_SIZE];struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);while (1) {// 4.1 接收客户端数据(通过src_addr获取客户端地址)memset(buf, 0, BUF_SIZE);ssize_t recv_len = recvfrom(sockfd, buf, BUF_SIZE-1, 0,(struct sockaddr*)&client_addr, &client_addr_len);if (recv_len < 0) {perror("recvfrom failed");continue;}// 打印客户端信息与数据char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);printf("Received from %s:%d, data: %s (len: %ld)\n",client_ip, ntohs(client_addr.sin_port), buf, recv_len);// 4.2 回射数据(通过dest_addr指定客户端地址)ssize_t send_len = sendto(sockfd, buf, recv_len, 0,(struct sockaddr*)&client_addr, client_addr_len);if (send_len < 0) {perror("sendto failed");continue;}printf("Echo to %s:%d, len: %ld\n", client_ip, ntohs(client_addr.sin_port), send_len);}// 5. 关闭socket(实际不会执行,需Ctrl+C终止)close(sockfd);return 0; }
4.2 UDP客户端实现(echo_client.c)
功能说明:接收用户输入,发送到服务器8888端口,接收服务器回射数据并打印
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h>#define BUF_SIZE 1024// 命令行参数:./echo_client [服务器IP] [服务器端口] int main(int argc, char *argv[]) {if (argc != 3) {fprintf(stderr, "Usage: %s \n", argv[0]);exit(EXIT_FAILURE);}const char *server_ip = argv[1];int server_port = atoi(argv[2]);// 1. 创建UDP socketint sockfd = socket(PF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {perror("socket create failed");exit(EXIT_FAILURE);}// 2. 配置服务器地址(IPv4)struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;// IP地址转换(点分十进制字符串转网络字节序)if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <= 0) {perror("invalid server ip");close(sockfd);exit(EXIT_FAILURE);}server_addr.sin_port = htons(server_port);// 3. 与服务器交互(发送用户输入,接收回射数据)char buf[BUF_SIZE];struct sockaddr_in recv_addr;socklen_t recv_addr_len = sizeof(recv_addr);while (1) {// 3.1 读取用户输入printf("Please enter data to send (q to quit): ");fgets(buf, BUF_SIZE-1, stdin);// 退出逻辑(输入'q'或'Q')if (buf[0] == 'q' || buf[0] == 'Q') {printf("Client exiting...\n");break;}// 去除fgets读取的换行符size_t data_len = strlen(buf);if (buf[data_len-1] == '\n') {buf[data_len-1] = '\0';data_len--;}// 3.2 发送数据到服务器ssize_t send_len = sendto(sockfd, buf, data_len, 0,(struct sockaddr*)&server_addr, sizeof(server_addr));if (send_len < 0) {perror("sendto failed");continue;}printf("Sent to server, len: %ld\n", send_len);// 3.3 接收服务器回射数据memset(buf, 0, BUF_SIZE);ssize_t recv_len = recvfrom(sockfd, buf, BUF_SIZE-1, 0,(struct sockaddr*)&recv_addr, &recv_addr_len);if (recv_len < 0) {perror("recvfrom failed");continue;}printf("Received from server: %s (len: %ld)\n\n", buf, recv_len);}// 4. 关闭socketclose(sockfd);return 0; }
5. 关键技术细节与常见问题
5.1 地址转换:主机字节序与网络字节序
不同CPU的字节序(大端/小端)不同,而网络协议规定使用“大端字节序”,因此需通过以下函数转换:
#include <netinet/in.h>// 主机字节序转网络字节序 unsigned long int htonl(unsigned long int hostlong); // 32位(IP地址) unsigned short int htons(unsigned short int hostshort); // 16位(端口号)// 网络字节序转主机字节序 unsigned long int ntohl(unsigned long int netlong); unsigned short int ntohs(unsigned short int netshort);
示例:服务器绑定端口时,server_addr.sin_port = htons(PORT)
将主机字节序的端口号转为网络字节序;客户端打印端口时,ntohs(client_addr.sin_port)
反向转换。
5.2 IPv4与IPv6地址兼容
若需支持IPv6,只需将地址结构体改为sockaddr_in6
,并调整相关参数:
// IPv6地址结构体 struct sockaddr_in6 {sa_family_t sin6_family; // AF_INET6in_port_t sin6_port; // 端口号(网络字节序)uint32_t sin6_flowinfo; // 流信息(通常为0)struct in6_addr sin6_addr; // IPv6地址uint32_t sin6_scope_id; // 作用域ID(本地链路地址需设置) };// IPv6地址转换函数 int inet_pton(AF_INET6, const char *src, void *dst); // 字符串转IPv6 const char *inet_ntop(AF_INET6, const void *src, char *dst, socklen_t size); // IPv6转字符串
5.3 非阻塞模式与超时控制
默认情况下,recvfrom
会阻塞直到有数据可读,可通过fcntl
设置非阻塞模式,或通过setsockopt
设置超时时间:
// 方式1:设置非阻塞模式 int flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);// 方式2:设置接收超时(5秒) struct timeval timeout = {5, 0}; setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
非阻塞模式下,recvfrom
无数据时返回-1并设置errno = EAGAIN
,需通过循环重试或I/O复用(select
/epoll
)优化性能。
5.4 常见错误与解决方案
错误类型 | 原因 | 解决方案 |
---|---|---|
bind: Address already in use | 端口已被占用,或处于TIME_WAIT状态 | 1. 更换端口;2. 设置SO_REUSEADDR 选项:int reuse = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); |
recvfrom: Resource temporarily unavailable | 非阻塞模式下无数据可读(errno = EAGAIN ) | 1. 增加重试逻辑;2. 使用select 监听socket就绪事件 |
inet_pton: Invalid argument | IP地址格式错误(如IPv4地址用AF_INET6解析) | 检查地址族与IP格式匹配,或增加格式校验逻辑 |
6. 扩展:UDP与TCP数据读写对比
为帮助开发者选择合适的传输层协议,以下对比UDP(recvfrom/sendto
)与TCP(recv/send
)的核心差异:
特性 | UDP(recvfrom/sendto) | TCP(recv/send) |
---|---|---|
连接方式 | 无连接,每次通信需指定地址 | 面向连接,需先connect 建立连接 |
数据边界 | 有边界(每个sendto 对应一个recvfrom ) | 无边界(字节流,需上层协议定义分隔符) |
可靠性 | 不可靠(无确认、重传、排序) | 可靠(确认、重传、排序、流量控制) |
并发支持 | 天然支持(无连接,无需创建新进程/线程) | 需多进程/线程或I/O复用(每个连接独占socket) |
适用场景 | 实时通信(语音、视频)、DNS查询、广播/多播 | 文件传输、HTTP/HTTPS、登录认证等可靠场景 |
总结:recvfrom
和sendto
是UDP编程的核心函数,其设计充分适配了UDP的无连接特性;在实际开发中,需结合业务场景选择协议,并注意地址转换、错误处理、超时控制等细节,才能实现高性能、高可靠的UDP应用。