8. C++通过epoll的方式实现高性能网络服务器

就好比接收快递一样,select就是快递员上门挨家挨户遍历去问有没有快递,而epoll不同,epoll就好比在楼下放一个箱子,然后谁有快递就将快递放在楼下箱子里面,也就是那个事件触发,就在箱子里面遍历就行。
在这里插入图片描述

使用epoll的好处

  • 没有文件描述符的限制
    这个可以达到几万的大小,远远超过了select所能承载的数量
  • 工作效率不会随着文件描述符的增加而下降
    对于select而言,文件描述符越多,在应用层会遍历所有的文件描述符,那么工作效率就会随着文件描述符的增加而下降,而epoll不会
  • Epoll经过系统优化更高效

epoll事件的触发模式

Level Trigger没有处理反复发送
第一种是水平触发,socket接收缓冲区不为空,有数据可读,读事件一直触发,socket发送缓冲区不满 可以继续写入数据 写事件一直触发。

Edge Trigger只发送一次(边缘出发)
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件,socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件,边沿触发仅触发一次,水平触发会一直触发。

epoll 常用API

epoll_create 函数

其实就是上面说的创建的一个箱子。这个其实他就是一个链表,只要size>0就行

#include <sys/epoll.h>
int epoll_create(int size);

创建一个指示 epoll 内核事件表的文件描述符,该描述符将用作其他 epoll 系统调用的第一个参数,此处的 size 参数不起作用。

epoll_ctl 函数

这个就是操作那栋楼的住户,可以增加、删除住户。一开始就是要把服务器socket加入进去。

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

该函数用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除:
epfd:为 epoll_create 的句柄;
op:表示动作,用 3 个宏来表示:

  • EPOLL_CTL_ADD:注册新的 fd 到 epfd;
  • EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
  • EPOLL_CTL_DEL:从 epfd 删除一个 fd;

event:告诉内核需要监听的事件。
其中,event 是 epoll_event 结构体指针类型,表示内核监听的事件,具体定义如下:

struct epoll_event {
    __uint32_t events;
    epoll_data_t data;
};

events 描述事件类型,其中 epoll 事件类型有以下几种:

  • EPOLLIN:表示对应的文件描述符可读(包括对端SOCKET正常关闭)
  • EPOLLOUT:表示对应的文件描述符可写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLLET:将 EPOLL 设置为边缘触发(ET)模式;
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列中。

epoll_wait 函数

就是检查那个箱子里面发生事件的socket。然后循环也是循环epoll_wait返回的socket.
在这里插入图片描述

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

该函数用于等待所监控的文件描述符上有事件的产生,返回就绪的文件描述符的个数。
events:用来存储内核得到的事件的集合;
maxevents:告知内核这个 events 有多大,这个值不能大于创建 epoll_create() 时的大小;
timeout:超时时间:

  • -1:阻塞;
  • 0:立即返回,非阻塞;
  • >0:指定毫秒数;

返回值:成功返回有多少文件描述符就绪,时间到时返回 0,出错时返回-1。

epoll的事件

  • EPOLLET
    epoll默认是水平触发,让水平触发变成边缘触发,用这个事件
  • EPOLLIN和EPOLLOUT
    这个两个是有数据来和往外写
  • EPOLLPRI
    出现中断出现问题的时候
  • EPOLLERR
    EPOLL读写的时候出现问题了
  • EPOLLHUP
    挂起的时候会出现

epoll重要的结构体

typedef union epoll_data {//联合体,只能用一个
	void *ptr;
	int fd;
	uint32_tu32;
	uint64_t u64;
} epoll_data_t;
struct epoll_event {
	uint32_t events;  /* Epoll events,就是epollin,epollout,epollerror等*/
	epoll_data_t data;   /* User data variable ,这里也就是socket_fd*/
};

主要代码

对于select和epoll我们首先都是要先将监听的那个socket加入进去,也就是这栋楼第一个住户。首先要创建一个epoll,然后把一开始创造的那个socket加入到epoll中去。

//创建epoll
epoll_fd = epoll_create(256);
//先将侦听的socket_fd添加进去,然后再将与数据通讯的客户端的socket_fd添加进去
ev.events=EPOLLIN;//对于侦听的这个事件来说就是输入,就是in,这个一般不变成边缘触发,为了保证所有来的都能连上
ev.data.fd=socket_fd;//这个就是文件描述符socket
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&ev);

然后利用epoll_wait判断发生事件的个数,也就是箱子里面发生事件的socket。

event_number = epoll_wait(epoll_fd,events,MAX_EVENTS,TIMEOUT);//发生事件的个数

然后判断是那个events[i]发生的事件
如果是那个侦听的那个一开始的socket发生了事件,那说明是来了新的socket需要接收,然后需要将这个新来的accept_fd加入到epoll中,一开始就一个监听的socket,点击连接之后新来的住户。调用的是epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev);//将accept_fd添加到epoll中去注意这里加入的是一个结构体struct epoll_event ev
这个结构体ev有两个数据,一个是ev.events就是事件,比如说EPOLLIN,EPOLLOUT等。ev.data.fd就是socket_fd。
在这里插入图片描述

if(events[i].data.fd==socket_fd){//如果这个是侦听的socket发生事件了,那么说明是来了新的连接
    socklen_t addr_len=sizeof(struct sockaddr);
    accept_fd = accept(socket_fd,
                       (struct sockaddr *)&remoteaddr,
                       &addr_len);
    //设置成非阻塞
    //创建了socket之后我们要设置成异步的
    flags = fcntl(accept_fd,F_GETFL,0);
    //然后设置成非阻塞
    fcntl(accept_fd,F_SETFL,flags | O_NONBLOCK);

    ev.events=EPOLLIN | EPOLLET;//|上边缘触发
    ev.data.fd=accept_fd;
    epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev);//将accept_fd添加到epoll中去

}

但是如果是已经连接的events[i]发生了事件,那说明是收发消息

else if(events[i].events&EPOLLIN){//这里只介绍读的
        do{
            memset(in_buff, 0, sizeof(in_buff));
            //接收消息
            ret = recv(events[i].data.fd,(void *)in_buff,MESSAGE_LEN,0);
            if(ret==0){
                close(events[i].data.fd);
            }
            if(ret==MESSAGE_LEN){//缓冲区满了
                std::cout<<"maybe have data..."<<std::endl;
            }
        }while(ret<-1&&errno==EINTR);

        if(ret<0){
            switch(errno){
                case EAGAIN:
                    break;
                dafault:
                    break;
            }
        }
        if(ret>0){//打印信息
            std::cout<<"receive messaage:"<<in_buff<<std::endl;
            //返回消息
            send(events[i].data.fd,(void*)in_buff,MESSAGE_LEN,0);
        }
    }
}

总的代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <iostream>
#include <fcntl.h>
#include <sys/types.h>
//端口
#define PORT 8888
#define MESSAGE_LEN 1024
#define MAX_EVENTS 20
#define TIMEOUT 500

int main(int argc,char* argv[]){

    int ret=-1;
    int on=1;
    int backlog=10;//缓冲区大小

    int socket_fd,accept_fd;

    struct sockaddr_in localaddr,remoteaddr;

    char in_buff[MESSAGE_LEN]={0,};

    int epoll_fd;

    struct epoll_event ev,events[MAX_EVENTS];//epoll中event的结构体

    int event_number;

    int flags;

    socket_fd=socket(AF_INET,SOCK_STREAM,0);
    if(socket_fd==-1){
        std::cout<<"Failed to create socket!"<<std::endl;
        exit(-1);
    }

    //创建了socket之后我们要设置成异步的
    flags = fcntl(socket_fd,F_GETFL,0);
    //然后设置成非阻塞
    fcntl(socket_fd,F_SETFL,flags | O_NONBLOCK);

    ret=setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
    if(ret==-1){
        std::cout<<"Failed to set socket options!"<<std::endl;
    }

    localaddr.sin_family=AF_INET;//地址族
    localaddr.sin_port=htons(PORT);//端口号
    localaddr.sin_addr.s_addr=INADDR_ANY;//这个就是0
    bzero(&(localaddr.sin_zero), 8);

    ret= bind(socket_fd,(struct sockaddr *)&localaddr,sizeof(struct sockaddr));//绑定
    if(ret==-1){//绑定失败
        std::cout<<"Failed to bind addr!"<<std::endl;
        exit(-1);
    }

    ret = listen(socket_fd,backlog);//第二个是缓冲区大小,因为同一时间只能处理一个,其他都放在缓冲区
    if(ret==-1){
        std::cout<<"failed to listen socket!"<<std::endl;
        exit(-1);
    }

    //创建epoll
    epoll_fd = epoll_create(256);
    //先将侦听的socket_fd添加进去,然后再将与数据通讯的客户端的socket_fd添加进去
    ev.events=EPOLLIN;//对于侦听的这个事件来说就是输入,就是in,这个一般不变成边缘触发,为了保证所有来的都能连上
    ev.data.fd=socket_fd;//这个就是文件描述符socket
    epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&ev);

    while(1){//等待连接
        event_number = epoll_wait(epoll_fd,events,MAX_EVENTS,TIMEOUT);//发生事件的个数
        for(int i=0;i<event_number;i++){//有多少个文件描述符发生事件了
            if(events[i].data.fd==socket_fd){//如果这个是侦听的socket发生事件了,那么说明是来了新的连接
                socklen_t addr_len=sizeof(struct sockaddr);
                accept_fd = accept(socket_fd,
                                   (struct sockaddr *)&remoteaddr,
                                   &addr_len);
                //设置成非阻塞
                //创建了socket之后我们要设置成异步的
                flags = fcntl(accept_fd,F_GETFL,0);
                //然后设置成非阻塞
                fcntl(accept_fd,F_SETFL,flags | O_NONBLOCK);

                ev.events=EPOLLIN | EPOLLET;//|上边缘触发
                ev.data.fd=accept_fd;
                epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_fd,&ev);//将accept_fd添加到epoll中去

            }else if(events[i].events&EPOLLIN){//这里只介绍读的
                do{
                    memset(in_buff, 0, sizeof(in_buff));
                    //接收消息
                    ret = recv(events[i].data.fd,(void *)in_buff,MESSAGE_LEN,0);
                    if(ret==0){
                        close(events[i].data.fd);
                    }
                    if(ret==MESSAGE_LEN){//缓冲区满了
                        std::cout<<"maybe have data..."<<std::endl;
                    }
                }while(ret<-1&&errno==EINTR);

                if(ret<0){
                    switch(errno){
                        case EAGAIN:
                            break;
                        dafault:
                            break;
                    }
                }
                if(ret>0){//打印信息
                    std::cout<<"receive messaage:"<<in_buff<<std::endl;
                    //返回消息
                    send(events[i].data.fd,(void*)in_buff,MESSAGE_LEN,0);
                }
            }
        }
    }
    std::cout<<"quit servet...\n"<<std::endl;
    close(socket_fd);

    return 0;
}

epoll可以放在多线程里面,这个是要加锁的,但是不推荐,加锁是对性能的一个消耗。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值