好的!下面给你整理一份关于 Linux 编程中 select
函数 的详解,帮助你理解和使用它进行多路 I/O 复用。
Linux 编程之 select
1. 什么是 select
select
是 Unix/Linux 提供的多路 I/O 复用函数。- 它可以让一个进程同时监控多个文件描述符(如套接字、管道、文件等),判断是否准备好读写或异常。
- 适用于单线程同时处理多个 I/O 事件,避免阻塞。
2. 函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds
:监控的文件描述符范围是[0, nfds-1]
,即最大文件描述符 + 1。readfds
:监控可读事件的文件描述符集合。writefds
:监控可写事件的文件描述符集合。exceptfds
:监控异常事件的文件描述符集合。timeout
:超时时间,NULL
表示阻塞等待。
3. 相关数据结构
fd_set
- 用于表示文件描述符集合,宏操作如下:
FD_ZERO(fd_set *set); // 清空集合
FD_SET(int fd, fd_set *set); // 向集合添加 fd
FD_CLR(int fd, fd_set *set); // 从集合删除 fd
int FD_ISSET(int fd, fd_set *set); // 判断 fd 是否在集合中
4. 使用步骤
- 初始化 fd_set,清空集合。
- 将要监控的文件描述符加入对应集合。
- 调用
select
,等待事件发生或超时。 - 通过
FD_ISSET
判断哪个文件描述符有事件。 - 处理事件。
5. 简单示例代码
监听标准输入(fd=0)是否有数据可读:
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
int main() {
fd_set rfds;
struct timeval tv;
int retval;
// 监听标准输入
FD_ZERO(&rfds);
FD_SET(STDIN_FILENO, &rfds);
// 设置超时时间为5秒
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(STDIN_FILENO + 1, &rfds, NULL, NULL, &tv);
if (retval == -1) {
perror("select()");
} else if (retval) {
printf("数据可读\n");
char buf[100];
int len = read(STDIN_FILENO, buf, sizeof(buf)-1);
if (len > 0) {
buf[len] = '\0';
printf("读取内容:%s\n", buf);
}
} else {
printf("超时,没有数据\n");
}
return 0;
}
6. 注意事项
select
会修改传入的 fd_set,调用前要重新设置。nfds
必须是最大文件描述符值 + 1。select
最大支持文件描述符为 1024(可编译时修改)。- 性能上不适合大文件描述符集,推荐用
poll
或epoll
。
7. select
应用场景
- 简单服务器监听多个客户端连接。
- 需要同时监听多个文件描述符。
- 事件数量不大,系统支持较好。
8. 进阶建议
- 大量连接时,推荐用
epoll
(Linux 特有)替代select
,性能更优。 poll
是select
的改进,支持更大文件描述符。
好的!下面给你一个基于 select
的简单多客户端 TCP 服务器示例,示范如何用 select
同时监听多个客户端连接和标准输入,实现基本的连接管理和数据收发。
基于 select 的多客户端 TCP 服务器示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/select.h>
#define PORT 12345
#define MAX_CLIENTS FD_SETSIZE
#define BUFFER_SIZE 1024
int main() {
int listen_fd, conn_fd, sock_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
fd_set all_fds, read_fds;
int max_fd;
int client_socks[MAX_CLIENTS];
char buffer[BUFFER_SIZE];
int i;
// 初始化客户端数组
for (i = 0; i < MAX_CLIENTS; i++) client_socks[i] = -1;
// 创建监听 socket
if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 绑定
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有接口
server_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(listen_fd);
exit(EXIT_FAILURE);
}
// 监听
if (listen(listen_fd, 10) < 0) {
perror("listen");
close(listen_fd);
exit(EXIT_FAILURE);
}
printf("服务器启动,监听端口 %d\n", PORT);
FD_ZERO(&all_fds);
FD_SET(listen_fd, &all_fds);
max_fd = listen_fd;
while (1) {
read_fds = all_fds; // 复制一份,select 会修改集合
int n_ready = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
if (n_ready < 0) {
perror("select");
break;
}
// 新连接到来
if (FD_ISSET(listen_fd, &read_fds)) {
conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &addr_len);
if (conn_fd < 0) {
perror("accept");
continue;
}
printf("新客户端连接: %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 加入客户端数组和监控集合
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_socks[i] < 0) {
client_socks[i] = conn_fd;
FD_SET(conn_fd, &all_fds);
if (conn_fd > max_fd) max_fd = conn_fd;
break;
}
}
if (i == MAX_CLIENTS) {
printf("客户端过多,拒绝连接\n");
close(conn_fd);
}
if (--n_ready <= 0) continue; // 没有其他事件
}
// 检查所有客户端是否有数据可读
for (i = 0; i < MAX_CLIENTS; i++) {
sock_fd = client_socks[i];
if (sock_fd < 0) continue;
if (FD_ISSET(sock_fd, &read_fds)) {
int n = read(sock_fd, buffer, sizeof(buffer) - 1);
if (n <= 0) {
if (n == 0) {
printf("客户端 %d 断开连接\n", sock_fd);
} else {
perror("read");
}
close(sock_fd);
FD_CLR(sock_fd, &all_fds);
client_socks[i] = -1;
} else {
buffer[n] = '\0';
printf("收到客户端 %d 消息: %s\n", sock_fd, buffer);
// 简单回显
write(sock_fd, buffer, n);
}
if (--n_ready <= 0) break;
}
}
}
close(listen_fd);
return 0;
}
代码说明
- 监听端口 12345。
- 用数组
client_socks
保存当前活跃客户端套接字。 - 使用
select
监听监听套接字和所有客户端套接字的可读事件。 - 有新连接时
accept
并加入监控。 - 有数据时读取并简单回显,连接关闭时清理。
- 单线程实现,适合小规模连接。
下一步建议
- 你可以在此基础上扩展写心跳检测、写事件处理等。
- 了解并尝试
poll
和 Linux 专属的epoll
,解决select
文件描述符数量限制和效率问题。 - 学习线程池、异步 IO,提升服务器性能。
发表回复