这篇博文围绕着服务器网络连接的百万并发问题来谈谈reactor网络处理模型,将回答以下这几个问题:1.什么是reactor模式,2. 为什么会有reactor模式,3. 网络并发的瓶颈在哪里,4.以http连接为例,讲解如何实现epoll reactor的封装。
几乎所有的服务器中间件的网络模型都是epoll 封装的reactor模式,比如redis,nginx,memcached的网路层。那什么是reactor模式呢?
1.什么是reactor模式:
首先,我们来回想一下普通函数调用的机制:程序调用某函数,函数执行,程序等待,函数将 结果和控制权返回给程序,程序继续处理。Reactor 释义“反应堆”,是一种事件驱动机制。 reactor模式和普通函数调用的不同之处在于:应用程序不是主动的调用某个 API 完成处理,而是恰恰相反,Reactor 逆置了事件处理流程,应用程序需要提供相应的接口并注册到 Reactor上, 如果相应的时间发生,Reactor 将主动调用应用程序注册的接口,这些接口又称为“回调函数”。这样说比较抽象,简单而言,reactor模式就是当有网络连接过来的时候,自动调用对应的函数,即网络事件将对应一个的业务层接口,做到一事件,一处理。具体理解,还得看下面的代码。我将接着讲reactor模式的好处。
2.reactor模型的好处
首先,需要记住的是,reactor模式是 非阻塞io+epoll,原因是一个epoll要处理多个io连接,如果使用epoll的水平触发模式,当调用read()函数的时候,如果数据为空,则会被阻塞。另外,一个reactor对应一个epoll事件,而一个线程是一个epoll,因此,一个reactor对应一个线程,所以不会有多线程竞争的情况。
下面,我讲讲解我写的一个基于epoll的reactor网络处理模型:
1.)头文件和预处理定义:
#include <unistd.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h> //头文件不懂的,linux下直接man命令 或者 info命令
#define BUFFER_LENGTH 1024
#define MAX_EPOLL_EVENTS 1024
#define SERVER_PORT 9999 //顾名思义
#define PORT_COUNT 20
typedef int CALLBACK(int, int, void*); //回调函数指针
2.) 结构体定义:
//反应堆对应的事件,其实就是连接的事件
typedef struct reactor_events_{
int fd;
int event_type;//epoll的事件类型
void* arg;
(void*)callback(int, int, void*);
char rbuffer[BUFFER_LENGTH];
char wbuffer[BUFFER_LENGTH];
int rlength;
int wlength;
int status; //判断事件类型
int methods;
//这里根据业务逻辑,可以再加其他的方法
}reactor_events;
//连接事件的储存,采用事件块储存,动态分配
typedef struct event_block_{
struct reactor_events* event; //指向event结构体的指针
struct event_block* next; //指向下一块block的指针
}event_block;
//反应堆,一个反应堆,一个epoll,一个线程
typedef struct C_Reactor_{
int epfd; //一个反应堆对应一个epoll
struct event_block* evblock; //指向event_block的指针
int evblockidx; //当前reactor所有的event_block的数量,其实也是最后一块的索引
}C_Reactor;
3.)重要的函数:
(1)epoll事件块的设置,增加,删除(网络连接的设置,增加,删除)
int C_event_set(struct reactor_events* ev,int fd, CALLBACK callback,void* arg){
ev->fd=fd;
ev->events=0; //没有任何epoll事件发生
ev->callback=callback;
ev->arg=arg;
return 0;
}
int C_event_add(struct reactor_events* ev, int epfd,int events){
struct epoll_event ep_ev={0,{0}}; //表示一个epoll中的就绪事件 ,事件类型和epoll_data
ep_ev.data.ptr=ev,
ep_ev.events=ev->events=events;
int op;
if (ev->status == 1) {
op = EPOLL_CTL_MOD;
} else {
op = EPOLL_CTL_ADD;
ev->status = 1;
}
if (epoll_ctl(epfd, op, ev->fd, &ep_ev) < 0) return -1;
return 0;
}
int C_event_delete(struct reactor_events* ev,int epfd){
struct epoll_event ep_ev = {0, {0}};
if (ev->status != 1) {
return -1;
}
ep_ev.data.ptr = ev;
ev->status = 0;
epoll_ctl(epfd, EPOLL_CTL_DEL, ev->fd, &ep_ev);
return 0;
}
读者可能会疑惑一下的代码:
struct epoll_event ep_ev={0,{0}};
ep_ev.data.ptr=ev,
ep_ev.events=ev->events=events
那么,可以去看看我的其他博文《深入剖析linux 网络io多路复用》中对epoll原理的讲解。其实,epoll事件块是由红黑树来储存的,里面有一个联合体和一个结构体:具体代码如下
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; // 表示事件类型的位标志(bit flag)
epoll_data_t data; // 与事件相关的数据
};
当我们用把结构体指针reactor_events* ev赋值给ep_ev.data.ptr时,ev和epoll_data这个联合体就已经建立对应的关系,也即建立了一个epoll事件块。(联合体在初始化时只能对其中一个成员赋值,对其它成员的值会被覆盖)。
那么,到这里,我们先看一下,这个reactor模式的运行方式,然后我再讲解里面的函数实现
int main(int argc,char**agv){
struct C_reactor *reactor = (struct C_reactor*)malloc(sizeof(struct C_reactor)); //创建一个reactor结构体
C_reactor_init(reactor); //初始化
unsigned short port = SERVER_PORT;
if (argc == 2) {
port = atoi(argv[1]);
}
int i = 0;
int sockfds[PORT_COUNT] = {0};
for (i = 0;i < PORT_COUNT;i ++) {
sockfds[i] = init_sock(port+i); //增加端口号
C_reactor_addlistener(reactor, sockfds[i], accept_cb);
}
C_reactor_run(reactor);
C_reactor_destory(reactor);
for (i = 0;i < PORT_COUNT;i ++) {
close(sockfds[i]);
}
free(reactor);
return 0;
}
我们先来看C_reactor_init(reactor)
int C_reactor_init(struct C_Reactor*reactor){
reactor=(struct C_Reactor*)malloc(sizeof(struct CReactor));
memset(reactor,0,sizeof(struct CReactor));
reactor->epfd=epoll_create(1); //创建epoll
if(reactor->epfd<=0){
return -1;
}
struct reactor_events* ev=(struct reactor_events*)malloc((MAX_EPOLL_EVENTS)*sizeof(struct reactor_events));
memset(ev,0,((MAX_EPOLL_EVENTS)*sizeof(struct reactor_events)));
if(ev==nullptr){
free(reactor);
close(reactor->epfd);
return -2;
}
struct event_block* block=(struct event_block*)malloc(sizeof(struct event_block));
memset(block,0,sizeof(struct event_block));
if(block==nullptr){
free(reactor);
free(ev);
close(reactor->epfd);
return -3;
}
reactor->evblock=block;
reactor->evblockidx=1;
reactor->evblock->events=ev;
reactor->evblock->next=nullptr;
return 0;
}
event_block是动态分配的,分配一个event_block块,这样一个块中有MAX_EPOLL_EVENTS个epoll事件块,同时events_block的个数加1:
int C_reactor_alloc(struct C_reactor *reactor) {
if (reactor == NULL) return -1;
if (reactor->evblock == NULL) return -1;
struct event_block* blk = reactor->evblock;
while (blk->next != NULL) {
blk = blk->next;
}
struct reactor_event* evs = (struct reactor_event*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct reator_event));
if (evs == NULL) return -2;
memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct reactor_event));
struct event_block *block = malloc(sizeof(struct event_block));
if (block == NULL) return -3;
block->events = evs;
block->next = NULL;
blk->next = block;
reactor->evblockid++;
return 0;
}
在网络编程中,五元组(Five-Tuple)是指用于唯一标识网络通信中的一个数据流的五个要素。这五个要素包括:
-
源IP地址(Source IP Address):发送数据包的源设备的IP地址。
-
目标IP地址(Destination IP Address):接收数据包的目标设备的IP地址。
-
源端口号(Source Port Number):发送数据包的源设备使用的端口号。
-
目标端口号(Destination Port Number):接收数据包的目标设备使用的端口号。
-
传输层协议(Transport Layer Protocol):TCP、UDP等用于传输数据的协议.
int init_sock(short port) {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(fd, F_SETFL, O_NONBLOCK);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (listen(fd, 20) < 0) {
printf("listen failed : %s\n", strerror(errno));
return -1;
}
printf("listen server port : %d\n", port);
return listenfd;
}
//sockfd所在的event_block,创建event事件块
struct reactor_event *C_reactor_idx(struct C_reactor *reactor, int sockfd) {
if (reactor == NULL) return NULL;
if (reactor->evblock== NULL) return NULL;
int blkidx = sockfd / MAX_EPOLL_EVENTS;
while (blkidx >= reactor->evblockidx) {
C_reactor_alloc(reactor);
}
int i = 0;
struct event_block* blk = reactor->evblock;
while (i++ != blkidx && blk != NULL) {
blk = blk->next;
}
return &blk->events[sockfd % MAX_EPOLL_EVENTS];
}
然后,将我们设置的listenfd加入epoll事件块里面:
int C_reactor_addlistener(struct C_reactor *reactor, int sockfd, NCALLBACK *acceptor) {
if (reactor == NULL) return -1;
if (reactor->evblock== NULL) return -1;
struct reactor_events *event = C_reactor_idx(reactor, sockfd);
if (event == NULL) return -1;
C_event_set(event, sockfd, acceptor, reactor);
C_event_add(reactor->epfd, EPOLLIN, event);
return 0;
}
启动反应堆监听io的连接,实际上,就是不断地让epoll_wait()去轮询,当有epoll事件块状态发生改变的时候,就返回相应的fd
int C_reactor_run(struct C_reactor *reactor) {
if (reactor == NULL) return -1;
if (reactor->epfd < 0) return -1;
if (reactor->evblock == NULL) return -1;
struct epoll_event events[MAX_EPOLL_EVENTS+1];
int checkpos = 0;
while (1) {
int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, 1000);
if (nready < 0) {
printf("epoll_wait error, exit\n");
continue;
}
for (int i = 0;i < nready;i++) {
struct reator_event *ev = (struct reator_event*)events[i].data.ptr;
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {
ev->callback(ev->fd, events[i].events, ev->arg);
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
ev->callback(ev->fd, events[i].events, ev->arg);
}
}
}
}
最后就是将已经申请的资源释放掉:
}
int C_reactor_destory(struct C_reactor *reactor) {
close(reactor->epfd);
struct eventblock *blk = reactor->ev_block;
struct eventblock *blk_next;
while (blk != NULL) {
blk_next = blk->next;
free(blk->events);
free(blk);
blk = blk_next;
}
return 0;
}