eventpoll实例
(结构体),通过epoll
提供的API操作该实例概览
#include
// 创建一个新的epoll实例
// 在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)
int epoll_create(int size);
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
// 检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_create(int size);
size
,目前没有意义了(之前底层实现是哈希表,现在是红黑树),随便写一个数,必须大于0epoll实例
的文件描述符int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd
:epoll实例对应的文件描述符op
:要进行什么操作
EPOLL_CTL_ADD
EPOLL_CTL_DEL
EPOLL_CTL_MOD
fd
:要检测的文件描述符event
:检测文件描述符什么事情,通过设置epoll_event.events
,常见操作
EPOLLIN
EPOLLOUT
EPOLLERR
EPOLLET
(默认水平触发)int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd
:epoll实例对应的文件描述符events
:传出参数,保存了发生了变化的文件描述符的信息maxevents
:第二个参数结构体数组的大小timeout
:阻塞时长
服务端
#include
#include
#include
#include
#include
#include
#define SERVERIP "127.0.0.1"
#define PORT 6789
int main()
{
// 1. 创建socket(用于监听的套接字)
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
perror("socket");
exit(-1);
}
// 2. 绑定
struct sockaddr_in server_addr;
server_addr.sin_family = PF_INET;
// 点分十进制转换为网络字节序
inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr.s_addr);
// 服务端也可以绑定0.0.0.0即任意地址
// server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
int ret = bind(listenfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
perror("bind");
exit(-1);
}
// 3. 监听
ret = listen(listenfd, 8);
if (ret == -1) {
perror("listen");
exit(-1);
}
// 创建epoll实例
int epfd = epoll_create(100);
// 将监听文件描述符加入实例
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = listenfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event);
if (ret == -1) {
perror("epoll_ctl");
exit(-1);
}
// 此结构体用来保存内核态返回给用户态发生改变的文件描述符信息
struct epoll_event events[1024];
// 不断循环等待客户端连接
while (1) {
// 使用epoll,设置为永久阻塞,有文件描述符变化才返回
int num = epoll_wait(epfd, events, 1024, -1);
if (num == -1) {
perror("poll");
exit(-1);
} else if (num == 0) {
// 当前无文件描述符有变化,执行下一次遍历
// 在本次设置中无效(因为select被设置为永久阻塞)
continue;
} else {
// 遍历发生改变的文件描述符集合
for (int i = 0; i < num; i++) {
// 判断监听文件描述符是否发生改变(即是否有客户端连接)
int curfd = events[i].data.fd;
if (curfd == listenfd) {
// 4. 接收客户端连接
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &client_addr_len);
if (connfd == -1) {
perror("accept");
exit(-1);
}
// 输出客户端信息,IP组成至少16个字符(包含结束符)
char client_ip[16] = {0};
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip));
unsigned short client_port = ntohs(client_addr.sin_port);
printf("ip:%s, port:%d\n", client_ip, client_port);
// 将信息加入监听集合
event.events = EPOLLIN;
event.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
} else {
// 只检测读事件
if (events[i].events & EPOLLOUT) {
continue;
}
// 接收消息
char recv_buf[1024] = {0};
ret = read(curfd, recv_buf, sizeof(recv_buf));
if (ret == -1) {
perror("read");
exit(-1);
} else if (ret > 0) {
printf("recv server data : %s\n", recv_buf);
write(curfd, recv_buf, strlen(recv_buf));
} else {
// 表示客户端断开连接
printf("client closed...\n");
close(curfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
break;
}
}
}
}
}
close(listenfd);
close(epfd);
return 0;
}
客户端:
#include
#include
#include
#include
#include
#define SERVERIP "127.0.0.1"
#define PORT 6789
int main()
{
// 1. 创建socket(用于通信的套接字)
int connfd = socket(AF_INET, SOCK_STREAM, 0);
if (connfd == -1) {
perror("socket");
exit(-1);
}
// 2. 连接服务器端
struct sockaddr_in server_addr;
server_addr.sin_family = PF_INET;
inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr.s_addr);
server_addr.sin_port = htons(PORT);
int ret = connect(connfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
perror("connect");
exit(-1);
}
// 3. 通信
char recv_buf[1024] = {0};
while (1) {
// 发送数据
char *send_buf = "client message";
write(connfd, send_buf, strlen(send_buf));
// 接收数据
ret = read(connfd, recv_buf, sizeof(recv_buf));
if (ret == -1) {
perror("read");
exit(-1);
} else if (ret > 0) {
printf("recv server data : %s\n", recv_buf);
} else {
// 表示客户端断开连接
printf("client closed...\n");
}
// 休眠的目的是为了更好的观察,放在此处可以解决read: Connection reset by peer问题
sleep(1);
}
// 关闭连接
close(connfd);
return 0;
}
参考:动画讲解:epoll io多路复用的工作原理,并解答4个常见面试题_哔哩哔哩_bilibili
如上图所示,调⽤epoll_ctl() 函数,把需要监控的 socket 加⼊内核中的红黑树⾥:(红⿊树的增删查时间复杂度是O(logn),不需要每次操作都传⼊整个集合,只需要传⼊⼀个待检测的socket,减少了内核和⽤户空间的⼤量数据拷贝和内存分配)
假设有三个客户端(C5、C6、C7)向服务器的网卡上发送了数据,这时候网卡就会利用DMA拷贝技术把数据拷贝到内核环形缓冲区。epoll在调用epoll_ctl()函数时,向内核注册fd和相关事件的同时,注册了一个回调函数,当操作系统把数据拷贝到缓冲区后就会执行回调函数,将缓冲区的内容和对应的文件描述符添加到就绪队列中。
其实红黑树和就绪队列存储在同一个结构体中(epitem),当epitem中对应的文件描述符上的事件(event)已经就绪的时候,回调函数就会将该节点连接到已就绪的双端就绪队列中,其实只是在红黑树中做了指针的链接,而我们为了描述更加清晰,可以直接说将红黑树上相应的节点拷贝到就绪列表中。
最后调用epoll_wait()方法 来判断哪些文件描述符上的哪些事件已经就绪了,它会把这些已就绪的文件描述符拷贝到epoll_wait()的第二个形参events数组中,然后再拷贝到用户态
1、epoll只是在调用epoll_ctl方法的时候,在监听事件的时候才会把数据从用户态拷贝到内核态,而select/poll每次执行的时候都要重新将监听的所有事件从用户态拷贝到内核态
2、epoll调用epoll_wait()方法读取事件的时候,它只会拷贝已就绪的这些事件,不会拷贝没有就绪的事件。
3、epoll采用回调机制 它会将已就绪的文件描述符加入到就绪队列种,而不是像select和poll那样去主动用轮循的方式去轮循哪个文件描述符上的事件已就绪
若使用哈希表存储,其优点为查询速度快(O(1)),但是当调用epoll_create的时候,哈希底层的数组究竟创建多大比较合适呢?若我们有几百万的客户连接,这个数组肯定是越大越好,若我们只有十几个的客户连接,若将数组创建的很大,就会造成空间的浪费,而我们也无法预知一共有多少个连接,因此哈希表就不太适合。
B+树实际上是一颗多叉树,一个节点可以存储多个key,主要的目的是为了降低树的高度,其适用场景是做磁盘索引,他在内存场景是不太适用的。
其实epoll未必会比poll快,如果并发量只有几个或者十几个,那么遍历一个结构体数组也未必慢。只有在高并发的网络编程场景种,epoll才能获取全胜。
1. select 和 poll 采⽤轮询的⽅式检查就绪事件,每次都要扫描整个⽂件描述符,复杂度O(N);
2. epoll 采⽤回调⽅式检查就绪事件,只会返回有事件发⽣的⽂件描述符的个数,复杂度O(1);
3. select 只⼯作在低效的LT模式,epoll 可以在 ET ⾼效模式⼯作;
4. epoll 是 Linux 所特有,⽽ select 则应该是 POSIX 所规定,⼀般操作系统均有实现;
5. select 单个进程可监视的fd数量有限,即能监听端⼜的⼤⼩有限,64位是2048;epoll 没有最⼤并发连接的限制,能打开的 fd 的上限远⼤于2048(1G的内存上能监听约10万个端⼜);
6. select 内核需要将消息传递到⽤户空间,都需要内核拷贝动作;epoll通过内核和⽤户空间共享⼀块内存来实现的