Unix网络编程之epoll函数模拟10万客户端链接服务器
来源:互联网 发布:sql server 2008教材 编辑:程序博客网 时间:2024/05/17 06:53
1.epoll函数是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
2.epoll与select和poll的区别。
相比select和poll,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。
内核中的select与poll的实现是采用轮询的方式来处理的,轮询的fd数目越多,自然就越耗时。
epoll的实现主要基于回调的,如果fd有期望的事情发生就通过回调函数将其加入到epoll就绪队列当中。也就是说他只关心活跃的fd,与fd数目无关。
内核/用户空间内存拷贝的问题。如何让内核吧fd关注的消息通知给用户空间呢?在这个问题上,select与poll采用的内存拷贝的方式,而epoll采用的是内存共享的方式。
epoll不仅会告诉应用程序有I/O事件的到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的。因此,根据这些信息应用程序就能直接定位到事件,而不必遍历整个fd集合。
3、epoll的优点
<1>支持一个进程打开大数 目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
<2>IO 效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行 操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
<3>使用mmap加速内核 与用户空间的消息传递。
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。
4、epoll的工作模式
令人高兴的是,2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦 是epoll有
2种工作方式:LT和ET。LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就
绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。传统的
select/poll都是这种模型的代表.ET (edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核
通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到你做了某些操作导致那个文件
描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致 了一个EWOULDBLOCK 错误)。但是请注意,
如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知,不过在TCP协议中,ET模式的加速效用仍需要更多的
benchmark确认。epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用,具体用法请参考http://www.xmailserver.org/linux-patches/nio-improve.html ,
在http://www.kegel.com/rn/也有一个完整的例子,大家一看就知道如何使用了
Leader/follower模式线程 pool实现,以及和epoll的配合。
5、 epoll的使用方法
首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的
所有操作 将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。之后在你的网络主循环里面,每一帧的调用
epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);其中kdpfd为用epoll_create创建之后的句柄,events是一个 epoll_event*的指针,当epoll_wait这个函
数操作成功之后,epoll_events里面将储存所有的读写事件。 max_events是当前需要监听的所有socket句柄数。最后一个timeout是 epoll_wait的超时,
为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没 有事件,则范围。一
般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环 的效率。
Epoll模型主要负责对大量并发用户的请求进行及时处理,完成服务器与客户端的数据交互。其具体的实现步骤如下:
(a) 使用epoll_create()函数创建文件描述,设定将可管理的最大socket描述符数目。
(b) 创建与epoll关联的接收线程,应用程序可以创建多个接收线程来处理epoll上的读通知事件,线程的数量依赖于程序的具体需要。
(c) 创建一个侦听socket描述符ListenSock;将该描述符设定为非阻塞模式,调用Listen()函数在套接字上侦听有无新的连接请求,在 epoll_event结构中设置要处理的事件类型EPOLLIN,工作方式为 epoll_ET,以提高工作效率,同时使用epoll_ctl()注册事件,最后启动网络监视线程。
(d) 网络监视线程启动循环,epoll_wait()等待epoll事件发生。
(e) 如果epoll事件表明有新的连接请求,则调用accept()函数,将用户socket描述符添加到epoll_da
(f) 如果epoll事件表明socket描述符上有数据可读,则将该socket描述符加入可读队列,通知接收线程读入数据,并将接收到的数据放入到接收数据 的链表中,经逻辑处理后,将反馈的数据包放入到发送数据链表中,等待由发送线程发送。
代码:
#include "unp.h"typedef std::vector<struct epoll_event> EventList;int main(void){ int count = 0; signal(SIGPIPE,handle_sigpipe); signal(SIGCHLD,handle_sigchld); int listenfd; if((listenfd = socket(AF_INET,SOCK_STREAM,0))<0) ERR_EXIT("socket"); struct sockaddr_in cliaddr,servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(45678); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) ERR_EXIT("bind"); listen(listenfd,SOMAXCONN); int on = 1; if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0) ERR_EXIT("setsockopt"); std::vector<int> clients; int epollfd; epollfd = epoll_create1(EPOLL_CLOEXEC); struct epoll_event event; event.data.fd = listenfd; event.events = EPOLLIN | EPOLLET; epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&event); EventList events(16); struct sockaddr_in peeraddr; socklen_t peerlen; int conn; int i; int nready; while(1){ nready = epoll_wait(epollfd,&*events.begin(),static_cast<int>(events.size()),-1); /*我们在这里等待events队列里面是否有 if(nready == -1){ 事件到来*/ if(errno == EINTR) continue; ERR_EXIT("epoll_wait"); } if(nready == 0){ continue; } if((size_t)nready == events.size()) //如果队列满了,重新开辟新的空间。为原来空间的2倍。 events.resize(events.size()*2); for(i = 0;i<nready;i++){ if(events[i].data.fd == listenfd){ //如果有新的客户端来了 peerlen = sizeof(peeraddr); conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen); if(conn == -1) ERR_EXIT("accept"); printf("ip = %s ,port = %d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); printf("count = %d\n",++count); clients.push_back(conn); activate_nonblock(conn); //先设置该描述符为非阻塞 event.data.fd = conn; //设置eventdata.fd为conn event.events = EPOLLIN | EPOLLET; epoll_ctl(epollfd,EPOLL_CTL_ADD,conn,&event); //将该event描述符注册到epollfd关注的队列里面,并为event建立一个链接到conn。 } else if(events[i].events & EPOLLIN){ //一般这个events里面的event都是有事件到来的。 conn = events[i].data.fd; //获取这个events[i]里面的套接字描述符。 if(conn < 0) continue; char recvbuf[1024]; int ret = readline(conn,recvbuf,1024); if(ret == -1) ERR_EXIT("readline"); if(ret == 0){ //如果对方关闭了这个套接字。 printf("client close \n"); close(conn); event = events[i]; epoll_ctl(epollfd,EPOLL_CTL_DEL,conn,&event); //我们就得将这个event从epollfd所关心的描述符集合里面将它删除。 clients.erase(std::remove(clients.begin(),clients.end(),conn),clients.end());//并且我们还要将它从clients结合里面将它remove掉 } fputs(recvbuf,stdout); writen(conn,recvbuf,strlen(recvbuf)); //回射客户端。 } } } return 0;}
--------unp.h----------
代码:
#include<stdio.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#include<unistd.h>#include<netinet/in.h>#include<arpa/inet.h>#include<errno.h>#include<stdlib.h>#include<signal.h>#include<fcntl.h>#include<sys/epoll.h>#include<vector>#include<algorithm>#include<sys/wait.h>#define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0)struct packet{ int len; char buf[1024];};typedef std::vector<struct epoll_event> EventList;ssize_t readn(int fd, void *buf,size_t count);ssize_t writen(int fd,const void *buf,size_t count);ssize_t readline(int sockfd,void *buf,size_t len);ssize_t recv_peek(int sockfd,void *buf,size_t len);void activate_nonblock(int fd);void handle_sigchld(int sig);void handle_sigpipe(int sig);
--------client.c--------
代码:
#include "unp.h"int num =0;int main(void){ int sock; int i = 0; while(1){ if((sock = socket(AF_INET,SOCK_STREAM,0))<0) ERR_EXIT("SOCKET"); struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(45678); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) //将connect写到while死循环里,直到客户端的内存被客户端链接占满了 ERR_EXIT("CONNECT"); printf("%d \n",++num); struct sockaddr_in myaddr; socklen_t mylen; getsockname(sock,(struct sockaddr*)&myaddr,&mylen); printf("ip = %s ,port = %d \n",inet_ntoa(myaddr.sin_addr),ntohs(myaddr.sin_port)); } close(sock); return 0;}
------------rw_h.c-------------
代码:
#include "unp.h"ssize_t readn(int fd,void *buf,size_t count){ size_t nleft = count; ssize_t nreadn = 0; char * ptr = (char*)buf; int n; while(1){ n = read(fd,ptr,nleft); if(n == 0){ break; }else if(n<0){ break; } nleft -= n; nreadn += n; ptr += n; } return nreadn;}ssize_t writen(int fd,const void *buf,size_t count){ size_t nleft = count; ssize_t nwriten = 0; char * ptr = (char *) buf; int n; while(1){ n = write(fd,ptr,nleft); if(n == 0){ break; }else if(n<0){ break; } nleft -= n; nwriten += n; ptr += n; } return nwriten;}ssize_t recv_peek(int sockfd,void *buf,size_t len){ while(1){ int ret = recv(sockfd,buf,len,MSG_PEEK); //从sockfd里面偷窥一些数据。 if(ret ==-1) continue; return ret; }}ssize_t readline(int fd,void *buf,size_t len ){ char *ptr = (char*)buf; size_t nleft = len; ssize_t ret; ssize_t nread = 0; char *bufp = (char*)buf; while(1){ ret = recv_peek(fd,ptr,MSG_PEEK); if(ret < 0) return ret; if(ret == 0 ){ return ret; } nread = ret; int i; for(i=0;i<nread;i++){ if(bufp[i] == '\n') { ret = readn(fd,bufp,i+1); if(ret != i+1) exit(EXIT_FAILURE); return len-nread-ret; } } if(nread > nleft) exit(EXIT_FAILURE); nleft -= nread; ret = readn(fd,bufp,nread); if(ret != nread) exit(EXIT_FAILURE); } return -1;}void activate_nonblock(int fd){ int ret; int flags = fcntl(fd,F_GETFL); if(flags == -1) ERR_EXIT("fcntl"); flags |= O_NONBLOCK; ret = fcntl(fd,F_SETFL,flags); if(ret == -1) ERR_EXIT("fcntl");}void handle_sigchld(int sig){ while(waitpid(-1,NULL,WNOHANG) > 0) ;}void handle_sigpipe(int sig){ printf("recv a sig = %d\n",sig);}int read_timeout(int fd,unsigned int wait_seconds){ int ret = 0; if(wait_seconds > 0){ fd_set set; struct timeval timeout; FD_ZERO(&set); FD_SET(fd,&set); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do{ ret = select(fd+1,&set,NULL,NULL,&timeout); }while(ret < 0 && errno == EINTR); if(ret == 0){ ret = -1; errno = ETIMEDOUT; } else if(ret == 1) ret = 0; } return ret;}int write_timeout(int fd,unsigned int wait_seconds){ int ret = 0; if(wait_seconds > 0){ fd_set set; struct timeval timeout; FD_ZERO(&set); FD_SET(fd,&set); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do{ ret = select (fd+1,NULL,&set,NULL,&timeout); }while(ret <0 && errno == EINTR); if(ret == 0){ ret = -1; errno == ETIMEDOUT; } else if(ret == 1){ ret = 0; } } return ret;}int accept_timeout(int fd,struct sockaddr_in *addr,unsigned int wait_seconds){ int ret = 0; socklen_t addrlen = sizeof(struct sockaddr_in); if(wait_seconds > 0){ fd_set set; struct timeval timeout; FD_ZERO(&set); FD_SET(fd,&set); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do{ ret = select(fd+1,&set,NULL,NULL,&timeout); }while(ret < 0 && errno == EINTR); if(ret == -1) return -1; else if(ret == 0){ errno = ETIMEDOUT; } } if(addr != NULL){ accept(fd,(struct sockaddr*)addr,&addrlen); }else ret = accept(fd,NULL,NULL); if(ret == -1) ERR_EXIT("accept"); return ret;}void deactivate_nonblock(int fd){ int ret; int flags = fcntl(fd,F_GETFL); if(flags ==-1) ERR_EXIT("fcntl"); flags &= ~O_NONBLOCK; ret = fcntl(fd,F_SETFL,flags); if(ret == -1) ERR_EXIT("fcntl");}int connect_timeout(int fd,struct sockaddr_in *addr,unsigned int wait_seconds){ int ret; socklen_t addrlen = sizeof(struct sockaddr_in); if(wait_seconds > 0){ activate_nonblock(fd); } ret = connect(fd,(struct sockaddr*)addr,addrlen); if(ret < 0&& errno == EINPROGRESS){ fd_set set; struct timeval timeout; FD_ZERO(&set); FD_SET(fd,&set); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do{ ret = select(fd+1,NULL,&set,NULL,&timeout); }while(ret < 0 && errno == EINTR); if(ret == 0){ ret = -1; errno = ETIMEDOUT; }else if(ret < 0) return -1; else if(ret ==1) { int err; socklen_t socklen = sizeof(err); int sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&socklen); if(err == 0){ ret = 0; }else{ errno = err; ret = -1; } } } if(wait_seconds > 0) { deactivate_nonblock(fd); } return ret;}
- Unix网络编程之epoll函数模拟10万客户端链接服务器
- UNIX网络编程之epoll
- Unix网络编程之poll函数实现服务器反射客户端-server.c
- 《unix网络编程》(16)epoll函数
- Unix网络编程之点对点聊天-客户端与服务器实现
- Linux网络编程之epoll服务器
- unix网络编程之socket:epoll 系列函数简介、与select、poll 的区别
- Unix网络编程之服务器回射客户端多线程编程01-server.c
- Unix网络编程之服务器回射客户端多线程编程02-server.c client.c
- UNIX网络编程——使用select函数编写客户端和服务器
- UNIX网络编程——使用select函数编写客户端和服务器
- Linux-C网络编程之epoll函数
- unix网络编程之从selelct谈到epoll
- unix网络编程之从selelct谈到epoll
- UNIX网络编程之epoll 的accept , read, write(重要)
- UNIX网络编程之epoll 的accept , read, write
- UNIX网络编程之epoll 的accept , read, write(重要)
- 【UNIX网络编程】自学epoll,使用epoll修改回射服务器程序
- 区块链:起源、原理及应用
- Flask Web 开发 测试
- Android横竖屏切换导致Actvivty生命周期变化和解决方法
- Leetcode 121. Best Time to Buy and Sell Stock 股票买卖 解题报告
- 237. Delete Node in a Linked List
- Unix网络编程之epoll函数模拟10万客户端链接服务器
- 自己写的贪吃蛇(3)
- iOS 推送 逻辑
- js判断来路是否是百度等搜索索引进行弹窗或自动跳转
- DocX.dll(无需安装OFFICE操作word文档)
- 分布式Session问题
- 自学-ES6篇-Proxy和Reflect
- Mongoose学习参考文档
- 第7周项目3-负数把正数赶出队列