I/O 复用使得程序能同时监听多个文件描述符,这对于提高程序的性能至关重要。
在客户端连接服务器时会产生套接字,如果要进行并发运行,同时客户端的数量庞大,就会导致产生多个线程,当线程产生的足够多的时候,CPU占有率会非常高,导致资源占用极大,所以提出了IO复用模型,使用一个线程来监视管理跟踪.具体怎么理解呢?
我们可以举一个例子,比如开学班长通知班里学生领书,但是不知道谁是什么时间领书,正常情况班里的每一个同学都去领书的地点,但是没有到领书的时间,只能等着,但是当一个班的同学足够多的时候,就会导致大家浪费了时间.但是IO复用呢,就是让班长充当监听套接字的作用,班长在领书的地点等待着领书,当轮到哪个同学领书,便通知哪个同学来领书,这就大大减轻了我们的资源占用.
I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当
多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以配合使用多线程或多进程等编程方法。
在一段指定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件.
和select函数一起配套使用的宏:
#include
int FD_ZERO(int fd, fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set); //设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位
select函数:返回就绪描述符总数,返回值为0为等待超时,返回值为-1,则select失败.
#include
#include
#include
#include
#include
#include
#include
#include #define MAXFD 10
int socket_init();
void fds_init(int fds[])
{for(int i = 0; i < MAXFD; i++ ){fds[i] = -1;}
}void fds_add(int fds[], int fd)
{for(int i = 0; i < MAXFD; i++ ){if ( fds[i] == -1 ){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 main()
{int sockfd = socket_init();if( sockfd == -1 ){printf("sockfd err\n");exit(0);}int fds[MAXFD];//数组存放描述符fds_init(fds);fds_add(fds,sockfd);//将描述符添加到数组fds中fd_set fdset;//读事件的结合 while( 1 ){FD_ZERO(&fdset);int maxfd = -1;for( int i = 0; i < MAXFD; i++ ){if ( fds[i] == -1 ){continue;}FD_SET(fds[i],&fdset);if ( fds[i] > maxfd ){maxfd = fds[i];}}struct timeval tv = {5,0};int n = select(maxfd+1,&fdset,NULL,NULL,&tv);//阻塞if ( n == -1 ){printf("select err\n");}else if ( n == 0 ){printf("time out\n");}else{for( int i = 0; i < MAXFD; i++ ){if ( fds[i] == -1 ){continue;//跳过无效描述符 -1}if ( FD_ISSET(fds[i],&fdset) )//是否事件就绪 (有没有数据,或者有人连接){if ( fds[i] == sockfd )//accept{struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);if ( c < 0 ){continue;}printf("accept c=%d\n",c);fds_add(fds,c);}else//recv{char buff[128] = {0};int num = recv(fds[i],buff,1,0);//读到数据,-1, 0if ( num <= 0 ){close(fds[i]);fds_del(fds,fds[i]);printf("close\n");}else{printf("read(%d):%s\n",fds[i],buff);send(fds[i],"ok",2,0);}}}}}}
}int socket_init()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if ( sockfd == -1 ){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if ( res == -1 ){return -1;}res = listen(sockfd,5);if ( res == -1 ){return -1;}return sockfd;
}
poll 系统调用和 select 类似,也是在指定时间内轮询一定数量的文件描述符,以测试其
中是否有就绪者。
#include
#include
#include
#include
#include
#include
#include
#include #define MAXFD 10int socket_init();
void fds_init(struct pollfd fds[])
{for(int i = 0; i < MAXFD; i++ ){fds[i].fd = -1;fds[i].events = 0;//fds[i].revents = 0;}
}void fds_add(struct pollfd fds[], int fd)
{for( int i = 0; i < MAXFD; i++ ){if ( fds[i].fd == -1 ){fds[i].fd = fd;fds[i].events = POLLIN;fds[i].revents = 0;break;}}
}void fds_del(struct pollfd fds[], int fd)
{for( int i = 0; i < MAXFD; i++ ){if ( fds[i].fd == fd){fds[i].fd = -1;fds[i].events = 0;fds[i].revents = 0;break;}}
}int accept_client(int sockfd)
{struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);return c;
}int recv_data(int c )
{char buff[128] = {0};int n = recv(c,buff,127,0);// 0 对方关闭连接, -1 recv失败,if ( n > 0 ){printf("read:%s\n",buff);send(c,"ok",2,0);}return n;
}
int main()
{int sockfd = socket_init();if ( sockfd == -1 ){exit(1);}struct pollfd fds[MAXFD];fds_init(fds);//清空数组fds_add(fds,sockfd);//添加sockfd监听套接字到fdswhile( 1 ){int n = poll(fds,MAXFD,5000);//阻塞 if ( n == -1 ){printf("poll err\n");}else if ( n == 0 ){printf("time out\n");}else{//处理就绪的描述符//首先找到就绪的描述符-- sockfd acce close(c);pt , c recv for( int i = 0; i < MAXFD; i++ ){if ( fds[i].fd == -1 )//跳过无效描述符{continue;}if ( fds[i].revents & POLLIN ) //检查有没有读事件就绪{//sockfd, c if ( fds[i].fd == sockfd )//监听套接字 accept{int c = accept_client(sockfd);if ( c >= 0 ){fds_add(fds,c);printf("accept c=%d\n",c);}}else//recv{if( recv_data(fds[i].fd) <= 0 )//连接套接字 recv {close(fds[i].fd);fds_del(fds,fds[i].fd);//移除描述符printf("client close\n");}}}}}}}int socket_init()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if ( sockfd == -1 ){printf("socket err\n");return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if ( res == -1 ){printf("bind err\n");return -1;}res = listen(sockfd,5);if ( res == -1 ){printf("listen err\n");return -1;}return sockfd;
}
epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、poll 有很大差异。首先,epoll 使用一组函数来完成任务,而不是单个函数。其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。
#include
#include
#include
#include
#include
#include
#include
#include #define MAXFD 10
int socket_init();
void fds_init(int fds[])
{for(int i = 0; i < MAXFD; i++ ){fds[i] = -1;}
}void fds_add(int fds[], int fd)
{for(int i = 0; i < MAXFD; i++ ){if ( fds[i] == -1 ){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 main()
{int sockfd = socket_init();if( sockfd == -1 ){printf("sockfd err\n");exit(0);}int fds[MAXFD];//数组存放描述符fds_init(fds);fds_add(fds,sockfd);//将描述符添加到数组fds中fd_set fdset;//读事件的结合 while( 1 ){FD_ZERO(&fdset);int maxfd = -1;for( int i = 0; i < MAXFD; i++ ){if ( fds[i] == -1 ){continue;}FD_SET(fds[i],&fdset);if ( fds[i] > maxfd ){maxfd = fds[i];}}struct timeval tv = {5,0};int n = select(maxfd+1,&fdset,NULL,NULL,&tv);//阻塞if ( n == -1 ){printf("select err\n");}else if ( n == 0 ){printf("time out\n");}else{for( int i = 0; i < MAXFD; i++ ){if ( fds[i] == -1 ){continue;//跳过无效描述符 -1}if ( FD_ISSET(fds[i],&fdset) )//是否事件就绪 (有没有数据,或者有人连接){if ( fds[i] == sockfd )//accept{struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);if ( c < 0 ){continue;}printf("accept c=%d\n",c);fds_add(fds,c);}else//recv{char buff[128] = {0};int num = recv(fds[i],buff,1,0);//读到数据,-1, 0if ( num <= 0 ){close(fds[i]);fds_del(fds,fds[i]);printf("close\n");}else{printf("read(%d):%s\n",fds[i],buff);send(fds[i],"ok",2,0);}}}}}}
}int socket_init()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if ( sockfd == -1 ){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if ( res == -1 ){return -1;}res = listen(sockfd,5);if ( res == -1 ){return -1;}return sockfd;
}
在epoll该方法下存在两种模式分别是LT和ET,这两种模式怎么来理解呢?
LT是epoll的默认操作模式,当epoll_wait函数检测到有事件发生并将通知应用程序,而应用程序不一定必须立即进行处理,这样epoll_wait函数再次检测到此事件的时候还会通知应用程序,直到事件被处理。
而ET模式,只要epoll_wait函数检测到事件发生,通知应用程序立即进行处理,后续的epoll_wait函数将不再检测此事件。因此ET模式在很大程度上降低了同一个事件被epoll触发的次数,因此效率比LT模式高。
简单来讲,LT就是客户端在给服务器发送数据或者服务器向客户端发送数据的时候,recv函数每次只能读取有限个数据,一次将数据读取不完,这样就会导致数据放在了服务器的接收缓存区中,LT模式就是epoll函数会知道该缓存区中有数据,会直接进行读取,而ET呢就是只读取一次,没有将数据读取完成便会在下一次接收数据时继续读取,如果没有接收数据就会一直停留在缓存区中。
ET模式解决的方法:
将套接字设置为非阻塞模式,这样就会使得读取信息时缓冲区没有数据就会返回-1,但是这样我们会发现读取失败和读完都会返回-1,这样我们就用到了errno,类似于标记的一个全局变量,如果缓冲区中没有数据返回-1就会看到errno为 EAGAIN 或者EWOULDBLOCK,情况不同返回的errno不同,这样我们就可以区分出是那种情况返回-1了。代码如下
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include #define MAXFD 10//SIGPIPE
//
int socket_init();
void set_nonblock(int fd)
{int oldfl = fcntl(fd,F_GETFL);int newfl = oldfl | O_NONBLOCK;//非阻塞if ( fcntl(fd,F_SETFL,newfl) == -1 ){printf("fcntl set err\n");}
}
void epoll_add(int epfd, int fd)
{struct epoll_event ev;ev.events = EPOLLIN|EPOLLET;ev.data.fd = fd;set_nonblock(fd);//设置非阻塞标志if ( epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1 ){printf("epoll add err\n");}
}
void epoll_del(int epfd, int fd)
{if( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1 ){printf("epoll del err\n");}
}int accept_client(int sockfd)
{struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);return c;
}int recv_data(int fd)
{int res = -1;while( 1 ){char buff[128] = {0};int n = recv(fd,buff,1,0);if ( n == -1 ){if ( errno == EAGAIN || errno == EWOULDBLOCK ){res = 1;send(fd,"ok",2,0);}else{printf("recv err\n");res = -1;} break;}else if ( n == 0 ){res = 0;break;}else{printf("read:%s\n",buff);}}return res;
}
int main()
{int sockfd = socket_init();if ( sockfd == -1 ){printf("sockfd err\n");exit(1);}int epfd = epoll_create(MAXFD);//创建内核事件表if ( epfd == -1 ){printf("epoll create err\n");exit(1);}epoll_add(epfd,sockfd);//向内核事件表中添加监听套接字struct epoll_event evs[MAXFD];//10, 存放就绪描述符while( 1 ){int n = epoll_wait(epfd,evs,MAXFD,5000);//获取就绪描述符 -- if( n == -1 ){printf("epoll wait err\n");}else if ( n == 0 ){printf("time out\n");}else{for( int i = 0; i < n; i++ ){int fd = evs[i].data.fd;if ( evs[i].events & EPOLLIN ){if ( fd == sockfd)//accept(){int c = accept_client(fd);if ( c >= 0 ){printf("accept c = %d\n",c);epoll_add(epfd,c);//将连接套接字c添加到内核事件表}}else//recv(){int num = recv_data(fd);if ( num == 0 ){epoll_del(epfd,fd);//先移除close(fd);//后关闭printf("client close\n");}}}}}}}
int socket_init()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if ( sockfd == -1 ){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("127.0.0.1");int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if ( res == -1 ){return -1;}res = listen(sockfd,5);if ( res == -1 ){return -1;}return sockfd;
}
类型 | select | poll | eopll |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | RBTree |
IO效率 | O(n) | O(n) | O(1) |
最大连接数 | 1024 | 无上限 | 无上限 |
fd拷贝 | 每次调用select都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并且保存,之后每一次epoll_wait不拷贝 |
模式 | LT | LT | ET或者LT |
上表中阐述了三者的区别,select和poll的操作方式是遍历,所以IO效率是O(n),相信大家都可以从代码中看出来,但是回调是什么呢?为什么IO效率是O(1)呢?回调可以这么理解,老师要检查一个班里学生的作业,老师一个一个人检查,这就是遍历,而回调就是提前发布了命令,写了作业的把作业交上来,这样便不需要进行遍历,直接就知道谁写了作业。
fd拷贝呢,select和poll是每次调用函数时,将整个集合的数据拷贝一份到内核中,内核进行运算得到有没有就绪的描述符,存在就绪描述符我们将进行下一步重新遍历寻找就绪的描述符并且做处理,而epoll呢在eoill_ctl函数时就将数据从拷贝进了内核,之后运行epoll函数时就不需要进行拷贝,内核会直接将就绪描述符存放进evs数组中,后面也不需要进行整体遍历,直接进行处理该数组即可。
上一篇:golang 环境搭建(二)