引言
"在计算机网络编程中,多路IO技术是非常常见的一种技术。其中,Poll函数和Epoll函数是最为常用的两种多路IO技术。这两种技术可以帮助服务器端处理多个客户端的并发请求,提高了服务器的性能。本文将介绍Poll和Epoll函数的使用方法,并探讨了在服务器开发中使用这两种技术的流程和注意事项。"
poll函数介绍
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
(man poll 调用)
函数说明: 跟select类似, 委托内核监控可读, 可写, 异常事件
函数参数:
fds: 一个struct pollfd结构体数组的首地址
struct pollfd {
int fd;//要监控的文件描述符,如果fd为-1, 表示内核不再监控
short events; //输入参数, 表示告诉内核要监控的事件, 读事件, 写事件, 异常事件
short revents;//输出参数, 表示内核告诉应用程序有哪些文件描述符有事件发生
};
events/revents:
POLLIN:可读事件,让内核监控读事件就要写这个
POLLOUT: 可写事件,缓冲区未满就可写
POLLERR: 异常事件
nfds: 告诉内核监控的范围, 具体是: 数组下标的最大值+1
timeout:
=0: 不阻塞, 立刻返回
-1: 表示一直阻塞, 直到有事件发生
>0: 表示阻塞时长, 在时长范围内若有事件发生会立刻返回;
如果超过了时长也会立刻返回
函数返回值:
>0: 发生变化的文件描述符的个数
=0: 没有文件描述符发生变化
-1: 表示异常
poll函数开发流程
1 创建socket ,得到监听文件描述符,lfd ----- socket();
2 设置端口复用----------setsockopt()
3 绑定 ------ bind()
4
struct pollfd client[1024]; client[0].fd = lfd; // 放在哪都行,放在最俩头方便使用client[0].events = POLLIN; //监控读事件,如果也让其监控可写事件,用或// 设置为fd 为-1 ,表示内核不在监控,这是一个初始化int maxi = 0; // 定义最大数组下标for(int i = 0;i < 1024;i ++){client[i].fd = -1;}//委托内核持续监控k= 0;while(1){nready = poll(client,maxi + 1,-1);//异常情况if(nready < 0 ){if(error == EINTR){continue;}break;}if(client[0].revents = POLLIN){//接受新的客户端连接k ++;cfd = Accept(lfd,NULL,NULL);/*继续委托内核监听事件寻找在client 数组中可用位置*/for(i = 0;i < 1024;i ++ ){if(client[i ].fd ==-1 ){client.fd[i] = cfd;client.fd[i] = POLLIN;break;}}//客户端连接数达到最大值if(i == 1024){close(cfd);continue; //退出,可能会有客户端连接退出,方便继续寻找}//修改client 数组下标最大值 if(maxi < i )maxi = i;if(--nready == 0 )continue;}//下面是有客户端发送数据的情况for(i = 1;i <= maxi;i ++){//如果client数组中fd 为-1,表示已经不再让内核监控了if(client[i].fd == -1)continue;if(client[i].revents == POLLIN){sockfd = client[i].fd;memset(buf,0x00,sizeof(buf));//read 数据n = Read(sockfd, buf,sizeof(buf));if(n <= 0){printf("read error or client closed,n =[%d]\n",n);close(sockfd);client[i].fd = -1; //告诉内核不再监控}else {printf("read error,n == [%d],buf==[%s]\n,"n,buf);//发送数据给客户端Write(sockfd,buf,n);}if(--nready == 0 ){break;}}}close(lfd);}
多路IO-epoll (重点)
将检测文件描述符的变化委托给内核去处理, 然后内核将发生变化的文件描述符对应的
事件返回给应用程序.
头文件
#include <sys/epoll.h>
函数
int epoll_create(int size)
函数说明:创建一棵poll树,返回一个数根节点
函数参数:size:必须传一个大于0的数
返回值:返回个文件描述符,这个文件描述符就表示epoll树的树根节点
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
函数说明:将fd上的epoll树,从树上删除和修改
函数参数:
epfd:epoll树的树根节点
op:
EPOLL_CTL_ADD: 添加事件节点到树 上
EPOLL_CTL_DEL: 从树上删除事件节点
EPOLL_CTL_MOD: 修改树上对应的事件节点fd:要操做的文件描述符
event :
event.events 常用的有:
EPOLLIN: 读事件
EPOLLOUT: 写事件
EPOLLERR: 错误事件
EPOLLET: 边缘触发模式
event.fd: 要监控的事件对应的文件描述符typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
struct epoll_event{
uint32 events; / * Epoll events */
epoll_data data; /* User data variable */
};
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数说明:等待内核返回事件发生
参数说明:
epfd: epoll树根
events: 传出参数, 其实是一个事件结构体数组
maxevents: 数组大小
timeout:
-1: 表示永久阻塞
0: 立即返回
>0: 表示超时等待事件
返回值:
成功: 返回发生事件的个数
失败: 若timeout=0, 没有事件发生则返回; 返回-1, 设置errno值,
使用epoll 模型开发服务器流程
1:创建socket,得到监听文件描述符lfd ---- socket()
2: 设置端口复用 ----- setsockopt()
3: 绑定 ------ bind()
4: 监听 -------- listen()
5. 创建一棵epoll树
开发完整的代码
//EPOLL 模型测试
#include "wrap.h"
#include <sys/epoll.h>
#include <ctype.h>
int main()
{int ret;int n;int nready;int lfd;int cfd;int sockfd;char buf[1024];socklen_t socklen;struct sockaddr_in svraddr;struct epoll_event ev;struct epoll_event events[1024];int k;int i;//创建socketlfd = Socket(AF_INET,SOCK_STREAM,0);//设置文件描述符为端口复用int opt = 1;setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));//绑定svraddr.sin_family = AF_INET;svraddr.sin_addr.s_addr = htonl(INADDR_ANY);svraddr.sin_port = htons(8888);Bind(lfd,(struct sockaddr *)&svraddr,sizeof(struct sockaddr_in));//ListenListen(lfd,128);//创建一棵epoll树int epfd = epoll_create(1024);if(epfd < 0 ){perror("create epoll error");return -1;} ev.data.fd = lfd;ev.events = EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); //lfd 对应的事件节点上树while(1){nready = epoll_wait(epfd,events,1024,-1); //等待内核返回事件 if(nready < 0){perror("epoll_wait error");if(nready == EINTR) //判断是否收到了中断信号 {continue;}break;}for(i = 0;i < nready;i ++) //小于发生事件的个数 {//有客户端连接发来请求 sockfd = events[i].data.fd;if(sockfd == lfd) {cfd = Accept(lfd,NULL,NULL);ev.data.fd = cfd;ev.events = EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);}//有客户端发送数据过来else {memset(buf,0x00,sizeof(buf));n = Read(sockfd,buf,sizeof(buf));if(n <= 0){close(sockfd);epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL); //把sockfd从epfd树上删除 } else {for(k = 0;k < n;k ++){buf[k] = toupper(buf[k]); //返回大写 }Write(sockfd,buf,n);}}}}close(epfd);close(lfd);return 0;
}
epoll 的两种模式 ET 和 LT 模式
epoll 的LT模式:
epoll 默认情况是LT模式,在这种情况下,如果读数据一次性没有读完,
缓冲区还有可读数据,则epoll_wait还会再次通知。
epoll 的ET模式:
如果将epoll设置为ET模式,若读数据的时候一次性没有读完,则epoll_wait不再通知
直到下次有新的数据
用ET模式下,为了防止第二个客户端可以正常连接,并且发送数据,需要将socket设置为非阻塞模式
ET设置了非阻塞模式是因为使用了边缘触发模式(EPOLLET)。在边缘触发模式下,当有数据可读时,只会触发一次EPOLLIN事件,如果该次读取没有将缓冲区中的数据全部读取完毕,下次还是会触发EPOLLIN事件。因此,为了保证每次读取完整的数据,需要将socket设置为非阻塞模式,避免在缓冲区没有全部读取完毕时进行阻塞。
代码:
//EPOLL 模型测试 ET
#include "wrap.h"
#include <sys/epoll.h>
#include <ctype.h>
#include <fcntl.h>
int main()
{int ret;int n;int nready;int lfd;int cfd;int sockfd;char buf[1024];socklen_t socklen;struct sockaddr_in svraddr;struct epoll_event ev;struct epoll_event events[1024];int k;int i;//创建socketlfd = Socket(AF_INET,SOCK_STREAM,0);//设置文件描述符为端口复用int opt = 1;setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));//绑定svraddr.sin_family = AF_INET;svraddr.sin_addr.s_addr = htonl(INADDR_ANY);svraddr.sin_port = htons(8888);Bind(lfd,(struct sockaddr *)&svraddr,sizeof(struct sockaddr_in));//ListenListen(lfd,128);//创建一棵epoll树int epfd = epoll_create(1024);if(epfd < 0 ){perror("create epoll error");return -1;} ev.data.fd = lfd;ev.events = EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); //lfd 对应的事件节点上树while(1){nready = epoll_wait(epfd,events,1024,-1); //等待内核返回事件 if(nready < 0){perror("epoll_wait error");if(nready == EINTR) //判断是否收到了中断信号 {continue;}break;}for(i = 0;i < nready;i ++) //小于发生事件的个数 {//有客户端连接发来请求 sockfd = events[i].data.fd;if(sockfd == lfd) {cfd = Accept(lfd,NULL,NULL);ev.data.fd = cfd;ev.events = EPOLLIN | EPOLLET; //epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);//将cfd设置为非阻塞模式int flag = fcntl(cfd, F_GETFL);flag |= O_NONBLOCK; //O_NONBLOCK(非阻塞)标志位置为1。fcntl(cfd, F_SETFL, flag);}//有客户端发送数据过来else {memset(buf,0x00,sizeof(buf));while(1){n = Read(sockfd,buf,sizeof(buf));printf("n == [%d]\n",n);if(n == -1){printf("read over,n == [%d]\n",n);break;}if(n < 0 || (n <0 && n!=-1)) //对方关闭连接,或者异常的情况 {printf("n == [%d],buf == [%s]\n",n,buf);close(sockfd);epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL); //把sockfd从epfd树上删除 break;} else {printf("n == [%d],buf == [%s]\n",n,buf);for(k = 0;k < n;k ++){buf[k] = toupper(buf[k]); //返回大写 }Write(sockfd,buf,n);}}}}}close(epfd);close(lfd);return 0;
}