高并发多路IO之select,poll和epoll模型区别与代码实现
多路IO之select
优点:单进程下支持高并发,可以跨平台
缺点:多次从内核到应用,应用到内核的数组拷贝;
每次内核都会重置填写的数据
最大支持1024客户端,原因在于fd_set定义使用了FD_SETSIZE,大小为1024;
以下是select模型server代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <sys/wait.h> #include <netinet/in.h> #include <errno.h> #include <fcntl.h> #include <sys/select.h> #include <ctype.h> int main(){ int lfd = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in serv; bzero(&serv,sizeof(serv)); serv.sin_port = htons(8888); serv.sin_family = AF_INET; serv.sin_addr.s_addr = htonl(INADDR_ANY); //reset port int opt = 1; setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); bind(lfd,(struct sockaddr*)&serv,sizeof(serv)); listen(lfd,128); fd_set rdset;//readevent fd_set allset;//bak readevent FD_ZERO(&rdset); FD_SET(lfd,&rdset); allset = rdset; struct sockaddr_in client; socklen_t len = sizeof(client); int nfds = lfd; int nready = 0; while(1){ rdset = allset;//备份传给内核 //阻塞等待事件就绪 nready = select(nfds+1,&rdset,NULL,NULL,NULL); if(FD_ISSET(lfd,&rdset)){ //有新连接事件,将得到的CFD加入到集合 int cfd = accept(lfd,(struct sockaddr*)&client,&len); if(cfd > 0){ //rdset是一个传入传出集合,每次都会重置 FD_SET(cfd,&allset); } if(nfds < cfd){ nfds = cfd; } nready --; //如果就绪事件就一个,且是新连接就跳出循环 if(nready <= 0) continue; } int i = 0; for(i = lfd+1;i<nfds +1;i++){ if(FD_ISSET(i,&rdset)){ char buf[256] = {0}; int ret = read(i,buf,sizeof(buf)); if(ret < 0){ perror("read err"); close(i); FD_CLR(i,&allset); } else if (ret == 0){ close(i);//client closed FD_CLR(i,&allset); } else{ int j = 0; for(;j<ret;j++){ buf[j] = toupper(buf[j]); } write(i,buf,ret); } if(--nready <= 0) break;//no event.jump for. } } } close(lfd); return 0; }
多路IO之POLL模型:
POLL的原理与select相同,比select改进的地方:
1,请求和返回分离,避免每次都要重设数组
2,可以突破1024限制,poll是由打开文件的上限决定,可以使用ulimit命令查看上限
3,不能跨平台
poll代码解析:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <sys/wait.h> #include <netinet/in.h> #include <errno.h> #include <fcntl.h> #include <ctype.h> #include <poll.h> #include <arpa/inet.h> #define _MAXLINE_ 80 #define _SERVER_PORT_ 8888 #define _MAX_OPEN 1024 int main(){ int i,maxi; char strIP[16]; int lfd = socket(AF_INET,SOCK_STREAM,0); struct pollfd client[_MAX_OPEN]; struct sockaddr_in clientaddr,servaddr; int len = sizeof(clientaddr); bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(_SERVER_PORT_); //set reuse port int opt = 1; setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); if(bind(lfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0){ perror("bind err"); return -1; } listen(lfd ,128); client[0].fd = lfd;//监听第一个文件描述符 client[0].events = POLLIN;//监听读事件 for(i = 1; i <_MAX_OPEN;i++){ client[i].fd = -1;//用-1初始化,因为0也是描述符 } maxi = 0;//记录client数组有效最大元素下标 while(1){ int nready = poll(client,maxi+1,-1); //判断是否有新连接 if(client[0].revents & POLLIN){ //此处不会阻塞 int cfd = accept(lfd,(struct sockaddr*)&clientaddr,&len); printf("recv form %s:%d\n", inet_ntop(AF_INET,&clientaddr.sin_addr,strIP,sizeof(strIP)), ntohs(clientaddr.sin_port)); for(i = 1;i<_MAX_OPEN;i++){ if(client[i].fd < 0){ client[i].fd = cfd; break; } } if(i == _MAX_OPEN){ //最大客户连接上限 printf("max connected...\n"); continue; } client[i].events = POLLIN; if (i > maxi) maxi = i; if(--nready <= 0) continue;//没有更多就绪事件,继续回到POLL阻塞 } for(i = 1;i<=maxi;i++){ //前面的IF没有满足,说明没有新连接,而是读事件 int cfd; //先找到第一个大于0的文件描述符 if((cfd = client[i].fd) < 0) continue; if(client[i].revents & POLLIN){ char buf[_MAXLINE_] = {0}; int ret = read(cfd,buf,sizeof(buf)); if(ret < 0){ if(errno == ECONNRESET){ printf("client[%d] aborted connection\n",i); close(cfd); client[i].fd =-1; //POLL中不需要像SELECT一样移除,直接置-1即可 } else{ perror("read error"); exit(-1); } } else if(ret == 0){ printf("client[%d] closed\n",i); close(cfd); client[i].fd = -1; } else{ write(cfd,buf,ret); } if(--nready <= 0) break; } } } close(lfd); return 0; }
多路IO之EPOLL:
不管是select,还是poll,都需要遍历数组轮询,而且select仅支持1024个客户端,在大量并发,少量活跃的情况下效率较低,也就滋生了epoll模型。
1,可以突破1024限制,不跨平台
2,无须遍历整个文件描述符集,只需遍历被内核IO事件异步唤醒,而加入ready队列的文件描述符。
3,除了select/poll的IO事件水平触发(level triggered)外,还提供边沿触发(edge Triggered),可以缓存IO状态,减少epoll_wait调用,提高效率
代码原型如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <sys/wait.h> #include <netinet/in.h> #include <errno.h> #include <fcntl.h> #include <sys/epoll.h> int main(){ int lfd = socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in serv; bzero(&serv,sizeof(serv)); serv.sin_addr.s_addr = htonl(INADDR_ANY); serv.sin_port = htons(8888); serv.sin_family = AF_INET; int opt = 1; setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); bind(lfd,(struct sockaddr*)&serv,sizeof(serv)); listen(lfd,128); struct sockaddr_in client; socklen_t len = sizeof(client); //create epoll node int epfd = epoll_create(1); struct epoll_event ev,evs[1024]; ev.data.fd = lfd; ev.events = EPOLLIN; epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); while(1){ int nready = epoll_wait(epfd,evs,1024,-1); if(nready > 0){ int i = 0; for(;i<nready;i++){ if(evs[i].data.fd == lfd){ if(evs[i].events & EPOLLIN){ int cfd = accept(lfd,(struct sockaddr*)&client,&len); if(cfd > 0){ ev.data.fd = cfd; //将新的连接上树 epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev); } } } else{ //处理读事 if(evs[i].events & EPOLLIN){ char buf[256]={0}; int ret = read(evs[i].data.fd,buf,sizeof(buf)); if(ret > 0){ write(evs[i].data.fd,buf,ret); } else if(ret == 0){ //client closed close(evs[i].data.fd); ev.data.fd = evs[i].data.fd; epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&ev); } else{ perror("read err"); close(evs[i].data.fd); ev.data.fd = evs[i].data.fd; epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&ev); } } } } } } close(epfd); close(lfd); return 0; }