1. 多路IO复用
内核监听多个socket文件描述符读写缓冲区属性的变化,若某个文件描述符的读缓冲区有变化,则将该事件告诉应用层。
内核提供多路IO复用的API:select、poll(使用较少)、epoll。
2. select
select原理
用户态创建文件描述符位图,将需要被监听的文件描述符置1,同时拷贝一份文件描述符位图做备份;
每调用一次select就将该文件描述符位图拷贝给内核,内核监听文件描述符是否有事件,将有事件的文件描述符保留,其余文件描述符置0,然后拷贝给用户态;
用户态遍历内核拷贝过来的文件描述符位图,若lfd(监听文件描述符)有事件则表示有新连接到来,则将新连接的文件描述符加入到备份的文件描述符位图中;
若其他文件描述符有事件,则进行处理;若有客户端关闭,则从备份文件描述符中删除该客户端的文件描述符;
下次再监听时则将备份的文件描述符位图拷贝到内核态进行监听。
select优缺点
优点:
① 是POSIX标准,跨平台较好:Linux、Windows、MAC OS;
缺点:
① 文件描述符数最大限制为1024,;可修改,但需要重新编译内核;
② 仅返回有变化的文件描述符个数,需要自己遍历才知道是哪个文件描述符发生变化;
③ 文件描述符位图需要在用户空间和内核空间来回拷贝;
④ 客户端较多,但只有少量客户端活跃时,效率低,因为每次都遍历所有的客户端。
select API
#include<sys/select.h>
#include<sys/types.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
/*
功能:
监听多个文件描述符的属性(读、写、异常)变化。
参数:
nfds:最大文件描述符+1
readfds:需要监听的读文件描述符的集合
writefds:需要监听的写文件描述符的集合,通常为NULL
exceptfds:需要监听的异常文件描述符的集合,通常为NULL
timeout:
> 0:监听超时时间(多久监听一次);
0:无文件描述符变化则立即返回;
NULL:阻塞监听到有文件描述符变化才返回。
返回值:
> 0:变化的文件描述符的个数;
= 0:没有文件描述符发生变化;
< 0 :调用发生错误,会设置errno。
*/
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
}
void FD_CLR(int fd, fd_set* set); // 从文件描述符位图set中移除文件描述符fd
int FD_ISSET(int fd, fd_set* set); // 判断文件描述符fd是否在文件描述符位图set中
void FD_SET(int fd, fd_set* set); // 将文件描述符fd添加到文件描述符位图set中
void FD_ZERO(fd_set* set); // 清空文件描述符位图set
select示例:
#include<stdio.h>
#include<sys/select.h>
#include<sys/types.h>
#include<sys/time.h>
#include<unistd.h>
#include"wrap.h"
#define PORT 8888
int main(int argc, const char* argv[]) {
// 1.创建socket,bind
int lfd = tcp4bind(PORT, NULL);
// 2.监听
Listen(lfd, 128);
// 3.while select监听
int maxfd = lfd; // 最初监听到最大的文件描述符
fd_set oldset, rset;
FD_ZERO(&oldset); // 清空集合
FD_ZERO(&rset); // 清空集合
FD_SET(lfd, &oldset); // 将lfd加入oldset
while (1) {
rset = oldset; // 备份oldset
int n = select(maxfd + 1, &rset, NULL, NULL, 0); // select监听
if (n < 0) {
perror("select");
break;
} else if (0 == n) {
continue; // 无文件描述符变化则继续监听
} else { // 有文件描述符发生变化
if (FD_ISSET(lfd, &rset)) { // lfd有变化, 表示有新连接到来
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
char ip[16] = "";
// 提取新连接
int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len);
printf("新连接到来:IP = %s, port = %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
ntohs(cliaddr.sin_port));
FD_SET(cfd, &oldset); // 将cfd加入到oldset中
if (cfd > maxfd) { // 更新maxfd
maxfd = cfd;
}
if (--n == 0) { // 如果只有lfd有变化,则继续下一轮监听
continue;
}
}
// cfd有变化,遍历lfd之后的文件描述符是否有变化
for (int i = lfd + 1; i <= maxfd; i++) {
if (FD_ISSET(i, &rset)) { // 若文件描述符i有变化
char buf[1500] = ""; // 1500是以太网最大传输单元
int ret = Read(i, buf, 1500);
if (ret < 0) { // 出错
perror("read");
close(i); // 关闭文件描述符
FD_CLR(i, &oldset); // 从oldset中删除该文件描述符
} else if (0 == ret) {
printf("客户端关闭\n");
close(i); // 关闭文件描述符
FD_CLR(i, &oldset); // 从oldset中删除该文件描述符
} else {
printf("客户端:%s\n", buf);
write(i, buf, ret);
}
}
}
}
}
return 0;
}
运行结果: