【网络编程】epoll

主旨思想

  • 直接在内核态创建 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);

    • 功能:创建一个新的epoll实例
    • 参数:size,目前没有意义了(之前底层实现是哈希表,现在是红黑树),随便写一个数,必须大于0
    • 返回值
      • -1:失败
      • >0:操作epoll实例的文件描述符
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    • 功能:对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
    • 参数:
      • epfd:epoll实例对应的文件描述符
      • op:要进行什么操作
        • 添加:EPOLL_CTL_ADD
        • 删除:EPOLL_CTL_DEL
        • 修改:EPOLL_CTL_MOD
      • fd:要检测的文件描述符
      • event:检测文件描述符什么事情,通过设置epoll_event.events,常见操作
        • 读事件:EPOLLIN
        • 写事件:EPOLLOUT
        • 错误事件:EPOLLERR
        • 设置边沿触发:EPOLLET(默认水平触发)
    • 返回值:成功0,失败-1
  • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

    • 功能:检测哪些文件描述符发生了改变
    • 参数:
      • epfd:epoll实例对应的文件描述符
      • events:传出参数,保存了发生了变化的文件描述符的信息
      • maxevents:第二个参数结构体数组的大小
      • timeout:阻塞时长
        • 0:不阻塞
        • -1:阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
        • >0:具体的阻塞时长(ms)
    • 返回值:
      • > 0:成功,返回发送变化的文件描述符的个数
      • -1:失败

代码实现

服务端

#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 模型的执行原理

参考:动画讲解: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数组中,然后再拷贝到用户态

epoll为什么会比select/poll快?快在哪些地方?

1、epoll只是在调用epoll_ctl方法的时候,在监听事件的时候才会把数据从用户态拷贝到内核态,而select/poll每次执行的时候都要重新将监听的所有事件从用户态拷贝到内核态

2、epoll调用epoll_wait()方法读取事件的时候,它只会拷贝已就绪的这些事件,不会拷贝没有就绪的事件。

3、epoll采用回调机制 它会将已就绪的文件描述符加入到就绪队列种,而不是像select和poll那样去主动用轮循的方式去轮循哪个文件描述符上的事件已就绪

为什么底层采用红黑树,而不用哈希表或者B+树?

若使用哈希表存储,其优点为查询速度快(O(1)),但是当调用epoll_create的时候,哈希底层的数组究竟创建多大比较合适呢?若我们有几百万的客户连接,这个数组肯定是越大越好,若我们只有十几个的客户连接,若将数组创建的很大,就会造成空间的浪费,而我们也无法预知一共有多少个连接,因此哈希表就不太适合。

B+树实际上是一颗多叉树,一个节点可以存储多个key,主要的目的是为了降低树的高度,其适用场景是做磁盘索引,他在内存场景是不太适用的。

epoll一定比poll快吗?为什么还有人使用poll模型?

其实epoll未必会比poll快,如果并发量只有几个或者十几个,那么遍历一个结构体数组也未必慢。只有在高并发的网络编程场景种,epoll才能获取全胜。

select和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通过内核和⽤户空间共享⼀块内存来实现的

你可能感兴趣的:(网络编程,网络)