LinuxI/O多路复用转接服务器——epoll模型实现
epoll函数
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
epoll函数组
epoll_create函数
原型:int epoll_create(int size)
作用:创建一个 epoll 对象,返回该对象的描述符,注意要使用 close 关闭该描述符。
参数:size:创建的红黑树的监听节点数量。(仅供内核参考)
返回值:
成功:指向新创建的红黑树的根结点的fd。
失败:-1 errno
epoll_ctl函数
原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
作用:控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
参数:
epfd:epoll_creat的句柄(即epoll_create函数返回值)
op:对该监听红黑树所做的操作
(1)EPOLL_CTL_ADD (添加fd到监听红黑树)
(2)EPOLL_CTL_MOD (修改fd在监听红黑树上的监听事件)
(3)EPOLL_CTL_DEL (将一个fd从监听红黑树上删除)
fd:待监听的fd
event:本质是struct epoll_event结构体的地址
struct epoll_event {
__uint32_t events; /* Epoll events 【EPOLLIN,EPOLLOUT,EPOLLERR 等】*/
epoll_data_t data; /* User data variable */
};
//联合体
typedef union epoll_data {
void *ptr;//泛型指针(内核自动调回调函数)
int fd;//对应监听事件
uint32_t u32;
uint64_t u64;
} epoll_data_t;
返回值:
成功:0
失败:-1 errno
epoll_wait函数
原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
作用:等待所监控文件描述符上有事件的产生。
参数:
events:用来存内核得到事件的集合。输出满足监听条件的fd结构体。【数组】
maxevents:【数组元素的总个数】。告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。
timeout:超时时间
-1:阻塞
0:立即返回,非阻塞
>0:指定毫秒
返回值:
大于0:有多少文件描述符就绪(满足监听的总个数,用作循环上限)
等于0:没有fd满足监听事件
等于-1:失败 ,errno
epoll实现实现I/O多路复用服务器
epoll模型
step1:创建套接字,绑定地址信息,设置监听上限;
step2:创建监听红黑树(epoll_create),返回用作其他epoll系统调用的参数(文件描述符efd),用来指定访问的内核事件表;
step3:将文件描述符fd上的注册事件添加到(事件表)红黑树上(epoll_ctl);
step4:调用epoll_wait函数进行循环监听,监听到事件,将所有就绪事件从内核事件表(由efd指定)复制到传出数组;
step5:判断返回数组元素,若监听套接字lfd满足,则等待客户端连接(accept),若数据通信套接字满足,则调用read函数读取数据并完成大小写转换,调用write函数写回。
程序实现
服务端程序
#include<iostream>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include "wrap.h"
using namespace std;
//定义服务端端口号
#define SERVER_PORT 9527
#define OPEN_MAX 1024
int main (int argc ,char*argv[])
{
int i,n;
//int client[FD_SETSIZE];//自定义数组,大小为1024
int nready=0;//保存epoll_wait函数返回值,记录满足监听事件的fd个数
int lfd=0;//用于监听的套接字
int cfd=0;//用于通信的套接字
int sockfd=0;
int efd=0;//用于接收epoll_create函数返回值
int maxi;//用于检索客户端文件描述符的下标
char buf[BUFSIZ],str[INET_ADDRSTRLEN];
//创建epoll_event结构体 tep:epoll_ctl参数 ep[]: epoll_wait参数
struct epoll_event tep ,ep[OPEN_MAX];
//创建地址结构
struct sockaddr_in server_addr,client_addr;
socklen_t client_addr_len;
//创建套接字
lfd=Socket(AF_INET,SOCK_STREAM,0);
//设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//初始化
//memset(&server_addr,0,sizeof(server_addr));//将地址结构清零
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(SERVER_PORT);
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
//绑定地址结构
Bind(lfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
//设置监听
Listen(lfd,128);
//创建epoll模型 efd指向红黑树根节点
efd=epoll_create(OPEN_MAX);
//检查epoll_create函数返回值
if(efd==-1)
{
sys_err("epoll_create error");
}
//指定文件描述符的监听事件为“读”事件
tep.events=EPOLLIN;
tep.data.fd=lfd;
//将lfd对应的结构体添加到红黑树上
int ret;
ret=epoll_ctl(efd,EPOLL_CTL_ADD,lfd,&tep);
//判断返回值
if(ret==-1)
{
sys_err("epoll_ctl error");
}
//需要循环设置监听
for(;;)
{
//调用epoll_wait函数阻塞监听是否有客户端连接请求
nready=epoll_wait(efd,ep,OPEN_MAX,-1);
//检查是否成功返回
if(nready==-1)
{
sys_err("epoll_ctl error");
}
for(i=0;i<nready;i++)
{
if(!(ep[i].events & EPOLLIN))//如果被“读”事件,继续循环
{
continue;
}
if(ep[i].data.fd==lfd)//判断满足事件的fd是否为lfd(监听事件)
{
client_addr_len=sizeof(client_addr);
cfd=Accept(lfd,(struct sockaddr*)&client_addr,&client_addr_len);
cout<<"received from "<<inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str))
<<"at PORT"<<ntohs(client_addr.sin_port)<<endl;
tep.events=EPOLLIN;
tep.data.fd=cfd;
//将后续接收的请求返回的文件描述符(cfd)添加到红黑树中
ret=epoll_ctl(efd,EPOLL_CTL_ADD,cfd,&tep);
//判断返回值
if(ret==-1)
{
sys_err("epoll_ctl error");
}
}
else//判断不是监听事件,则为数据读写事件
{
sockfd=ep[i].data.fd;
n=read(sockfd,buf,sizeof(buf));
//判断read函数返回值
if(n==0)//对端关闭
{
//将文件描述符从红黑树中移除
ret=epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL);
if(ret==-1)
{
sys_err("epoll_ctl error");
}
Close(sockfd);
}
else if(n<0)//出错
{
ret=epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL);
Close(sockfd);
}
else{//实际读到的字节数
for(i=0;i<n;i++)
{
//大小写转换
buf[i]=toupper(buf[i]);
}
//写回buf
Write(sockfd,buf,n);
//写到屏幕输出
Write(STDOUT_FILENO, buf, n);
}
}
}
}
Close(lfd);
return 0;
}
客户端程序
运行结果
服务端:
客户端:
epoll事件模型
ET模式
Edge Triggered(边缘触发)工作模式
缓冲区剩余未读尽的数据不会导致epoll_wait返回,新的事件满足触发。
高速工作方式,只支持非阻塞no-block socket。
LT模式
Level Triggered(电平触发)工作模式
缓冲区剩余未读尽的数据会导致epoll_wait返回
缺省工作方式,同时支持block和no-block socket
epoll的ET非阻塞模式(忙轮询)
服务端程序
#include<iostream>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include "wrap.h"
using namespace std;
//定义服务端端口号
#define SERVER_PORT 9527
#define MAXLINE 10
#define OPEN_MAX 1024
int main (int argc ,char*argv[])
{
int i,n;
//int client[FD_SETSIZE];//自定义数组,大小为1024
int nready=0;//保存epoll_wait函数返回值,记录满足监听事件的fd个数
int lfd=0;//用于监听的套接字
int cfd=0;//用于通信的套接字
int sockfd=0;
int efd=0;//用于接收epoll_create函数返回值
int maxi;//用于检索客户端文件描述符的下标
char buf[BUFSIZ],str[INET_ADDRSTRLEN];
//创建epoll_event结构体 tep:epoll_ctl参数 ep[]: epoll_wait参数
struct epoll_event tep ,ep[OPEN_MAX];
//创建地址结构
struct sockaddr_in server_addr,client_addr;
socklen_t client_addr_len;
//创建套接字
lfd=Socket(AF_INET,SOCK_STREAM,0);
//设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//初始化
//memset(&server_addr,0,sizeof(server_addr));//将地址结构清零
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(SERVER_PORT);
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
//绑定地址结构
Bind(lfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
//设置监听
Listen(lfd,128);
struct epoll_event event;
struct epoll_event resevent[10];
int res, len;
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; /* ET 边沿触发 */
//event.events = EPOLLIN; /* 默认 LT 水平触发 */
cout<<"Accepting connections ..."<<endl;
client_addr_len = sizeof(client_addr);
cfd = accept(lfd, (struct sockaddr *)&client_addr, &client_addr_len);
cout<<"received from "<<inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str))
<<"at PORT"<<ntohs(client_addr.sin_port)<<endl;
//设置cfd为非阻塞
flag = fcntl(cfd,F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
event.data.fd = cfd;
epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &event);
while (1)
{
res = epoll_wait(efd, resevent, 10, -1);
cout<<"res:"<<res<<endl;
if (resevent[0].data.fd == cfd)
{
len = read(cfd, buf, MAXLINE/2); //readn(500)
write(STDOUT_FILENO, buf, len);
}
}
return 0;
}
客户端程序
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define MAXLINE 10
#define SERV_PORT 9527
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, i;
char ch = 'a';
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (1)
{
//aaaa\n
for (i = 0; i < MAXLINE/2; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//bbbb\n
for (; i < MAXLINE; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//aaaa\nbbbb\n
write(sockfd, buf, sizeof(buf));
sleep(5);
}
close(sockfd);
return 0;
}
运行结果
epoll优缺点
优点:
高效,突破文件描述符上限1024,事件分离。
缺点:
能跨平台,限于linux
epoll反应堆模型
step1:创建套接字,绑定地址信息,设置监听上限;
step2:创建监听红黑树(epoll_create),返回用作其他epoll系统调用的参数(文件描述符efd),用来指定访问的内核事件表;
step3:将文件描述符fd上的注册事件添加到(事件表)红黑树上(epoll_ctl);
step4:调用epoll_wait函数进行循环监听,监听到事件,将所有就绪事件从内核事件表(由efd指定)复制到传出数组;
step5:判断返回数组元素,若监听套接字lfd满足,则等待客户端连接(accept),若cfd满足,则调用read函数读取数据并完成大小写转换;
step6:将文件描述符cfd从监听红黑树上移除,将事件类型改为EPOLLOUT,调用epoll_ctl函数往(红黑树)事件表中添加事件并监听(写事件);
step7:等待epoll_wait函数成功返回(说明cfd可写),调用write函数写回;
step8:将文件描述符cfd从监听红黑树上移除,将事件类型改为EPOLLIN,调用epoll_ctl函数往(红黑树)事件表中添加事件并监听(读事件),调用epoll_wait函数进行监听,继续循环step4。
select、poll、epoll对比分析
系统调用 | select | poll | epoll |
---|---|---|---|
事件集合 | 用户通过三个参数分别传入感兴趣的可读、可写、异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件,使得每次调用select都需要重置三个参数 | 统一处理所有事件类型,用户只需要传通过pollfd.events传入感兴趣的事件,内核通过修改传入事件反馈其中的就绪事件 | 内核通过一个事件表直接管理用户感兴趣的所有事件,因此每次调用epoll_wait时,不需要反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件 |
应用程序索引就绪文件描述符的时间复杂度 | O(n) | O(n) | O(1) |
最大支持的文件描述符数 | 一般有最大限制值 | 65535 | 65535 |
工作模式 | LT | LT | 支持ET高效模式 |
内核实现和工作效率 | 采用轮询来检测就绪事件,算法时间复杂度为O(n) | 采用轮询来检测就绪事件,算法时间复杂度为O(n) | 采用回调函数来检测就绪事件,算法时间复杂度为O(1) |