一、UDP 通信模型概述
UDP(User Datagram Protocol)是一种无连接的传输层协议,具有轻量级、低延迟的特点,适用于实时通信、流媒体传输等场景。本文通过一个完整的 C 语言示例,演示如何利用select机制实现基于 UDP 的双向非阻塞通信,解决传统阻塞式编程中无法同时处理收发数据的问题。
二、核心功能模块解析
1. 套接字创建与本地地址绑定
// 创建UDP套接字(IPv4协议,数据报模式)
int serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
if (serverSocket < 0) {
perror("socket创建失败");
return -1;
}
// 绑定本地地址(IP+端口)
struct sockaddr_in serverSockaddr;
bzero(&serverSockaddr, sizeof(serverSockaddr));
serverSockaddr.sin_family = AF_INET; // IPv4协议族
serverSockaddr.sin_port = htons(atoi(argv[1])); // 本地端口(主机字节序转网络字节序)
serverSockaddr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地网卡
if (bind(serverSocket, (struct sockaddr*)&serverSockaddr, sizeof(serverSockaddr)) < 0) {
perror("bind绑定失败");
close(serverSocket);
return -1;
}
关键说明:
AF_INET指定 IPv4 协议,SOCK_DGRAM标识 UDP 协议;INADDR_ANY允许套接字接收所有本地 IP 的流量,常用于服务器端;htons将主机字节序端口号转换为网络字节序(大端模式),确保跨平台兼容性。
2. 远程目标地址配置
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET; // IPv4协议族
dest_addr.sin_port = htons(atoi(argv[3])); // 目标端口
dest_addr.sin_addr.s_addr = inet_addr(argv[2]); // 目标IP地址(字符串转网络字节序)
socklen_t dest_addrlen = sizeof(dest_addr);
注意事项:
命令行参数需按./程序名 本地端口 远程IP 远程端口格式传入;inet_addr用于将点分十进制 IP(如192.168.1.1)转换为 32 位网络字节序整数
3. select 多路复用机制核心实现
fd_set readfd;
struct timeval timeout;
while (1) {
FD_ZERO(&readfd); // 清空文件描述符集合
FD_SET(0, &readfd); // 监听标准输入(键盘输入)
FD_SET(serverSocket, &readfd); // 监听UDP套接字
timeout.tv_sec = 5; // 超时时间5秒
timeout.tv_usec = 0;
int ret = select(serverSocket + 1, &readfd, NULL, NULL, &timeout);
if (ret > 0) { // 检测到可读事件
// 处理标准输入(用户发送数据)
if (FD_ISSET(0, &readfd)) {
char writeBuf[64] = {0};
fgets(writeBuf, sizeof(writeBuf), stdin);
sendto(serverSocket, writeBuf, strlen(writeBuf), 0,
(struct sockaddr*)&dest_addr, dest_addrlen);
}
// 处理UDP套接字数据接收
if (FD_ISSET(serverSocket, &readfd)) {
char readBuf[64] = {0};
struct sockaddr_in src_addr;
socklen_t src_addrlen = sizeof(src_addr);
int recv_len = recvfrom(serverSocket, readBuf, sizeof(readBuf), 0,
(struct sockaddr*)&src_addr, &src_addrlen);
if (recv_len > 0) {
printf("来自 %s:%d -> %s",
inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port), readBuf);
}
}
} else if (ret == 0) { // 超时处理
printf("等待超时(5秒)\n");
} else { // 错误处理
perror("select调用失败");
break;
}
}
select 机制核心原理:
文件描述符集合:通过FD_ZERO和FD_SET管理监听的描述符(标准输入0和套接字serverSocket);超时控制:timeout结构体设置 5 秒超时,避免程序无限阻塞;事件驱动:select返回后通过FD_ISSET判断具体哪个描述符就绪,实现 “单线程处理多事件”。
三、数据收发与异常处理
发送数据流程
从标准输入读取用户输入(fgets);通过sendto指定目标地址发送 UDP 数据包,无需建立连接即可通信。
接收数据流程
recvfrom接收数据时会获取发送方地址(src_addr);inet_ntoa和ntohs将网络字节序的 IP 和端口转换为可读格式。
异常处理策略
套接字创建 / 绑定失败时立即报错并释放资源;select超时(ret==0)时打印提示,继续循环监听;数据接收返回0时视为连接关闭(UDP 无连接,此场景较少见)。
四.完整代码
main.c文件完整代码:
#include "main.h"
socklen_t dest_addrlen;
struct sockaddr_in dest_addr;
socklen_t src_addrlen;
struct sockaddr_in src_addr;
int main(int argc, char **argv) {
if (argc != 4) {
printf("使用格式: ./udp 本地端口 远程IP 远程端口\n");
return 0;
}
// 1. 创建UDP套接字
int serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
if (serverSocket < 0) {
perror("socket创建失败");
return -1;
}
// 2. 绑定本地地址
struct sockaddr_in serverSockaddr;
bzero(&serverSockaddr, sizeof(serverSockaddr));
serverSockaddr.sin_family = AF_INET;
serverSockaddr.sin_port = htons(atoi(argv[1]));
serverSockaddr.sin_addr.s_addr = INADDR_ANY;
if (bind(serverSocket, (struct sockaddr*)&serverSockaddr, sizeof(serverSockaddr)) < 0) {
perror("bind绑定失败");
close(serverSocket);
return -1;
}
// 3. 配置远程目标地址
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(atoi(argv[3]));
dest_addr.sin_addr.s_addr = inet_addr(argv[2]);
dest_addrlen = sizeof(dest_addr);
src_addrlen = sizeof(src_addr);
// 4. 使用select监听多描述符
fd_set readfd;
struct timeval timeout;
while (1) {
FD_ZERO(&readfd);
FD_SET(0, &readfd);
FD_SET(serverSocket, &readfd);
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int ret = select(serverSocket + 1, &readfd, NULL, NULL, &timeout);
if (ret > 0) {
// 处理用户输入(发送数据)
if (FD_ISSET(0, &readfd)) {
char writeBuf[64] = {0};
fgets(writeBuf, sizeof(writeBuf), stdin);
if (sendto(serverSocket, writeBuf, strlen(writeBuf), 0,
(struct sockaddr*)&dest_addr, dest_addrlen) < 0) {
perror("sendto发送失败");
}
}
// 处理数据接收
if (FD_ISSET(serverSocket, &readfd)) {
char readBuf[64] = {0};
int recv_len = recvfrom(serverSocket, readBuf, sizeof(readBuf), 0,
(struct sockaddr*)&src_addr, &src_addrlen);
if (recv_len > 0) {
printf("来自 %s:%d -> %s",
inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port), readBuf);
} else if (recv_len == 0) {
printf("接收数据长度为0,连接关闭\n");
break;
} else {
perror("recvfrom接收失败");
}
}
} else if (ret == 0) {
printf("等待超时(5秒)\n");
} else {
perror("select调用失败");
break;
}
}
close(serverSocket);
return 0;
}
main.h文件完整代码
#ifndef _MAIN_H_
#define _MAIN_H_
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#endif
五、编译与运行示例
编译命令
gcc -o udp_demo main.c -g -Wall
运行示例
服务器端(本地端口 8888,假设远程目标为 192.168.1.100:9999):
./udp_demo 8888 192.168.1.100 9999
客户端(假设另一台主机执行相同程序,本地端口 9999,远程目标为 192.168.1.200:8888):
./udp_demo 9999 192.168.1.200 8888
六、技术扩展与应用场景
select 与 poll/epoll 的对比
select适用于小规模并发(描述符数量 < 1024),跨平台兼容性好;epoll(Linux 特有)通过事件通知机制优化大规模并发场景(如高并发服务器)。
UDP 通信的典型场景
实时聊天应用(如微信语音通话);游戏网络传输(低延迟要求);传感器数据采集(无需确保所有数据包到达)。
优化方向
添加数据分包 / 组包机制处理大数据传输;实现简单的丢包重传逻辑,提升可靠性;引入多线程分离数据收发与业务逻辑处理。
通过select机制与 UDP 协议的结合,该模型实现了单线程下的双向非阻塞通信,相比传统阻塞式编程具有更高的资源利用率和响应效率,适合对实时性要求较高的轻量级网络应用开发。