UNP学习_I/0复用之epoll函数实现回射服务器

UNP学习_I/0复用之epoll函数实现回射服务器

一、函数原型

#include

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);

struct epoll_event{
	uint32_t events;
	epoll_data_t data;
}
typedef union epoll_data{
	void* ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_date_t;

(1)epoll_create函数生成一个epoll专用的文件描述符,size参数表示通知内核epoll监听数目的大致数量,可拓展。

(2)epoll_ctl是epoll的注册函数,用于控制某个epoll文件描述符下监听的事件,可以注册、修改、删。返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。
epfd:epoll_create生产的epoll文件描述符
op:
EPOLL_CTL_ADD --注册
EPOLL_CTL_MOD --修改
EPOLL_CTL_DEL --删除
fd:关联的文件描述符
event:告诉内核要监听的事件
EPOLLIN(读)、EPOLLOUT(写)、EPOLLERR(异常)

(3)epoll_wait函数用来等待IO事件发生,可以设置阻塞。
epfd:要检测的epoll文件描述符
events:回传待处理事件的数组
maxevents:告诉内核这个events的大小
timeout:-1(永久阻塞)、0(立即返回)、>0(没有检测到事件发生时最多等待的时间(单位为毫秒))

二、epoll工作模式

1、水平触发 LT
只要fd对应的缓冲区有数据epoll_wait就会返回,返回的次数和发送数据的次数没有关系,这是epoll默认的工作模式。
2、阻塞边沿触发 block-ET
fd文件属性默认为阻塞,client给server发一次数据,server的epoll_wait就返回一次,无论缓冲区是否还有数据,都只返回一次,此时如果使用while(recv())可以读完数据,但是读到最后一次recv()会阻塞。
3、非阻塞边沿触发nonblock-ET
这个就是在阻塞的基础上更改fd的属性,可以利用open函数或者fcntl函数更改

int flag = fcntl(fd,F_GETFL);
flag |=O_NONBLOCK;
fcntl(fd,F_SETFL,flag)

三、非阻塞边沿触发的回射服务器代码实现

// File Name: epoll_server.c
// Author: AlexanderGan
// Created Time: Wed 31 Jul 2020 04:20:11 PM CST

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

int main(int argc, char* argv[]){
     if(argc < 2)
    {
        printf("eg: ./a.out IP port\n");
    }
    int port = atoi(argv[1]);

    struct sockaddr_in serv_addr, client_addr;
    socklen_t serv_len = sizeof(serv_addr);
    socklen_t cli_len = sizeof(client_addr);

    //创建套接字
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    printf("lfd = %d\n",lfd);
    //初始化服务器
    memset(&serv_addr,0,serv_len);
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(port);
    //绑定ip和端口
    bind(lfd,(struct sockaddr*)&serv_addr,serv_len);
    //设置同时监听的最大个数
    listen(lfd,36);
    printf("Start accept !\n");

    
    //epoll根结点创建
    int epfd = epoll_create(2000);
    //初始化epoll树
    struct epoll_event ev; 
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);

    struct epoll_event all[2000];

    while(1)
    {
        //使用epoll通知内核进行检测
        int ret = epoll_wait(epfd,all,sizeof(struct epoll_event),-1);
        printf("============= epoll_wait! ============\n");
        
        //遍历数组
        for(int i = 0; i < ret; i++){

            int fd = all[i].data.fd;
            //判断是否有新连接
            if(fd == lfd){
                int cfd = accept(lfd,(struct sockaddr*)&client_addr,&cli_len);
                //接受连接请求
                if(cfd == -1){
                    perror("accept error!\n");
                    exit(1);
                }
                //设置文件为非阻塞模式
                int flag = fcntl(cfd,F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd,F_SETFL,flag);
                //将新的fd挂到树上
                struct epoll_event temp;
                //设置边沿触发
                temp.events = EPOLLIN | EPOLLET;
                temp.data.fd = cfd;

                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&temp);
                //打印客户端信息
                char ip[64];
                printf("New Client IP: %s,Port:%d\n",
                        inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,ip,sizeof(ip)),
                        ntohs(client_addr.sin_port));

            }
            else{
                //处理已经连接的客户端发过来的消息

                if(!all[i].events & EPOLLIN) continue;//如果没有EPOLLIN事件则跳一次循环
                //循环读数据
                char buf[5] = {0};
                int len;
                while((len = recv(fd,buf,sizeof(buf),0)) > 0) 
                {
                    //打印到标准输出
                    write(STDOUT_FILENO,buf,len);
                    //发送给客户端
                    send(fd,buf,len,0);
                }
                if(len == 0)
                {
                    printf("客户端已主动断开连接。\n");
                    //从树上删除节点
                    int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
                    if(ret == -1){
                        perror("epoll_ctl error!\n");
                        exit(1);
                    }
                    close(fd);
                }
                else if(len == -1){
                    if(errno == EAGAIN){
                        printf("缓冲区数据已经读取完。\n");
                    }
                    else{
                        printf("recv error!\n");
                        exit(1);
                    }
                }

            } 

        }
    }
    close(lfd);
  return 0 ;
}

4、epoll的优缺点

epoll建立了一个红黑树用于存放socket,另外维护了一个链表用来存放准备就绪的事件。执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,向红黑树添加节点,然后向内核注册回调函数。当监控事件发生时向就绪链表中插入数据,执行epoll_wait时立刻返回准备就绪链表里的数据。

优点:
(1)epoll只返回触发事件的描述符链表,不需要再遍历大量的描述符来寻找触发事件的那个描述符。

(2)epoll将事件的注册和监控分离,可以在任何时候添加套接字或从移除,即使另一个线程在epoll_wait函数中。还可以修改描述符事件,一切都会正常工作,而且这种行为是被支持和记录的。

(3)使用epoll_wait()可以让多个线程在同一个epoll队列中等待,这在select/poll中是做不到的。事实上,这不仅在epoll中可以实现,而且是边沿触发模式下的推荐方法。

缺点:
(1)改变事件标志(即从read到write)需要epoll_ctl的syscall,而使用poll时,这是一个完全在用户空间完成的简单的位掩码操作。用epoll将5000个套接字从读切换到写,需要5000次syscall,从而进行上下文切换(截至2014年,对epoll_ctl的调用仍然不能批量化,每个描述符必须单独更改),而在poll中,则需要在pollfd结构上进行一次循环。
(2)每个accept()套接字都需要添加到集合中,和上面一样,用epoll必须通过调用epoll_ctl来完成–这意味着每个新的连接套接字需要两个系统调用,而不是poll的一个。如果你的服务器有很多发送或接收流量很少的短连接,epoll可能会比poll花费更多的时间来服务它们。
(3)跨平台支持不好,epoll是Linux的专有,虽然其他平台也有类似的机制,但它们并不完全相同–例如,边缘触发是非常独特的(虽然FreeBSD的kqueue也支持)。
(4)高性能的处理逻辑比较复杂,因此调试起来比较困难,尤其是边缘触发,如果错过了额外的读/写,很容易出现死锁。

你可能感兴趣的:(Linux网络编程,epoll,linux,socket)