【C/C++】从 epoll 到 reactor 网络框架
本文描述了 从 epoll 到实现 reactor 百万 io 并发的编程模式
什么是 epoll?
epoll 是 Linux 的一个处理 io 并发的模式,它包含三个函数
- epoll_create:返回一个引用新 epoll 实例的文件描述符,epoll 实例是一个检测集合
- epoll_ctl:操作 epoll 实例,利用第二个参数的值来改变 fd 与 epoll 实例之间的关系,一共有三种关系,第四个参数 epoll_event 用于控制检测集合的观测方式
- EPOLL_CTL_ADD:将 fd 注册到 epoll 检测集合中
- EPOLL_CTL_DEL:将 fd 从 epoll 检测集合中移除
- EPOLL_CTL_MOD:修改 epoll 检测集合对 fd 的观测方式
- 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 事件时,我们就可以通过回调函数的方式对它进行相关操作
图片解析
- EPOLLIN 代表可读、EPOLLOUT代表可写,对于客户端和服务端之间,要么是服务端接收连接 accept,要么是服务端接收数据 recv
- 将 sockfd 置为 EPOLLIN 为第一个状态,后续与客户端之间的 io 操作根据 clientfd 的 EPOLL 状态来完成
思路
- 需要先把 sockfd 置为 EPOLLIN,当客户端与服务端连接时,触发 sockfd 上的观测事件,epoll_wait 失去阻塞,调用 accept_cb 回调函数
- 调用 accept 函数后,拿到 clientfd,对 clientfd 注册接下来的回调函数(recv_callback、send_callback),观测 clientfd 的 io 就绪状态
- 回到主函数的事件处理循环,根据 clientfd 的 io 就绪状态分别进行不同的回调处理
拓展
- 通过添加服务所拥有的端口数提高连接数
- 统计每一千个连接所需要的时间
#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(¤t, NULL);
int time_used = TIME_SUB_MS(current, begin);
memcpy(&begin, ¤t, 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;
}
总结
- 实现了单线程百万 io 并发处理
- 通过 服务初始化 - 注册回调 - 事件回调的方式,当客户端出现 io 事件,触发回调,就可以对其进行处理