1. 基本概念
我们在上一篇文章代码的注释中讲到了关于C10K的问题,也就是同时10k个客户端来访问服务器,如果采用pthread_create()函数来对每一个服务器的访问创建一个新的线程来处理,虽然说同一个进程下的上下文的切换不是很大, 但是频繁地创建和销毁线程就会有很大的系统开销。即使我们使用线程池来避免线程的创建和销毁,10k个线程也是操作系统无法承受的量级了。因此我们引入了三种方式来处理该问题,分别是select, poll和epoll。他们的核心原理都是基于IO多路复用技术,因为我们已经知道了无法通过多个进程或者线程来处理,那么我们就需要通过通过一个进程或者线程来实现,也就是“复用”,多个客户端请求重复使用同一个进程/线程。
select实现IO多路复用的方式是来维护一个文件描述符(File Descriptor)集合,然后调用select函数将该文件描述符集合拷贝到内核中(select函数见下文代码分析),然后我们通过遍历来检测是否有事件产生,如果有事件产生,我们则将整个文件描述符集合在copy到用户的程序中,用户的程序再遍历进行检测到具体是哪一个事件产生,之后进行处理。
文件描述符的集合使用bitmap来维护,也就是fds_bits,默认最大值是1024。
Select缺点:
- 参数较多不利于维护(5个)
- 每次都需要将所有的rset集合(待检测的io集合)拷贝到内核,再从内核拷贝出来
- 对io数量有限制,收到文件描述符最大数量的限制
2. 代码分析
//select 单线程处理多个客户端
//0:stdin 1:stdout 2:stderr 3:listen
//int nready = select(maxfd, rset, wset, eset, timeout); //判断最大fd的值, 可读, 可写, 出错, 轮循间隔
/*typedef struct {
unsigned long fds_bits[1024 / (8 * sizeof(long))];
} __kernel_fd_set;*/
fd_set rfds, rset;
//清除文件描述符集合中的所有位,即初始化 rfds 集合为空
FD_ZERO(&rfds);
//将监听套接字文件描述符sockfd添加到rfds中
FD_SET(sockfd, &rfds);
//初始化,后文会修改
int maxfd = sockfd;
printf("loop\n");
while (1) {
rset = rfds;
int nready = select(maxfd + 1, &rset, NULL, NULL, NULL/*一直等待*/);
//检测服务端是否连接,开始监听
if (FD_ISSET(sockfd, &rset)) {
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
//接受新的连接请求
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
//输出哪个socket在处理
printf("sockfd: %d\n", sockfd);
//将客户端fd添加至rfds,为后续遍历找到该客户端准备
FD_SET(clientfd, &rfds);
maxfd = clientfd;//maxfd = max(maxfd, clientfd)
}
int i = 0;
//遍历所有的文件描述符集合
for (i = sockfd + 1; i <= maxfd; i++) {
//判断i对应的fd是否有事件发生
if (FD_ISSET(i, &rset)) {
char buffer[128] = {0};
int count = recv(i, buffer, 128, 0);
if (count == 0) { //调用close()
printf("disconnect\n");
//close(i); ①
//FD_CLR(i, &rfds); ②
//标准断开:清空多路io复用再关闭 ③
FD_CLR(i, &rfds);//第三种才是正确的方式
close(i);
break;
}
send(i, buffer, count/*128*/, 0);//发送至客户端检测整个过程是否正确
printf("sockfd: %d, clientfd: %d, count: %d, buffer: %s\n", sockfd, i, count, buffer);
}
}
}