多路I/O复用之epoll
1、epoll简介
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
2、epoll 常用函数
epoll使用过程中,经常使用以下3个函数:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
(1) epoll_create 函数创建一个epoll句柄,参数size表明内核要监听的描述符数量。
调用成功时返回一个epoll句柄描述符,失败时返回-1。
- 核心数据结构是:1个红黑树和1个链表
(2) epoll_ctl 函数注册要监听的事件类型。
四个参数解释如下:
- epfd 表示epoll句柄
- op 表示fd操作类型,有如下3种
EPOLL_CTL_ADD 注册新的fd到epfd中
EPOLL_CTL_MOD 修改已注册的fd的监听事件
EPOLL_CTL_DEL 从epfd中删除一个fd - fd 是要监听的描述符
- event 表示要监听的事件
epoll_event 结构体定义如下:
struct epoll_event {
__uint32_t events; /* Epoll events */ EPOLLIN表示对应的文件描述符可以读
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr; int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
(3) epoll_wait 函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。
- epfd 是epoll句柄
- events 表示从内核得到的就绪事件集合
- maxevents 告诉内核events的大小
- timeout 表示等待的超时事件
3、示例程序,多客户端给服务器发送数据包,服务器收到包后,转发给每个客户端。
服务器代码:
#include<iostream>
#include<unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include<stdio.h>
#include<string.h>
#include<arpa/inet.h>
#include <sys/epoll.h>
#include<vector>
using namespace std;
#define SERVER_PORT 9527
vector<int > client_info; //定义一个容器来存放accept_fd
int main()
{
int w_size=0;
//socket
int server_fd;
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0)
{
perror("socket error");
return -1;
}
//bind
int ret = 0;
struct sockaddr_in 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);
ret = bind(server_fd, (const struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret < 0)
{
perror("bind error");
return -1;
}
//listen
ret = listen(server_fd, 10);
if (ret < 0)
{
perror("listen error");
return -1;
}
//epoll
int epoll_fd; //epoll 文件描述符
int epoll_wait_return;
int accept_fd;
int r_size;
char buffer[50] = { 0 };
struct epoll_event e_event;
struct epoll_event event_array[50] = { 0 };
epoll_fd = epoll_create(10); //创建epoll
if (epoll_fd < 0)
{
perror("epoll_create error");
return -1;
}
e_event.data.fd = server_fd;
e_event.events = EPOLLIN; //对事件的可读感兴趣
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &e_event);
while (1)
{
cout << "epoll wait......" << endl;
epoll_wait_return = epoll_wait(epoll_fd, event_array, 50, -1);//永久等待
for (int i = 0; i < epoll_wait_return; i++)
{
if (event_array[i].data.fd == server_fd) //有客户端连接过来
{
accept_fd = accept(server_fd, NULL, NULL);
cout << "client connect" << endl;
//把accept_fd 放入epoll 容器里面
e_event.data.fd = accept_fd;
e_event.events = EPOLLIN;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accept_fd, &e_event);
client_info.push_back(accept_fd); //把每一个客户端的accept添加到向量中
}
else if (event_array[i].events & EPOLLIN) //检查容器里的是否是读
{
r_size = read(event_array[i].data.fd, buffer, sizeof(buffer));
//判断 r_size >0 读到数据 ==0 有客户端退出 <0 失败
if (r_size > 0)
{
cout << "server recv: " << buffer << endl;
cout << "链接的 size=" << client_info.size() << endl;
for (int i = 0; i < client_info.size(); i++) //把收到的消息转发到每个客户端
{
w_size = write(client_info[i],buffer,sizeof(buffer));
}
}
else if (r_size == 0)
{
//把套接字从内核容器中移除
close(event_array[i].data.fd);
e_event.data.fd = accept_fd;
e_event.events = EPOLLIN;
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, event_array[i].data.fd, &e_event);
//删除向量中的accept_fd
vector<int >::iterator it;
for (it = client_info.begin(); it != client_info.end(); it++)
{
if (event_array[i].data.fd== accept_fd)
{
break;
}
}
cout << "client disconnect!" << endl;
}
}
}
}
return 0;
}
运行效果: 这里客户端使用Linux 自带命令来代替客户端。
命令为: nc IP 端口号