多路转接(使用epoll实现)
文章目录
1.epoll的引入
epoll是poll的升级版本,它是在Linux2.5.44内核中被引进的。它几乎具备了之前所说的一切优点,被公认为是Linux2.6下性能最好的多路I/O就绪通知方法,即以后要使用多路转接,使用epoll时性价比最高的。
2.epoll相关的系统调用
不同于select和poll,epoll有三个系统调用来帮助完成等待的工作。
2.1epoll_create
int epoll_create(int size);
它的意思是创建一个epoll空间,epoll空间具体表示的是两段空间,一个策略:红黑树,就绪队列以及回调策略。这个在下面epoll原理中会进行详细的介绍。它的参数已经弃用了。
我们可以将该空间理解成一个文件,即返回值是一个文件描述符,用完该空间之后必须进行关闭。
2.2epoll_ctl
2.2.1参数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
它的作用是用户告诉内核,哪些事件需要进行关心。
第一个参数表示的就是epoll_create创建的epoll空间,第二个参数表示的是操作的选项(可以是删除,更改或者注册一个文件描述符),第三个参数表示要监听的fd,第四个参数告诉内核要监听的事件。
2.2.2选项
对于第二个参数的选项,一共有三种:
| 宏 | 作用 |
|---|---|
| EPOLL_CTL_ADD | 注册新的fd到epfd中 |
| EPOLL_CTL_MOD | 修改已经注册的fd的监听事件 |
| EPOLL_CTL | 从epfd中删除一个fd |
一旦设置好需要监听之后,OS就已经记住了,如果想删除或者修改,重新调用epoll_ctl即可。
2.2.3结构体
对于第三个参数,它的结构体结构如下:
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
event表示对于某个事件的关心,它的作用和poll中的events是一样的,它的取值如下:
| 宏 | 作用 |
|---|---|
| EPOLLIN | 表示对应的文件描述符可以读(包括堆对端SOCKET正常关闭) |
| EPOLLOUT | 表示对应的文件描述符可以写 |
| EPOLLPRI | 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据的到来) |
| EPOLLERR | 表示对应的文件描述符发生错误 |
| EPOLLHUP | 表示对应的文件描述符被挂断 |
| EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的 |
| EPOLLONESHOT | 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 |
| 第二个参数也是一个结构体,它的结构是这样的: |
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
我们需要关心的是fd字段。
2.3epoll_wait
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
它表示的是OS通知用户,哪些文件描述符已经就绪了。
参数events是分配好的epoll_event结构体数组(注意是数组)。
epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会帮助我们在用户态分配内存)
maxevents告知内核这个events由多大,timeout以毫秒为单位,-1表示永久阻塞。
如果函数调用成功,返回对应I/O上已经准备好的文件描述符数目,如果返回0表示已经超时,返回小于0表示函数失败。
值得注意的是,events一定是从下到大有连续继续数据的。
3.epoll工作原理
3.1epoll_create
当创建一个epoll空间的时候,会完成三部分内容:建立一个红黑树,建立一个就绪队列,明确使用回调策略。

3.2epoll_ctrl
当epoll_ctrl的作用是,向文件描述符中输入节点,每一个节点需要包括文件描述符和该文件描述符对应的events,当增加要监听的文件描述符时就插入节点,当删除文件描述符时,就删除节点。
3.3epoll_wait
当我们将timeout设置为-1的时候,没有数据就绪的话进程就会进入等待队列进行等待。一旦有数据就绪了,网卡驱动就会将数据传入到内核中,OS将使用回调策略生成一个就绪序列的节点,并将所有就绪的文件描述符形成的节点插入到就绪序列中。但是其实没有将fd一同放进该就绪序列的节点中,此时epoll_data发挥作用(因为它保存着文件描述符)。
就绪序列有数据了,进程被唤醒(从等待队列到运行队列),然后进程只需要与就绪队列打交道,以O(1)的时间复杂度检测是否有数据就绪(即检测就绪队列是否为空)

3.epoll实现多路转接
3.1实现思路
其实epoll和select以及poll的实现思路是基本相同的。
1.首先实现服务端的代码。
2.建立一个epoll模型。
3.使用epoll_ctl先将listen_sock描述符添加到epoll模型中。
4.调用循环,每一次进行epoll_wait,当有文件描述符就绪的时候,分为两种情况:
分别是当监听套接字就绪了以及正常的读取就绪了。
当监听套接字就绪的时候,除了需要使用accept来进行处理之外,还需要将accept返回的文件描述符添加到epoll的空间中。
当有正常数据就绪的时候分三种情况进行讨论即可。
3.2网络通信代码
#include <iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
using namespace std;
namespace ns_Sock
{
class Sock
{
private:
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "创建套接字失败" << endl;
exit(-2);
}
else
{
return sock;
}
}
static int Listen(int sock)
{
if(listen(sock,5)<0)
{
cerr<<"listen error"<<endl;
exit(-3);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int fd=accept(sock,(struct sockaddr*)&peer,&len);
if(fd>=0)
{
return fd;
}
else
{
exit(-5);
}
}
static void Bind(int sock,uint16_t port)
{
sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
cerr<<"bind error"<<endl;
exit(-4);
}
}
static void Connect(int sock,string ip,uint16_t port)
{
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(port);
server.sin_addr.s_addr=inet_addr(ip.c_str());
if(connect(sock,(struct sockaddr*)&server,sizeof(server))==0)
{
cout<<"connect success!"<<endl;
}
else
{
cout<<"connect fail"<<endl;
exit(-5);
}
}
};
}
3.3epoll实现网络通信
#include"Sock.hpp"
using namespace ns_Sock;
#define NUM 128
#include<sys/epoll.h>
void Usage(char* proc)
{
cout<<"Usage \n\t"<<proc<<" port"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(-1);
}
uint16_t port=(uint16_t)atoi(argv[1]);
int listen_sock=Sock::Socket();
Sock::Bind(listen_sock,port);
Sock::Listen(listen_sock);
//建立epoll模型,获得epfd
int epfd=epoll_create(128);
//先添加listen_sock和它所关心的事件到内核中
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=listen_sock;//虽然epoll_ctl有文件描述符,但是revs数组中的元素是epoll_event没有fd,因此需要将fd添加都epoll_event的data字段中
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
//事件循环
volatile bool quit=false;
struct epoll_event revs[NUM];//由于epoll_wait的数组是输出型参数,因此需要接收
while(!quit)
{
int timeout=1000;
int n=epoll_wait(epfd,revs,NUM,-1);//epoll_wait会将epfd中就绪事件的epoll_event结构体放在revs数组中,返回值表示数组大小
switch(n)
{
case 0:
cout<<"timeout....."<<endl;
break;
case -1:
cerr<<"epoll error"<<endl;
break;
default:
cout<<"有事件就绪了"<<endl;
//处理就绪事件
for(int i=0;i<n;i++)
{
int sock=revs[i].data.fd;//暂时方案
cout<<"文件描述符"<<sock<<"有数据就绪了"<<endl;
if(revs[i].events&EPOLLIN)//读事件就绪
{
cout<<"文件描述符"<<sock<<"读事件就绪了"<<endl;
if(sock==listen_sock)
{
int fd=Sock::Accept(listen_sock);
if(fd>=0)
{
cout<<"获取新链接成功了"<<endl;//此时还不能读需要添加到epfd的空间中
struct epoll_event _ev;
_ev.events=EPOLLIN;
_ev.data.fd=fd;
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&_ev);
cout<<"已经把"<<fd<<"添加到epfd空间中了"<<endl;
}
}
//正常的读处理
else
{
cout<<"文件描述符"<<sock<<"正常数据准备就绪"<<endl;
char buffer[1024];
ssize_t s=read(sock,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
cout<<"client["<<sock<<"]#"<<buffer<<endl;
}
else if(s==0)
{
cout<<"client quit "<<sock<<"将被关闭"<<endl;
close(sock);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);//将该套接字从epoll空间关注的位置删除
cout<<"Sock:"<<sock<<"delete from epoll success"<<endl;
}
else
{
cout<<"recv error"<<endl;
close(sock);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
cout<<"delete sock"<<sock<<endl;
}
}
}
}
}
}
}

浙公网安备 33010602011771号