实现Reactor模型
reactor 将 io 管理,转变为事件管理,核心在于不同的 io 事件,对应不同的回调函数,先register(注册),后callback(回调)
io | event | callback |
---|---|---|
listenfd | EPOLLIN | accept_cb |
clientfd | EPOLLIN | recv_cb |
clientfd | EPOLLOUT | send_cb |
一共有两类 io,分别是用于监听的 listenfd 和用于通信的 clientfd;一共有两类事件,分别是可读事件 EPOLLIN 和 可写事件EPOLLOUT,要实现 reactor,重点在于 event 与 callback 的匹配以及每一个 io 与之对应的参数
io 与之对应的参数结构体可以定义如下:
struct conn
{
int fd; // io
char rbuffer[BUFFER_LENGTH]; // 读缓冲区
int rlength; // 读缓冲区长度
char wbuffer[BUFFER_LENGTH]; // 写缓冲区
int wlength; // 写缓冲区长度
RCALLBACK send_callback;
union // recv_cb和accept_cb在同一个io中不可能同时出现
{
RCALLBACK recv_callback;
RCALLBACK accept_callback;
} r_action;
};
总体代码如下所示:
#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#define BUFFER_LENGTH 1024
#define CONNECTION_SIZE 1024
typedef int (*RCALLBACK)(int fd);
int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);
int epfd = 0;
struct conn
{
int fd;
char rbuffer[BUFFER_LENGTH];
int rlength;
char wbuffer[BUFFER_LENGTH];
int wlength;
RCALLBACK send_callback;
union
{
RCALLBACK recv_callback;
RCALLBACK accept_callback;
} r_action;
};
struct conn conn_list[CONNECTION_SIZE] = {0};
int set_event(int fd, int event, int flag)
{
if (flag)
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
}
else
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
}
int event_register(int fd, int event)
{
if (fd < 0) return -1;
conn_list[fd].fd = fd;
conn_list[fd].r_action.recv_callback = recv_cb;
conn_list[fd].send_callback = send_cb;
memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
conn_list[fd].rlength = 0;
memset(conn_list[fd].wbuffer, 0, BUFFER_LENGTH);
conn_list[fd].wlength = 0;
set_event(fd, event, 1);
}
int accept_cb(int fd)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(fd, (struct sockaddr *)&clientaddr, &len);
printf("accept finished: %d\n", clientfd);
if (clientfd < 0)
{
printf("accept errno: %d --> %s\n", errno, strerror(errno));
return -1;
}
event_register(clientfd, EPOLLIN);
return 0;
}
int recv_cb(int fd)
{
int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
if (count == 0)
{
printf("client disconnect: %d\n", fd);
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
return 0;
}
conn_list[fd].rlength = count;
printf("RECV: %s\n", conn_list[fd].rbuffer);
conn_list[fd].wlength = conn_list[fd].rlength;
memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);
set_event(fd, EPOLLOUT, 0);
return count;
}
int send_cb(int fd)
{
int count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
set_event(fd, EPOLLIN, 0);
return count;
}
int init_server(unsigned short port)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
if (-1 == bind(sockfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s\n", strerror(errno));
}
listen(sockfd, 10);
printf("listen finshed: %d\n", sockfd);
return sockfd;
}
int main()
{
unsigned short port = 2000;
epfd = epoll_create(1);
int sockfd = init_server(port);
conn_list[sockfd].fd = sockfd;
conn_list[sockfd].r_action.recv_callback = accept_cb;
set_event(sockfd, EPOLLIN, 1);
while (1)
{
struct epoll_event events[1024] = {0};
int nready = epoll_wait(epfd, events, 1024, -1);
int i = 0;
for (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);
}
}
}
这段一共不到200行的代码,虽然看起来简陋,也确实只是玩具,但也实现了一个基于 epoll 的 reactor 网络模型
各个函数的作用如下:
(1)set_event(int fd, int event, int flag)
这个函数用于设置 epoll 事件。它将文件描述符 fd 注册到 epoll 实例 epfd,注册的事件类型是 event(如 EPOLLIN, EPOLLOUT 等)。flag 参数决定是添加新的文件描述符(EPOLL_CTL_ADD),还是修改现有的文件描述符事件(EPOLL_CTL_MOD)。
(2)event_register(int fd, int event)
这个函数用于初始化并注册一个新的连接到 epoll 实例。它设置文件描述符 fd 的基本属性(接收和发送缓冲区、回调函数等),并且调用 set_event() 将其添加到 epoll 监视列表中。
(3)accept_cb(int fd)
这是一个回调函数,当服务器套接字准备好接受新连接时调用。它接受新的客户端连接,打印相关信息,并注册新客户端套接字以便接收数据。
(4)recv_cb(int fd)
这是一个回调函数,当数据准备好被读取时调用。它从套接字 fd 读取数据到接收缓冲区,并将数据复制到发送缓冲区,准备回显给客户端。此函数还处理客户端断开连接的情况。
(5)send_cb(int fd)
这是一个回调函数,用于当套接字准备好发送数据时调用。它将数据从发送缓冲区写入到客户端套接字,并将套接字的事件重新设置为 EPOLLIN 以继续接收来自客户端的数据。
(6)init_server(unsigned short port)
此函数用于初始化服务器,创建套接字,绑定到指定端口,并开始监听连接请求。
(7)main()
这是程序的入口点。它创建 epoll 实例,初始化服务器套接字,并进入主循环,不断等待 epoll 事件。当一个事件被触发时,根据套接字状态调用相应的回调函数处理连接或数据传输。
后续会用这份 reactor 代码实现 http server(也就是C++选手熟知的烂大街的webserver)和 websocket server,后面还会更新知名的用 reactor 模型设计的网络库,如muduo等。