Reactor实战-百万TCP连接
1 Reactor模型基本原理
我们知道Epoll的IO模型性能优于传统的select和Poll模型由轮询的方式改成的了事件通知的方式。Reactor正是居于Epoll模型的封装,这种封装主要的目的是将对系统IO的管理转换为对于事件的管理。大大的提高了事件处理的效率。
2 Reactor水平触发与边沿触发
水平触发可以理解为对于内核读缓冲区和写缓冲区向用户空间一种触发方式。
2.1 水平触发
2.1.1 表现形式
对于读缓冲区:当内核读缓冲区有数据,epoll_wait会一直被触发,直到没有数据可读为止;
对于写缓冲区:当内核写缓冲区有空间可以写,epoll_wait会一直触发,直到写缓冲区满了为止。
2.1.2 使用场景
- 对于小数据量的处理,每次可以处理完成;
- 对于listenfd的监听,我们也希望用水平触发防止有客户端被遗漏;
2.2 边沿触发
2.2.1 表现形式
对于读缓冲区:当收到一次来自客户端的数据请求,epoll_wait会触发一次,用户需要自己实现将数据全部读出;
对于写缓冲区:当客户添加一次EPOLLOUT事件,epoll_wait会触发一次。
2.2.2 使用场景
- 数据量比较大;(分界线:一次性能否读完数据:一次读完小数据;否则是大数据)
- 重IO操作
3 Reactor模型百万连接实战
3.1 数据结构的定义
Reactor模型event事件采用二维数组进行组织,利用链表将事件块串在一起,每一个事件块大小定义为1024个事件组成。可以动态增加链表长度。事件元素包含基本参数包括:基本socket fd,事件,回调函数参数,回调函数,数据buf和数据长度等组成。事件块只包含了块指针和指向下一个块指针。Reactor包含:epoll fd,块的数量和链表头结点。
//单一事件元素的定义
typedef struct event_item_{
int fd;
int event;
void *arg;
int (*callback)(int fd, int events, void *arg);
int status;//是否已经添加事件
unsigned char buffer[MAX_BUFFER_LENGTH];
int length;
}event_item;
//事件块链表节点定义
typedef struct event_blk_ {
event_item *events;//事件块(单块定义1024大小)
struct event_blk_ *next;
}event_blk;
//reactor定义
typedef struct reactor_
{
int epfd;//epoll创建的efd
int blk_count;//事件块数量
event_blk *ev_blk;//链表头结点
}reactor;
3.2 事件处理
(1)事件处理比较简单主要是增删改等操作。对于的epoll_ctl接口操作。主要选项包括:EPOLL_CTL_ADD,EPOLL_CTL_MOD, EPOLL_CTL_DEL等操作。
int set_event(event_item *ev, int fd, EVENT_CALLBACK callback, void *arg)
{
ev->fd = fd;
ev->callback = callback;
ev->event = 0;
ev->arg = arg;
}
int add_event(int epfd, int event, event_item *ev)
{
struct epoll_event ep_ev = {0, {0}};
ep_ev.data.ptr = ev;
ep_ev.events = ev->event = event;
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) {
printf("event add failed [fd=%d], events[%d]\n", ev->fd, event);
printf("fd[fd=%d] error[%d]:%s\n", ev->fd, errno, strerror(errno));
return -1;
}
return 0;
}
int del_event(int epfd, event_item *ev)
{
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;
}
3.3 Reactor初始化
Reactor初始化主要是对于结构体内部的epfd分配利用:epoll_create进行创建;另一个是分配第一个事件块大小;后续根据实际需要可以动态扩展。
//块扩展,每次申请一个1024大小的块
int reactor_alloc(reactor *re)
{
if(re == NULL) return -1;
event_blk *blk = (event_blk *)malloc(sizeof(event_blk));
if(blk == NULL)
{
printf("reactor_alloc event_blk failed.\n");
return -2;
}
event_item * item = (event_item *)malloc(sizeof(event_item)*MAX_EPOLL_LENGTH);
if(item == NULL)
{
free(blk);
printf("reactor_alloc event_item failed.\n");
return -2;
}
blk->events = item;
blk->next = NULL;
//获取链表尾部
event_blk *cur = re->ev_blk;
if(cur == NULL)
{
re->ev_blk = blk;
re->blk_count = 1;
return 0;
}
else
{
while (cur->next)
{
cur = cur->next;
}
cur->next = blk;
re->blk_count++;
}
return 0;
}
//初始化Reactor
int reactor_init(reactor *re)
{
if(re == NULL) return -1;
memset(re, 0, sizeof(reactor));
re->epfd = epoll_create(1);
if (re->epfd <= 0) {
printf("create epfd in %s err %s\n", __func__, strerror(errno));
return -2;
}
reactor_alloc(re);
return 0;
}
//释放Reactor对象
void destory_reactor(reactor *re)
{
close(re->epfd);
event_blk *blk = re->ev_blk;
event_blk *blk_next = NULL;
while (blk->next)
{
blk_next = blk->next;
free(blk);
free(blk->events);
}
}
3.4事件监听和循环
事件监听过程主要是对于epoll注册的事件进行处理,并且回调到具体的回调函数中。
//核心api,Reactor事件派发在这里完成,利用:epoll_wait完成事件触发回调
int reactor_run(reactor *re) {
if (re == NULL) return -1;
if (re->epfd < 0) return -1;
struct epoll_event events[MAX_EPOLL_LENGTH+1];
int checkpos = 0, i;
while (1) {
int nready = epoll_wait(re->epfd, events, MAX_EPOLL_LENGTH, 1000);
if (nready < 0) {
printf("epoll_wait error, exit\n");
continue;
}
for (i = 0;i < nready;i ++) {
event_item *ev = (event_item*)events[i].data.ptr;
if ((events[i].events & EPOLLIN) && (ev->event & EPOLLIN)) {
ev->callback(ev->fd, events[i].events, ev->arg);
}
if ((events[i].events & EPOLLOUT) && (ev->event & EPOLLOUT)) {
ev->callback(ev->fd, events[i].events, ev->arg);
}
}
}
}
3.5 回调函数实现
回调函数分为接收数据后回调,发送数据回调和accept接收客户端回调函数。利用这些回调实现了整个服务器处理流程。首先是客户端连接请求,然后客户端向服务器发送数据,服务器处理数据后回发给客户端。将整个数流程串联起来。
//这里需要根据socketfd找到对应的块,利用的是soketfd与块的对应寻找,当溢出时需要扩展块的数量。
event_item *reactor_get_event_withfd(reactor *re, int sockfd)
{
if(re == NULL) return NULL;
int blk_num = sockfd / MAX_EPOLL_LENGTH;
while(blk_num >= re->blk_count)
{
reactor_alloc(re);
}
int i = 0;
event_blk *blk = re->ev_blk;
for(i = 0; i < blk_num && blk != NULL; i++)
{
blk = blk->next;
}
int index = sockfd % MAX_EPOLL_LENGTH;
return &blk->events[index];
}
int recv_cb(int fd, int event, void *arg)
{
reactor *re = (reactor *)arg;
if(re == NULL) return -1;
event_item *ev = reactor_get_event_withfd(re, fd);
int len = recv(fd, ev->buffer, MAX_BUFFER_LENGTH, 0);
del_event(re->epfd, ev);
if(len > 0){
ev->length = len;
ev->buffer[len] = '\0';
printf("C[%d]:%s\n", fd, ev->buffer);
set_event(ev, fd, send_cb, re);
add_event(re->epfd, EPOLLOUT, ev);
}else if(len == 0){
close(ev->fd);
printf("[fd=%d] pos[%d], closed\n", fd, ev->event);
}else{
if (errno == EINTR || errno == EWOULDBLOCK)
return len;
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return len;
}
int send_cb(int fd, int event, void *arg)
{
reactor *re = (reactor *)arg;
if(re == NULL) return -1;
event_item *ev = reactor_get_event_withfd(re, fd);
int len = send(fd, ev->buffer, ev->length, 0);
if (len > 0) {
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buffer);
del_event(re->epfd, ev);
set_event(ev, fd, recv_cb, re);
add_event(re->epfd, EPOLLIN, ev);
} else {
del_event(re->epfd, ev);
if (errno == EINTR || errno == EWOULDBLOCK) {
return len;
}
close(ev->fd);
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return len;
}
int accept_cb(int fd, int event, void *arg)
{
reactor *re = (reactor *)arg;
if(re == NULL) return -1;
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int clientfd;
if ((clientfd = accept(fd, (struct sockaddr*)&client_addr, &len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
}
printf("accept: %s\n", strerror(errno));
return -1;
}
int flag = 0;
if ((flag = fcntl(clientfd, F_SETFL, O_NONBLOCK)) < 0) {
printf("%s: fcntl nonblocking failed, %d\n", __func__, MAX_EPOLL_LENGTH);
return -2;
}
event_item *ev = NULL;
ev = reactor_get_event_withfd(re, clientfd);
set_event(ev, clientfd, recv_cb, re);
add_event(re->epfd, EPOLLIN|EPOLLET, ev);
printf("new connect [%s:%d] pos[%d]\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), fd);
return 0;
}
int reactor_addlistener(reactor *re, int sockfd, EVENT_CALLBACK *acceptor) {
if (re == NULL) return -1;
event_item *ev = NULL;
ev = reactor_get_event_withfd(re, sockfd);
set_event(ev, sockfd, acceptor, re);
add_event(re->epfd, EPOLLIN, ev);
return 0;
}
4 设备配置要点
修改fd的socket数量,这里是临时修改:
ulimit -a
ulimit -n 1024*1024
修改协议栈和接收发送缓冲区大小,以便于提高单台设备的连接数量,需要根据实际业务调整
vi /etc/sysctl.cof
net.ipv4.tcp_mem = 262144 524288 786432
net.ipv4.tcp_wmem= 1024 1024 2048
net.ipv4.tcp_rmem = 1024 1024 2048
生效:
/sbin/sysctl -p
5 具体测试
需要一个客户端模拟连接,服务器负责接收连接。