UDP协议的多线程网络编程——利用select函数高效地监控多个文件描述符,实现并发数据处理。

UDP协议的多线程网络编程——利用select函数高效地监控多个文件描述符,实现并发数据处理。

一、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 协议的结合,该模型实现了单线程下的双向非阻塞通信,相比传统阻塞式编程具有更高的资源利用率和响应效率,适合对实时性要求较高的轻量级网络应用开发。

相关推荐

新手指导 问道中的首饰携带与选择
best365投注

新手指导 问道中的首饰携带与选择

📅 08-18 👁️ 8562
DNF 罐子隐藏秘密大揭秘,开罐前必知的小技巧
365bet安全上网导航

DNF 罐子隐藏秘密大揭秘,开罐前必知的小技巧

📅 09-01 👁️ 5010
新版金属大师技能
best365投注

新版金属大师技能

📅 07-12 👁️ 5863