利用epoll模型实现多个客户端和一个服务端的CS模型——多路IO复用技术
epoll优点
- select和poll是线性处理,而epoll是红黑树处理
- select和poll频繁的在内核和用户区进行拷贝,而epoll使用的是共享内存
- 程序需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,但epoll可以直接得到已就绪的文件描述符集合,无需再次检测
- epoll
将检测文件描述符的变化委托给内核去处理,然后内核将发生变化的描述符对应的事件返回给程序。不仅告诉程序有几个发生变化,而且精准的告诉程序哪几个发生变化
创建一棵epoll树
int epoll_create(int size);
param:
size: 最大节点数,需要传递大于0的数,linux上该参数被忽略
return:
成功:返回大于0的文件描述符,代表整个树的树根
失败:返回-1
将要监听的节点在epoll树上添加、删除和修改
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
param:
epfd:epoll_create函数的返回值
op: EPOLL_CTL_ADD: 往 epoll 模型中添加新的节点
EPOLL_CTL_MOD: 修改 epoll 模型中已经存在的节点
EPOLL_CTL_DEL: 删除 epoll 模型中的指定的节点
fd:要操作的文件描述符
event:检测这个文件描述符的什么事件
event.events:委托 epoll 检测的事件
EPOLLIN:读事件,接收数据,检测读缓冲区
EPOLLOUT:写事件,发送数据,检测写缓冲区
EPOLLERR:异常事件
event.data:通常情况下使用里边的fd成员,委托内核监控的文件描述符,在调用 epoll_wait函数的时候这个值会被传出
return:
成功:返回 0
失败:返回 - 1
委托内核监控epoll实例中有没有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
param:
epfd: epoll_create函数的返回值,通过这个参数找到 epoll 实例
events: 传出参数,里边存储了已就绪的文件描述符的信息
maxevents: 修饰第二个参数,结构体数组的容量(元素个数)
timeout: 0:函数不阻塞
>0:函数阻塞对应的毫秒数再返回
-1:函数一直阻塞,直到 epoll 实例中有已就绪的文件描述符之后才解除阻塞
服务端代码如下,客户端代码不变
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<ctype.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<netinet/in.h>
#include<errno.h>
#include<sys/epoll.h>
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd < 0)
{
perror("socket error");
return -1;
}
struct sockaddr_in serv;
bzero(&serv, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd, (struct sockaddr*)&serv, sizeof(serv));
if(ret < 0)
{
perror("bind error");
return -1;
}
listen(lfd, 128);
//创建一棵epoll树
struct epoll_event ev;
struct epoll_event events[1024];
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);
int nready;
int max=0;//表示内核监控的范围,一开始有1个
int cfd;
int sockfd;
int i;
int n;
char buf[1024];
while(1)
{
nready = epoll_wait(epfd, events, 1024, -1);
if(nready < 0)
{
if(errno == EINTR)
continue;
perror("epoll wait error\n");
break;
}
for(i=0; i<nready; i++)
{
//有客户端连接请求到来
sockfd = events[i].data.fd;
if(sockfd == lfd)
{
cfd = accept(lfd, NULL, NULL);
//将新的cfd上树
ev.data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
continue;
}
//有数据发来
memset(buf, 0, sizeof(buf));
//n = read(sockfd, buf, sizeof(buf));
n = recv(sockfd, buf, sizeof(buf), 0);
if(n <= 0)
{
close(sockfd);
//将sockfd对应的事件的节点从epoll树上删除
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
printf("read error or client close\n");
continue;
}
printf("n == [%d], buf == [%s]\n", n, buf);
for(int j=0; j<n; j++)
{
buf[j] = toupper(buf[j]);
}
//write(sockfd, buf, n);
send(sockfd, buf, n, 0); }
}
close(epfd);
close(lfd);
return 0;
}
扩展:
epoll的ET(边沿触发) 和 LT(水平触发) 模式
- epoll默认情况下是LT模式,在这种模式下,若读数据一次性没有读完,缓冲区还有可读数据,则epoll_wait还会通知(比如发10个数据,一次只读两次,那么会循环读取5次)
- 若将epoll设置为ET模式(ev.events = EPOLLIN | EPOLLET),若读数据的时候一次性没有读完,则epoll_wait不再通知,直到下次有新的数据发来(还会读取上一次缓冲区已收到但还未读走的数据)