【C/C++】从 epoll 到 reactor 网络框架

【C/C++】从 epoll 到 reactor 网络框架


本文描述了 从 epoll 到实现 reactor 百万 io 并发的编程模式

什么是 epoll?

epoll 是 Linux 的一个处理 io 并发的模式,它包含三个函数

  1. epoll_create:返回一个引用新 epoll 实例的文件描述符,epoll 实例是一个检测集合
  2. epoll_ctl:操作 epoll 实例,利用第二个参数的值来改变 fd 与 epoll 实例之间的关系,一共有三种关系,第四个参数 epoll_event 用于控制检测集合的观测方式
    1. EPOLL_CTL_ADD:将 fd 注册到 epoll 检测集合中
    2. EPOLL_CTL_DEL:将 fd 从 epoll 检测集合中移除
    3. EPOLL_CTL_MOD:修改 epoll 检测集合对 fd 的观测方式
  3. epoll_wait:等待 epoll 检测集合中的事件,第二个参数通常是一个 epoll_event 数组,用于接收返回的 io 就绪对应的 fd 属性,第四个参数 timeout 用于控制超时时间,时间到时停止阻塞,如果为 -1 则表示一直阻塞,返回就绪事件的个数

什么是 epoll_event?

epoll_event 描述了文件描述符 fd 的属性,把 io 事件与fd关联起来,我们 无法得知fd具体是什么,但当 io 事件发生时(fd 可读、可写),就可以与之相对应

reactor 网络框架

什么是 reactor?

reactor 描述了一种编程方式,即每一个fd对应于一个 io 事件,而每一个 io 事件又对应一个回调函数,当出现网络连接,触发 io 事件时,我们就可以通过回调函数的方式对它进行相关操作
在这里插入图片描述

图片解析

  1. EPOLLIN 代表可读、EPOLLOUT代表可写,对于客户端和服务端之间,要么是服务端接收连接 accept,要么是服务端接收数据 recv
  2. 将 sockfd 置为 EPOLLIN 为第一个状态,后续与客户端之间的 io 操作根据 clientfd 的 EPOLL 状态来完成

思路

  1. 需要先把 sockfd 置为 EPOLLIN,当客户端与服务端连接时,触发 sockfd 上的观测事件,epoll_wait 失去阻塞,调用 accept_cb 回调函数
  2. 调用 accept 函数后,拿到 clientfd,对 clientfd 注册接下来的回调函数(recv_callback、send_callback),观测 clientfd 的 io 就绪状态
  3. 回到主函数的事件处理循环,根据 clientfd 的 io 就绪状态分别进行不同的回调处理

拓展

  1. 通过添加服务所拥有的端口数提高连接数
  2. 统计每一千个连接所需要的时间
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/time.h>

#define MAX_SIZE 1024
#define MAX_CONN 1048576

#define MAX_PORTS 20
#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

typedef int (*CALLBACK)(int fd);

struct conn {
    int fd;
    char rbuffer[MAX_SIZE];
    int rlength;
    char wbuffer[MAX_SIZE];
    int wlength;
    union {
        CALLBACK recv_callback;
        CALLBACK accept_callback;
    } r_action;
    CALLBACK send_callback;
};

int accept_cb(int fd);
int send_cb(int fd);
int recv_cb(int fd);

int epfd = 0;
struct conn conn_list[MAX_CONN] = {0};
struct timeval begin;

int set_event(int fd, int event, int flag) {
    struct epoll_event ev;
    ev.events = event;
    ev.data.fd = fd;
    if (flag) {
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    } else {
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
    }
    return 0;
}

int register_event(int fd, int event) {
    conn_list[fd].fd = fd;
    conn_list[fd].r_action.recv_callback = recv_cb;
    conn_list[fd].send_callback = send_cb;
    conn_list[fd].rlength = 0;
    memset(conn_list[fd].rbuffer, 0, MAX_SIZE);
    conn_list[fd].wlength = 0;
    memset(conn_list[fd].wbuffer, 0, MAX_SIZE);

    set_event(fd, event, 1);
}

int recv_cb(int fd) {
    int count = recv(fd, conn_list[fd].rbuffer, MAX_SIZE, 0);
    if (count == 0) {
        close(fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
    }
    conn_list[fd].rlength = count;
    conn_list[fd].wlength = count;
    memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, count);
    set_event(fd, EPOLLOUT, 0);
    return 0;
}

int accept_cb(int fd) {
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    int clientfd = accept(fd, (struct sockaddr *)&clientaddr, &len);
    if (clientfd < 0) {
        return -1;
    }

    register_event(clientfd, EPOLLIN);

    if (clientfd % 1000 == 0) {
        struct timeval current;
        gettimeofday(&current, NULL);
        int time_used = TIME_SUB_MS(current, begin);
        memcpy(&begin, &current, sizeof(struct timeval));
        printf("accept finished fd:%d, time %d\n", clientfd, time_used);
    }

    return 0;
}

int send_cb(int fd) {
    send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
    set_event(fd, EPOLLIN, 0);
    return 0;
}

int init_server(uint16_t port) {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(port);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
        printf("bind failed %s\n", strerror(errno));
        return -1;
    }
    listen(sockfd, 5);
    return sockfd;
}

int main(void) {
    uint16_t port = 2000;
    epfd = epoll_create(1);

    for (int i = 0; i < MAX_PORTS; i++) {
        int sockfd = init_server(port + i);
        conn_list[sockfd].fd = sockfd;
        conn_list[sockfd].r_action.recv_callback = accept_cb;
        set_event(sockfd, EPOLLIN, 1);
    }

    gettimeofday(&begin, NULL);

    while (1) {
        struct epoll_event events[1024];
        int nready = epoll_wait(epfd, events, 1024, -1);
        for (int i = 0; i < nready; i++) {
            int connfd = events[i].data.fd;
            if (events[i].events & EPOLLIN) {
                conn_list[connfd].r_action.recv_callback(connfd);
            }
            if (events[i].events & EPOLLOUT) {
                conn_list[connfd].send_callback(connfd);
            }
        }
    }

    return 0;
}

总结

  1. 实现了单线程百万 io 并发处理
  2. 通过 服务初始化 - 注册回调 - 事件回调的方式,当客户端出现 io 事件,触发回调,就可以对其进行处理

技术参考


https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值