定义:reactor是一种事件处理模式,用于处理通过一个或多个输入同时交付给服务处理程序的服务请求。 然后,服务处理程序对传入的请求进行多路分解,并将它们同步分发到关联的请求处理程序。
从定义我们可以知道,首先reactor是 一种事件驱动,并且有一个或多个输入源、然后就是服务程序对所有的请求进行处理。这是不是有些像C/S架构呀,就是锁哥客户端连接同一个服务端进行读写操作。但是通过之前说的,通过epoll就可以实现一个服务器了,这个服务器也是可以支持处理多个连接请求建立之后的读写的,那为什么还需要reactor呢,又或者说reactor的数出现解决了什么问题呢?
首先我们来看着张截图,主要是buffer的输出和发送,这版代码存储发送和结束使用的缓冲区分别是rbufer和wbuffer。
现在我们再来考虑epoll实现的服务器中可能会出现哪些问题,对于上面的这版server来说,当多个客户端成功建立连接后,然后每个客户端进行接发数据都是正常的,但是如果我们将rbuffer[len] = '\0’注释掉之后再进行建立连接发送数据时会发生什么相关呢?
//rbuffer[len] = '\0';
现象就是会因为共用缓冲区从而导致数据发送覆盖现象,也就是先发送的客户端发送数据还在缓冲区内,后发送的客户端发送的数据会覆盖缓冲区之前已经存在的数据,如果此时后发送到的数据量小于前面发送的数据量,那么此时后发送的客户端接收到的数据除了自己发送的数据之外还有之前一个客户端发送的数据。
解决这种问题办法就是分别给每个连接都分配独立的读写buffer,这样每个连接的数据就不会发生数据覆盖了。Reactor的出现就是为了解决这个问题的。
在代码实现Reactor实现前,我们还是需要好好分析一下,Reactor应该做的工作有哪些,或者说应该怎样去设计实现一个Reactor呢?
首先我们根据上面出现的问题,我们想到,reactor里面必须满足每个连接建立都拥有独立的读写缓冲区,然后我们再考虑下,如何去标示不同连接的读写缓冲区呢,答案是通过文件描述符fd,因为每个连接都依赖一个fd,也就是说fd是独一的,自然可以作为区分buffer的标识。考虑到似乎已经解决了上面遇到的问题,结果也确实是,但实际的reactor实现势必我们分析的要严谨多的,
实际的reactor是通过三个结构体实现,我们分别来看看
struct sock_item {
int fd;
char *rbuffer;
int rlength;
char *wbuffer;
int wlength;
int event;
void (*recv_cb)(int fd, char *buffer, int length); // 回调函数
void (*send_cb)(int fd, char *buffer, int length);
void (*accept_cb)(int fd, char *buffer, int length);
};
这个结构体封装了独立的读写缓冲区,也对应将fd对应的三件事通过回调函数封账在一起了,除此之外还有event表示事件的种类,这样,我们让一个成功建立的客户端连接拥有这样一个结构体就实现了读写缓冲的分离。
还有个问题,就是在建立连接的时候,我们应该给客户端持有的内存空间怎么处理,首先肯定是不能分配固定的,因为这样根本支撑不了百万并发。所以这里要使用动态扩容,就是我们将内存分为好几个块,每个块存储固定数量的fd数组,块与块之间依赖指针实现,采用单链表去实现一个event_block块。
// 实现动态扩容,不能使用头插法,会破坏fd的有序性
struct event_block {
struct sock_item *item;
struct event_block *next;
};
我简单画了个图,凑合着看吧。
我们这样好处就是我们在开始的时候只需要分配一个event_block块即可,当客户端不断到来建立连接时,当第一个event_block块已经满的时候,这时我们在申请一个event_block块继续按照fd 的顺序进行存储,这样,我们就能很大程度上去利用内存了。
然后我们在客户端建立连接的时候去查询到这个fd所在的sock_item位置,找到item之后就给对应的rbuffer、wbuffer申请空间,这样就实现了动态申请内存的状态。
// 根据分配的fd确定sock_item的位置
struct sock_item* reactor_lookup(struct reactor *r, int sockfd) {
if(r == NULL || sockfd <= 0) {
std::cout << "参数错误!" << std::endl;
return NULL;
}
int blkidx = sockfd / ITEM_LENGTH; // 获取fd应该处于第几块
while(blkidx >= r->blkcnt) { // 说明fd应该处于新分配的块
reactor_resize(r);
}
int i = 0;
struct event_block *blk = r->evblk;
while (i++ < blkidx && blk != NULL) {
blk = blk->next;
}
// std::cout << __FUNCTION__ << " " << __LINE__ << " " << "Index:" << sockfd % ITEM_LENGTH << std::endl;
return &blk->item[sockfd % ITEM_LENGTH];
}
第三个结构体也就是我们所说的reactor了
struct reactor {
int epfd;
int blkcnt;
struct event_block *evblk;
};
这里有三个参数,一个是监听的fd,一个记录块号,一个记录对应块,这样我们就可以通过evblk去查询到最终的sock_item了。其实说白了reactor就是三个结构体,但更重要的是这三个结构体的设计方式,以及这样设计解决的问题,我觉得这是比较重要的。
接下来就是百万并发的具体实现,需要准备三台虚拟机+一台服务器,服务器4G虚拟内存,其他2G就可以
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_LENGTH 128
#define EVENT_LENGTH 128
#define ITEM_LENGTH 1024
struct sock_item {
int fd;
char *rbuffer;
int rlength;
char *wbuffer;
int wlength;
int event;
void (*recv_cb)(int fd, char *buffer, int length); // 回调函数
void (*send_cb)(int fd, char *buffer, int length);
void (*accept_cb)(int fd, char *buffer, int length);
};
// 实现动态扩容,不能使用头插法,会破坏fd的有序性
struct event_block {
struct sock_item *item;
struct event_block *next;
};
struct reactor {
int epfd;
int blkcnt;
struct event_block *evblk;
};
int reactor_resize(struct reactor *r) {
if (r == NULL) return -1;
struct event_block *blk = r->evblk;
while (blk != NULL && blk->next != NULL) {
blk = blk->next;
}
struct sock_item* item = (struct sock_item*)malloc(ITEM_LENGTH * sizeof(struct sock_item));
if (item == NULL) return -4;
memset(item, 0, ITEM_LENGTH * sizeof(struct sock_item));
printf("-------------\n");
struct event_block *block = (event_block*)malloc(sizeof(struct event_block));
if (block == NULL) {
free(item);
return -5;
}
memset(block, 0, sizeof(struct event_block));
block->item = item;
block->next = NULL;
if (blk == NULL) {
r->evblk = block;
} else {
blk->next = block;
}
r->blkcnt++;
return 0;
}
// 根据分配的fd确定sock_item的位置
struct sock_item* reactor_lookup(struct reactor *r, int sockfd) {
if(r == NULL || sockfd <= 0) {
std::cout << "参数错误!" << std::endl;
return NULL;
}
int blkidx = sockfd / ITEM_LENGTH; // 获取fd应该处于第几块
while(blkidx >= r->blkcnt) { // 说明fd应该处于新分配的块
reactor_resize(r);
}
int i = 0;
struct event_block *blk = r->evblk;
while (i++ < blkidx && blk != NULL) {
blk = blk->next;
}
// std::cout << __FUNCTION__ << " " << __LINE__ << " " << "Index:" << sockfd % ITEM_LENGTH << std::endl;
return &blk->item[sockfd % ITEM_LENGTH];
}
int main(int argc, char *argv[] ) {
// 初始化服务器
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
memset(&serv_addr, 0x00, serv_len);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(9999);
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1) {
std::cout << "Create socket error:" << strerror(errno) << " Errno:" << errno << std::endl;
return -1;
}
if(bind(listenfd, (struct sockaddr *)&serv_addr, serv_len)) {
std::cout << "Bind listenfd error:" << strerror(errno) << " Errno:" << errno << std::endl;
return -1;
}
if(listen(listenfd, 10) == -1) {
std::cout << "Listen listenfd error:" << strerror(errno) << " Errno:" << errno << std::endl;
return -1;
}
struct reactor *r = (struct reactor *)calloc(1, sizeof(struct reactor));
if(r == NULL) {
std::cout << "Create reactor error:" << strerror(errno) << " Errno:" << errno << std::endl;
return -1;
}
r->epfd = epoll_create(1);
r->blkcnt = 0;
struct epoll_event ev, events[EVENT_LENGTH];
ev.data.fd = listenfd;
ev.events = EPOLLIN;
epoll_ctl(r->epfd, EPOLL_CTL_ADD, listenfd, &ev);
while(1) {
int nready = epoll_wait(r->epfd, events, EVENT_LENGTH, -1);
int i = 0;
for(i; i < nready; i++) {
int clientfd = events[i].data.fd;
if(clientfd == listenfd) {
// 连接请求
struct sockaddr_in client;
socklen_t cli_len;
memset(&client, 0x00, cli_len);
int connfd = accept(clientfd, (struct sockaddr*)&client, &cli_len);
if(connfd == -1) {
std::cout << "Accept errno:" << strerror(errno) << " Errno:" << errno << std::endl;
break;
}
std::cout << __FUNCTION__ << " " << __LINE__ << " Connfd:" << connfd << std::endl;
int flags = fcntl(connfd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flags);
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(r->epfd, EPOLL_CTL_ADD, connfd, &ev); // 将新连接发挥的connfd加入到epoll树上
// 为fd分配专属的读写buffer
struct sock_item *item = reactor_lookup(r, connfd);
if(item == NULL) {
std::cout << __FUNCTION__ << " " << __LINE__ << " " << "Connfd:" << connfd << std::endl;
}
item->fd = connfd;
// item->event = EPOLLIN; // 记录item事件
item->rbuffer = (char*)calloc(1, BUFFER_LENGTH);
item->rlength = 0;
item->wbuffer = (char*)calloc(1, BUFFER_LENGTH);
item->wlength = 0;
}
else if(events[i].events & EPOLLIN) {
// 读事件
struct sock_item *item = reactor_lookup(r, clientfd);
char *rbuffer = item->rbuffer;
char *wbuffer = item->wbuffer;
int len = recv(clientfd, rbuffer, BUFFER_LENGTH, 0);
if(len > 0) {
//std::cout << "Recv from buffer:" << rbuffer << std::endl;
memcpy(wbuffer, rbuffer, BUFFER_LENGTH);
// 更改fd的事件
ev.events = EPOLLOUT;
ev.data.fd = clientfd;
epoll_ctl(r->epfd, EPOLL_CTL_MOD, clientfd, &ev);
}
else if(len == 0) {
free(rbuffer);
free(wbuffer);
ev.data.fd = 0;
ev.events = 0;
close(clientfd);
}
}
else if(events[i].events * EPOLLOUT) {
// 写事件
struct sock_item *item = reactor_lookup(r, clientfd);
char *wbuffer = item->wbuffer;
int ret = send(clientfd, wbuffer, BUFFER_LENGTH, 0);
// 数据写完之后将事件修改为EPOLLIN
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(r->epfd, EPOLL_CTL_MOD, clientfd, &ev);
}
}
}
return 0;
}
1、上版代码测试首先会在27000-28000个客户端连接会断开,原因是Ubuntu的端口开发只有两万的多个,需要在sysctl.conf文件里将端口改为1024 65525。
2、再测试会在64000个连接断开,这时是因为宽口已经被占用完了,此时我们需要根据五元组,五元组能够确定一个连接,我们可以去增加端口或者服务器IP的方式去增大连接数,这里需要在服务器开放100个端口,上面的代码里我只开放了一个端口,也就是测试到64000的时候就会崩溃。
3、如果开放100端口正常是可以一台虚拟机是可以测到大概38万的并发量的。
我们来总结一下reactor的好处把。
1、epoll是对IO的管理、reactor是对事件进行管理,当某个fd有可读可写事件时会触发对应的回调函数,这样就实现了IO的业务解耦,因为reactor的一个事件对应不同的回调函数,这时reactor核心
2、对于未处理完的事件可以放在独立的buffer中,好处就是httpserver中,当GET/POST请求的数据量非常大的时候,我们recv限制在1024时,这时候我们可以先处理需要的信息,也就是http头,处理完了之后在接收数据,那么GET/PoST请求剩余的数据就会临时存储在独立buffer中,灵活性很大