当前位置: 首页 > news >正文

Linux服务器编程实践45-UDP数据读写:recvfrom与sendto函数的使用实例

1. 前言:UDP协议与数据读写的特殊性

在Linux网络编程中,UDP(用户数据报协议)作为无连接、不可靠的传输层协议,与面向连接的TCP在数据读写方式上有显著差异。TCP通过建立持久连接,使用recv/send即可完成数据交互;而UDP因无连接特性,每次通信都需明确指定目标地址,因此内核提供了专门的recvfromsendto函数处理UDP数据报。

本文将从函数原理、参数解析、使用场景出发,结合完整代码示例,详解UDP数据读写的实现逻辑,并通过JavaScript可视化工具展示数据交互流程,帮助开发者快速掌握UDP编程的核心要点。

注意:UDP不保证数据的有序性和完整性,也不进行重传,因此在需要可靠通信的场景(如文件传输)中需上层协议自行实现确认机制;但UDP的无连接特性使其在低延迟、高并发场景(如实时通信、DNS查询)中性能更优。

2. UDP数据读写核心函数:recvfrom与sendto

Linux内核在sys/socket.h头文件中定义了recvfromsendto函数,二者均支持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)
sockfdUDP类型的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客户端与服务器的数据流交互图,展示recvfromsendto的调用时机与数据流向。

从流程图可见,UDP服务器必须先通过bind绑定端口(让客户端知道目标地址),而客户端可省略bind(内核自动分配临时端口);每次数据交互时,客户端通过sendto指定服务器地址,服务器通过recvfrom获取客户端地址并回复。

4. 实战:UDP回射服务器与客户端实现

下面通过“回射服务”实例(客户端发送数据,服务器原样返回),完整展示recvfromsendto的使用方式,包含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 = EAGAIN1. 增加重试逻辑;2. 使用select监听socket就绪事件
inet_pton: Invalid argumentIP地址格式错误(如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、登录认证等可靠场景

总结recvfromsendto是UDP编程的核心函数,其设计充分适配了UDP的无连接特性;在实际开发中,需结合业务场景选择协议,并注意地址转换、错误处理、超时控制等细节,才能实现高性能、高可靠的UDP应用。

http://www.dtcms.com/a/494839.html

相关文章:

  • 基于SpringBoot+Vue的数码交流管理系统(AI问答、协同过滤算法、websocket实时聊天、Echarts图形化分析)
  • 设计模式篇之 状态模式 State
  • linux系统编程(十)RK3568 socket之 UDP的实现
  • MySQL事务隔离
  • 甜点的网站建设规划书长春市城乡建设局网站
  • C++ 多线程实战 11|如何系统性避免死锁
  • WAPR断网攻击天阶大法根基法之wifi爆破
  • 集群冗余:高可用的核心设计
  • Vue 3 完全指南:响应式原理、组合式 API 与实战优化
  • Netscape 浏览器
  • 笔记:TFT_eSPI不支持ESP32C6;ESP8266运行LVGL注意事项
  • 会网站开发没学历seo网络营销
  • 简述深度学习中的四种数据并行方法(DP,DDP,TP,PP)
  • YOLO-World 全面解析:实时开放词汇目标检测的新范式(附实践指南)
  • 西瓜网络深圳网站建设 东莞网站建设电商型网站
  • AI+大数据时代:时序数据库的生态重构与价值跃迁——从技术整合到行业落地
  • 设计素材网站图案免费建设银行社保卡网站在哪
  • 预告!星火社吕诚将推 “星星之火” 线上课堂,哲思 + 投资赋能公益新生态
  • 孟德尔随机化 哪个计算最消耗时间 在肠道菌群、代谢物和疾病三类数据中,**肠道菌群数据的处理通常最消耗时间**
  • 【Redis学习】持久化机制(RDB/AOF)
  • 栈式自编码器(Stacked Auto-Encoder)
  • 像wordpress一样的网站建设银行网站转账必须u盾吗
  • 让低端机也能飞:Canvas/WebGL/Viz 分层、降级渲染与数据抽样策略
  • 【grafana查询超时问题】
  • 广播系统配线-批量测量快速计算
  • 电商网站商品页的优化目标是什么?第一推是谁做的网站
  • 从零开始的C++学习生活 9:stack_queue的入门使用和模板进阶
  • docker 运行容器限制内存、限制磁盘 IO
  • Compose Multiplatform+Kotlin Multiplatfrom 第七弹跨平台 AI开源
  • C++设计模式_行为型模式_状态模式State