基于epoll的简单高并发服务器程序
1.什么是epoll
epoll,select,poll都是基于Linux/Unix的io复用技术。所谓io复用简单来说就是让内核来告知我们哪些文件描述符读或写准备就绪。具体定义大家可以查阅各类书籍。
epoll相对于select和poll来说对内核资源的利用更高效,因为select和poll是需要将带有文件描述符的数据结构拷贝到内核,通知时再把就绪的文件描述符从内核区拷贝到用区。而epoll使用共享内存存储数据,与内核区共享文件描述符。
除此之外,epoll遍历文件描述符是以红黑树遍历,而select和poll是以线性表的形式遍历。
2.epoll的使用
/*
epoll使用步骤:
- 创建一个epoll模型 -> 一个函数
- 将需要检测的文件描述符添加到epoll模型中
- 开始使用epoll检测文件描述符的状态
- 检测到了状态变化
- 判断是不是监听的fd
- accept接受连接, 得到新的通信fd, 将其添加到epoll树上
- 负判断是不是通信的fd
- read
- 返回值为0 -> 对方关闭连接
- 将这个fd从树上删除
- write
*/
函数:
// 创建一个epoll模型
#include <sys/epoll.h>
int epoll_create(int size);
参数:
- size: 保证其大于0即可, 没有其他的实际意义
返回值:
是一个文件描述符, 理解为创建的epoll树的根节点
// 对epoll树操作函数, 添加节点/删除节点/修改节点属性
typedef union epoll_data {
void *ptr;
int fd; // 通常使用这个变量, 和epoll_ctl第三个参数值相同即可
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events事件的取值为三个宏:
- EPOLLIN: 读事件 -> 检测是fd的读缓冲区
- EPOLLOUT: 写事件 -> 检测是fd的写缓冲区
- EPOLLERR: 异常 -> 检测fd是否有异常
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
- epfd: epoll数的根节点, 通过 epoll_create(int size); 得到的, 返回值
- op: 对文件描述符的操作
EPOLL_CTL_ADD: 添加新节点
EPOLL_CTL_MOD: 修改节点是属性
EPOLL_CTL_DEL: 删除现有的节点
- fd: 要操作的文件描述符
- event: fd的属性设置
- 如果是删除操作, 这个参数指定为NULL即可
// 委托内核检测epoll树上的文件描述符状态
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数:
- epfd: epoll数的根节点, 通过 epoll_create(int size); 得到的, 返回值
- events: 传出参数, 指向一个数组的地址, 存储发送变化的文件描述符的信息
- maxevents: 参数events元素的个数
- timeout: 阻塞的时长, 单位是毫秒, ms
- 0: 直接返回
- -1: 一直阻塞, 直到检测的文件描述符有状态变化, 解除阻塞
- >0: 阻塞的毫秒数,
- 在阻塞期间如果检测的文件描述符有状态变化, 解除阻塞
- 没有变化, 时间到了解除阻塞
返回值:
-1: 失败
>0: 状态有变化的文件描述符的个数
代码:
#include <cstdio>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
//1.建立套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd==-1)
{
perror("socket");
exit(0);
}
//2.lfd绑定本地的IP和端口
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9999);//主机序转网络序
server_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*) &server_addr, sizeof(server_addr));
//注意第二参数要强制转换为(struct socket*)类型
//3,监听lfd
ret = listen(lfd, 128);
if (ret==-1)
{
perror("listen");
exit(0);
}
//4.创建epoll树
int epfd=epoll_create(0);
//5.将监听文件描述符添加到epoll树上
//一旦产生连接就会被内核检测到,我们后面做处理
struct epoll_event lev;
lev.data.fd = lfd;
lev.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd,&lev);
if (ret==-1)
{
perror("epoll_ctl");
exit(0);
}
//6.创建epoll_event 结构体数组
struct epoll_event epr[100];
int size = sizeof(epr) / sizeof(struct epoll_event);
//7.开始循环检测
while (true)
{
int count = epoll_wait(epfd, epr, size, -1);
//返回值为epoll树上文件读写缓冲区有变化的文件描述符个数
//将有变化的描述符放到epr数组里
for (int i = 0; i < count; i++)
{
if (lfd==epr[i].data.fd)
{ //如果是监听描述符则accept获得通信描述符
//并将通信描述符上epoll树
int cfd = accept(lfd, NULL, NULL);
lev.data.fd = cfd;
lev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &lev);
}
else
{ //如果不是监听的就是通信的
char buf[100] = { 0 };
int len = read(epr[i].data.fd, buf, sizeof(buf));
//recv(epr[i].data.fd, buf, sizeof(buf), 0);也行
if (len>0)
{
//通信 回复"收到数据"
write(epr[i].data.fd,"收到数据", len);
//send(epr[i].data.fd, buf, len, 0);也行
}
else
{
perror("read");
exit(0);
}
}
}
}
}