实现多路复用的方式方式有很多种,除了select,还有poll和epoll。各种方式都有其合适的应用应用场景,实际使用中,应根据应用场景来适配合适的模型。
1. 基于select 多路复用的缺点
select实现多路复用的方式是:将已经连接上的socket所对应的文件描述符放到一个文件描述符的集合中,然后调用select函数将文件描述符集合拷贝至内核中,让内核来检查是否有网络事件产生,检查的方式也很粗暴,就是通过遍历文件描述符集合,当检测到有事件产生后,将此socket标记为可读或者可写,然后再把文件描述符集合拷贝到用户态,用户态再通过遍历的方法找到可读或者可写socket,然后在对其进行处理。
所以其缺点很明显:
- 进行两次遍历文件描述符集合:一次是在内核中,一次是在用户态
- 进行两次文件描述符集合的拷贝,先由用户空间传到内核空间,然后由内核空间传到用户空间
针对应用编程缺点:
- 调用select函数后常见编程方式中都需要针对整个文件描述符集合进行循环语句
- 每次调用select函数时都需要向该函数传递监视对象信息
另外,select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。
2. epoll的理解与应用
2.1 epoll的改进
epoll 主要就是针对select和poll进行了改进
- 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可
- 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒
- 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合
这样编写epoll程序:
- 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句
- 无需每次传递监视对象信息
2.2 epoll的具体使用
- 创建一个epoll句柄
// size epoll实例的大小,该值只是向系统建议,仅供系统进行一定参考
//创建成功返回对应的文件描述符,失败-1
int epoll_create(int size);
- 向内核增加、修改或删除要监控的文件描述符
int epoll_ctl(
int epfd, int op, int fd, struct epoll_event *event);
- 待文件描述符发生变化,类似于select函数
int epoll_wait(
int epfd, struct epoll_event *events, int max events, int timeout);
3. epoll 示例
在select回声程序中进行修改:
epoll_server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <stdlib.h>
int EPOLL_SIZE=50;
int BUF_SIZE=100;
int server_handler(int server)
{
struct sockaddr_in addr = {0};
socklen_t asize = sizeof(addr);
return accept(server, (struct sockaddr*)&addr, &asize);
}
int client_handler(int client)
{
printf("client_handler. \n");
char buf[32] = {0};
int ret = read(client, buf, sizeof(buf)-1);
printf("Receive: %s\n", buf);
if( ret > 0 )
{
buf[ret] = 0;
printf("Receive: %s \n", buf);
if( strcmp(buf, "quit") != 0 )
{
ret = write(client, buf, ret);
}
else
{
ret = -1;
}
}
return ret;
}
int main()
{
int server = 0;
struct sockaddr_in saddr = {0};
struct timeval timeout = {0};
int i = 0;
int epfd;
int event_cnt;
struct epoll_event* ep_events;//用于存储监视的文件描述符
struct epoll_event event;
char buf[BUF_SIZE];
int str_len = 0;
struct sockaddr_in clnt_adr = {0};
socklen_t adr_sz = sizeof(clnt_adr);
server = socket(PF_INET, SOCK_STREAM, 0);
if( server == -1 )
{
printf("server socket error \n");
return -1;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons(8888);
if( bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1 )
{
printf("server bind error \n");
return -1;
}
if( listen(server, 1) == -1 )
{
printf("server listen error \n");
return -1;
}
printf("server start success \n");
epfd = epoll_create(EPOLL_SIZE);//可以忽略这个参数,填入的参数为操作系统参考
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events=EPOLLIN;
event.data.fd=server;
epoll_ctl(epfd,EPOLL_CTL_ADD,server,&event);//例程epfd 中添加文件描述符 socket,目的是监听 socket的enevt 中的事件
while( 1 )
{
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
if( event_cnt==-1 ){
printf("epoll_wait() error");
break;
}
for(i=0; i<event_cnt; i++){
if( ep_events[i].data.fd==server)
{
int client_sock = server_handler(server);
event.events = EPOLLIN ;
event.data.fd = client_sock; //把客户端套接字添加进去
epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &event);
printf("connect client:%d \n",client_sock);
}
else//是客户端套接字时
{
client_handler( ep_events[i].data.fd); //
}
}
}
close(server);
close(epfd);
return 0;
}