前面我们所有的代码都是通过read和write操作, 每次只能处理一个IO请求. 但是IO复用可以在同时处理多个socket的IO请求. 而且IO复用与线程连用是最常见的搭配.
本节介绍IO多路复用select
函数.
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
成功 : 返回事件发生的个数, 并且将返回改变的文件符保存在对应的集合中.
失败 : 返回负数. 如果被中断不可重启, 会设置errno的值为EINTR.
超时 : 返回0.
函数参数有点多, 可能一时间掌握不了可以慢慢做实验体验每个参数的意义.
nfds
: 打开的最大文件描述符 + 1 (注意 : 是最大 + 1)readfds
: 读描述符集合writefds
: 写描述符集合exceptfds
: 错误描述符集合timeout
: 定时
select
函数有一个叫文件描述符集合的数据类型fd_set, fd_set
很像信号集sigset_t而且两者的实现都很相似. sigset_t
信号集有32位之多, 每一位表示一个信号. 同样fd_set
描述符集默认有1024位(最大值用FD_SETSIZE表示), 每一位表示一个文件描述符.
sigset_t有一系列sigset
函数, fd_set
也同样有一系列的操作函数 :
// 从描述符集中删除fd
FD_CLR(int fd, fd_set *fdset);
// 清除描述符集所有设置的描述符
FD_ZERO(fd_set *fdset);
// 将fd添加到描述符集中
FD_SET(int fd, fd_set *fdset);
// 验证fd是否在描述符集中
FD_ISSET(int fd, fd_set *fdset);
我们使用本节介绍的select
函数来修改之前的代码, 实验验证与write函数的不同.
将客户端的代码做了修改, 这里只将修改的代码贴出分析. 完整的代码 select_client.c
// 客户端
int client(int port, const char *cli_addr)
{
int sockfd;
sockfd = Socket(0);
Connect(sockfd, port, cli_addr);
char buf[1024];
int n;
// 1
fd_set rfds, testrfds;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
FD_SET(STDIN_FILENO, &rfds);
while(1)
{
// 2
testrfds = rfds;
if(select(sockfd + 1, &testrfds, NULL, NULL, NULL) < 0)
EXIT("select");
// 3
if(FD_ISSET(STDIN_FILENO, &testrfds))
{
n = read(STDIN_FILENO, buf, sizeof(buf));
if(n == 0)
break;
send(sockfd, &buf, strlen(buf), 0);
}
// 4
if(FD_ISSET(sockfd, &testrfds))
{
n = recv(sockfd, buf, sizeof(buf), 0);
if(n == 0)
break;
write(STDOUT_FILENO, buf, n);
}
}
close(sockfd);
return 0;
}
需要注意几点
最终运行的结果可以看出正常的回射任何问题, 并且服务端关闭后客户端立马也就关闭了.
I/O多路复用技术(multiplexing)是什么?