I/O复用函数的使用——select
目录
一、概念
二、select接口
2.1 基础概念
2.2 使用 select 函数的标准输入读取代码
2.3 基于 `select` 模型的多客户端 TCP 服务器实现
一、概念
i/o复用使得程序能同时监听多个文件描述符,可以提高程序性能。
之前为了让服务器能够服务多个客户端,我们使用多线程或多进程进行服务器的并发,意味着有多少个客户端就要产生很多进程。会浪费,开销很大。所以我们引入i/o复用
i/o复用方法select,poll,epoll可以帮助应用程序找到就绪描述符eg:老师检查作业,谁写完了谁举手,老师过去检查,不用一个一个等待去检查
二、select接口
2.1 基础概念
检测键盘是否有数据输入,如果有打印输出,没有就阻塞。select系统调用的用途是在一段指定时间内,监听用户感兴趣的文件描述符的可读,可写异常事件。
系统调用原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
:需要监视的文件描述符的最大值加1(fd + 1
),因为文件描述符是从0开始的
readfds
、writefds
、exceptfds
:分别表示可读、可写和异常条件的文件描述符集合,程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符。
timeout
:超时时间,可以是NULL
(阻塞等待),或者指定超时时间(非阻塞等待)。成功时,返回就绪文件描述符的总数,超时没有任何描述符就绪则返回0,失败返回-1,如果在等待期间,程序收到信号则立即返回-1并设置error为EINTR
fd_set:结构体,fd_mask,数组类型,fd_bits,数组名。用于存储和管理一组文件描述符 fd_set可以容纳1024个位 数组收集 描述符,把数组中的描述符添加到集合fd_set
fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位bit标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量
由于位操作过于频繁,可以用一系列宏来访问fd_set结构体中的位
使用I/O复用技术(如
select
、poll
或epoll
)来处理多个文件描述符(FD)的流程图
假设只有3和7上有数据,结构体集合经过select返回值为2,之后会测试和集合作比较看看对于的位是不是1,找到看哪个上面有数据
2.2 使用 select 函数的标准输入读取代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <time.h>
#define STDIN 0 // 定义STDIN常量,表示标准输入的文件描述符
int main()
{
int fd = STDIN; // 将文件描述符fd初始化为标准输入
fd_set fdset; // 定义一个文件描述符集合fdset
while (1) // 无限循环,持续等待输入
{
FD_ZERO(&fdset); // 清空文件描述符集合fdset
FD_SET(fd, &fdset); // 将标准输入的文件描述符添加到集合fdset中
struct timeval tv = {5, 0}; // 定义一个timeval结构体,设置超时时间为5秒
int n = select(fd+1, &fdset, NULL, NULL, &tv); // 调用select函数,监视fdset集合中的文件描述符
if (-1 == n) // 如果select返回-1,表示发生错误
{
printf("select err\n"); // 打印错误信息
continue; // 继续下一次循环
}
else if (n == 0) // 如果select返回0,表示超时,没有文件描述符就绪
{
printf("time out\n"); // 打印超时信息
}
else // 如果select返回大于0的值,表示有文件描述符就绪
{
if (FD_ISSET(fd, &fdset)) // 检查标准输入的文件描述符是否就绪
{
char buff[128] = {0}; // 定义一个字符数组buff,用于存储读取的数据
read(fd, buff, 127); // 从标准输入读取数据到buff中,最多读取127个字符
printf("read:%s\n", buff); // 打印读取到的数据
}
}
}
return 0; // 程序正常结束
}
这段代码使用
select
函数来监视标准输入(键盘输入),并在有输入时读取数据。它首先定义了一个文件描述符集合fdset
,并将标准输入的文件描述符添加到这个集合中。然后,它调用select
函数来监视这个集合中的文件描述符,等待最多5秒。如果在这5秒内有任何文件描述符就绪(即有输入),select
函数会返回就绪的文件描述符的数量。程序然后检查标准输入的文件描述符是否就绪,并从标准输入读取数据。如果5秒内没有文件描述符就绪,select
函数会返回0,程序会打印超时信息。如果select
函数返回-1,表示发生错误,程序会打印错误信息并继续下一次循环。if (FD_ISSET(fd, &fdset))
FD_ISSET
是一个宏,用于检查一个特定的文件描述符(FD)是否已经包含在文件描述符集合(fd_set
)中。这个宏是在使用select
函数后,用来确定哪些文件描述符已经准备好进行 I/O 操作(如读、写)的常用方法。struct timeval tv = {5, 0}; // 定义一个timeval结构体,设置超时时间为5秒
struct timeval tv = {5, 0};
:定义了一个timeval
结构体变量tv
,并初始化它。
timeval
结构体:在Linux中,timeval
结构体用于表示时间值,它包含两个字段:
tv_sec
:表示时间的秒数部分。
tv_usec
:表示时间的微秒数部分。初始化:这里将
tv_sec
初始化为5,tv_usec
初始化为0。这意味着超时时间为5秒。
2.3 基于 `select` 模型的多客户端 TCP 服务器实现
select.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define MAXFD 10 // 定义最大文件描述符数量
// 初始化文件描述符数组
void fds_init(int fds[])
{
for (int i = 0; i < MAXFD; i++)
{
fds[i] = -1; // 将所有元素初始化为-1,表示未使用
}
}
// 向文件描述符数组添加一个文件描述符
void fds_add(int fds[], int fd)
{
for (int i = 0; i < MAXFD; i++)
{
if (-1 == fds[i])
{
fds[i] = fd; // 找到空位,添加文件描述符
break; // 添加成功后退出循环
}
}
}
// 从文件描述符数组中删除一个文件描述符
void fds_del(int fds[], int fd)
{
for (int i = 0; i < MAXFD; i++)
{
if (fds[i] == fd)
{
fds[i] = -1; // 找到文件描述符,将其删除
break; // 删除成功后退出循环
}
}
}
// 初始化套接字
int socket_init()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个IPv4 TCP套接字
if (sockfd == -1)
{
return -1; // 创建失败,返回-1
}
struct sockaddr_in saddr; // 定义一个sockaddr_in结构体,用于存储服务器地址信息
memset(&saddr, 0, sizeof(saddr)); // 将结构体清零
saddr.sin_family = AF_INET; // 设置地址族为IPv4
saddr.sin_port = htons(6000); // 设置端口号为6000,并转换为网络字节序
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置IP地址为127.0.0.1
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)); // 绑定套接字到本地地址
if (-1 == res)
{
printf("bind err\n"); // 绑定失败,打印错误信息
return -1; // 返回-1
}
res = listen(sockfd, 5); // 开始监听连接请求,最多允许5个连接请求排队
if (res == -1)
{
return -1; // 监听失败,返回-1
}
return sockfd; // 返回监听套接字的文件描述符
}
// 接受客户端连接请求
void accept_client(int sockfd, int fds[])
{
int c = accept(sockfd, NULL, NULL); // 接受一个连接请求
if (c < 0)
{
return; // 接受失败,直接返回
}
printf("accept c=%d\n", c); // 打印接受的客户端文件描述符
fds_add(fds, c); // 将客户端文件描述符添加到数组
}
// 接收数据
void recv_data(int c, int fds[])
{
char buff[128] = {0}; // 定义一个缓冲区,用于存储接收的数据
int n = recv(c, buff, 127, 0); // 从客户端接收数据
if (n <= 0)
{
close(c); // 关闭套接字
fds_del(fds, c); // 从数组中删除客户端文件描述符
printf("client close\n"); // 打印客户端关闭信息
return; // 返回
}
printf("recv:%s\n", buff); // 打印接收到的数据
send(c, "ok", 2, 0); // 向客户端发送确认信息
}
int main()
{
int fds[MAXFD]; // 定义一个文件描述符数组
fds_init(fds); // 初始化文件描述符数组
int sockfd = socket_init(); // 初始化套接字
if (sockfd == -1)
{
exit(1); // 初始化失败,退出程序
}
fds_add(fds, sockfd); // 将监听套接字的文件描述符添加到数组
fd_set fdset; // 定义一个文件描述符集合
while (1)
{
FD_ZERO(&fdset); // 清空文件描述符集合
int maxfd = -1; // 初始化最大文件描述符
for (int i = 0; i < MAXFD; i++)
{
if (fds[i] == -1)
{
continue; // 跳过未使用的文件描述符
}
if (maxfd < fds[i])
{
maxfd = fds[i]; // 更新最大文件描述符
}
FD_SET(fds[i], &fdset); // 将文件描述符添加到集合
}
struct timeval tv = {5, 0}; // 设置超时时间为5秒
int n = select(maxfd + 1, &fdset, NULL, NULL, &tv); // 调用select函数,监视文件描述符集合
if (-1 == n)
{
printf("select err\n"); // select失败,打印错误信息
}
else if (0 == n)
{
printf("time out\n"); // 超时,打印超时信息
}
else
{
for (int i = 0; i < MAXFD; i++)
{
if (-1 == fds[i])
{
continue; // 跳过未使用的文件描述符
}
if (FD_ISSET(fds[i], &fdset)) // 检查文件描述符是否有就绪事件
{
// 处理就绪的文件描述符
if (fds[i] == sockfd)
{
// 监听套接字就绪,接受客户端连接请求
accept_client(sockfd, fds);
}
else
{
// 客户端套接字就绪,接收数据
recv_data(fds[i], fds);
}
}
}
}
}
}
1. **包含必要的头文件**:
- 引入标准输入输出、标准库、Unix 系统调用、字符串操作、套接字编程、IP 地址转换等相关的头文件。2. **定义常量**:
- 定义 `MAXFD` 常量,表示文件描述符数组的最大长度。3. **初始化文件描述符数组** (`fds_init` 函数):
- 遍历数组,将所有元素初始化为 `-1`,表示这些文件描述符当前未被使用。4. **添加文件描述符到数组** (`fds_add` 函数):
- 在数组中查找第一个 `-1` 值的位置,并将传入的文件描述符 `fd` 存储在该位置。5. **从数组中删除文件描述符** (`fds_del` 函数):
- 在数组中查找传入的文件描述符 `fd`,并将对应的元素设置为 `-1`。6. **初始化套接字** (`socket_init` 函数):
- 创建 TCP 套接字。
- 设置服务器地址结构体 `sockaddr_in`,包括 IP 地址和端口号。
- 绑定套接字到指定的 IP 地址和端口号。
- 使套接字开始监听连接请求。7. **接受客户端连接请求** (`accept_client` 函数):
- 使用 `accept` 函数接受一个新的客户端连接请求。
- 打印客户端文件描述符。
- 将客户端文件描述符添加到文件描述符数组中。8. **接收数据** (`recv_data` 函数):
- 从指定的客户端文件描述符接收数据。
- 如果接收到的数据长度小于等于0(表示客户端已关闭连接),则关闭套接字并从数组中删除对应的文件描述符。
- 打印接收到的数据,并给客户端发送确认信息。9. **主函数 (`main`)**:
- 初始化文件描述符数组。
- 调用 `socket_init` 函数初始化套接字,并将监听套接字的文件描述符添加到数组中。
- 进入一个无限循环,持续等待和处理事件:
- 清空 `fd_set` 结构体 `fdset`。
- 遍历文件描述符数组,将所有有效的文件描述符添加到 `fdset` 中,并更新 `maxfd` 变量。
- 设置超时时间。
- 调用 `select` 函数监视 `fdset` 中的文件描述符,等待最多5秒钟。
- 根据 `select` 的返回值处理错误、超时或就绪的文件描述符:
- 如果 `select` 返回 `-1`,表示发生错误,打印错误信息。
- 如果 `select` 返回 `0`,表示超时,打印超时信息。
- 如果 `select` 返回大于 `0` 的值,表示有文件描述符就绪,遍历文件描述符数组,检查哪些文件描述符有就绪事件,并进行相应的处理:
- 如果就绪的文件描述符是监听套接字,则调用 `accept_client` 函数接受新的客户端连接请求。
- 如果就绪的文件描述符是客户端套接字,则调用 `recv_data` 函数接收数据。通过这些步骤,服务器能够同时处理多个客户端连接请求和数据接收,实现高效的 I/O 复用。
这段代码实现了一个简单的TCP服务器,它使用
select
函数来同时处理多个客户端连接请求和数据接收。服务器首先初始化一个监听套接字,并将其文件描述符添加到文件描述符数组中。然后,它进入一个无限循环,使用select
函数监视文件描述符数组中的所有文件描述符。当有文件描述符就绪时,服务器会根据文件描述符的类型(监听套接字或客户端套接字)来处理相应的事件(接受连接请求或接收数据)。
cli.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个IPv4 TCP套接字
if (sockfd == -1) // 检查套接字创建是否成功
{
exit(1); // 如果失败,退出程序
}
struct sockaddr_in saddr; // 定义一个sockaddr_in结构体,用于存储服务器地址信息
memset(&saddr, 0, sizeof(saddr)); // 将结构体清零
saddr.sin_family = AF_INET; // 设置地址族为IPv4
saddr.sin_port = htons(6000); // 设置端口号为6000,并转换为网络字节序
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置IP地址为127.0.0.1
int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 连接到服务器
if (res == -1) // 检查连接是否成功
{
printf("connect err\n"); // 如果失败,打印错误信息
exit(1); // 退出程序
}
while (1) // 无限循环,持续发送和接收数据
{
printf("input\n"); // 提示用户输入
char buff[128] = {0}; // 定义一个缓冲区,用于存储输入和接收的数据
fgets(buff, 128, stdin); // 从标准输入读取一行数据
if (strncmp(buff, "end", 3) == 0) // 检查是否输入了"end"命令
{
break; // 如果是,退出循环
}
send(sockfd, buff, strlen(buff) - 1, 0); // 发送数据到服务器,不包括换行符
memset(buff, 0, 128); // 清空缓冲区
recv(sockfd, buff, 127, 0); // 从服务器接收数据
printf("read:%s\n", buff); // 打印接收到的数据
}
close(sockfd); // 关闭套接字
exit(0); // 程序正常结束
}
这段代码实现了一个简单的TCP客户端,它连接到本地服务器(127.0.0.1:6000),并持续发送和接收数据。用户可以在命令行中输入数据,输入"end"命令时,程序会退出。