reactor百万并发模型
一、reactor是什么?
reactor是一种事件驱动的方式,通常用于处理大量的客户端请求,根据i/o上触发的读写事件,执行不同的回调函数,由之前的io管理,转变为对事件的管理。常常用在高并发的网络IO编程中。
二、reactor的实现步骤
***事件源:***指可以触发事件的一些实体,比如网络I/O套接字,当套接字状态发生变化,比如有数据可读、有数据可写时,会触发一个事件。
事件循环: reactor依赖于一个事件循环机制,他会不断检测事件源上是否有事件发生,当有事件发生后,会把事件分发下去。
事件处理: 事件源上不同的事件,有自己的对应的事件处理函数,不同的事件处理具体的业务逻辑。
三、reactor的工作流程
1、监听事件:reactor会监听所有注册的事件源,(比如套接字),并等待事件的发生。
2、事件触发:当某个事件源准备好进行某种操作时,(例如网络连接 的有数据可读),reactor就会触发事件。
3、分发事件:reactor会将事件分发给对应事件处理函数,每个事件处理器负责处理特定的类型事件 比如 数据读取、写入等。
4、处理事件:事件处理器会执行与事件相关的操作,比如读取数据、处理请求、发送响应。
四、具体实现(epoll方式)
1.设计连接池
首先定义一个连接池,其目的是将每个io连接和其相关的数据进行关联,通过该连接池的文件描述符就可以很方便的找到相关联的数据。连接池定义如下
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; // 这里为什么做个联合体(共用同一块内存) 就是recv 和accept 对应的事件都是读事件
};
2、事件源关联函数
事件源就是sockfd,一个网络IO,分为listenfd和clientfd.事件即为可读或可写,在EPOLL中事件源关联事件,将fd添加到epoll的红黑树上,并设置监听事件,通过epoll进行管理。
int set_event(int fd, int event, int flag)
{
if(flag) // 1 add
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
}else{ //0 修改
struct epoll_event ev;
ev.events = event;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
}
3、定义回调函数
需要定义三个回调函数
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)
{
printf("accept errno: %d ----> %s \n", errno, strerror(errno));
return -1;
}
event_register(clientfd, EPOLLIN); // 或上就是加个共功能,
// 默认水平触发
// EPOLLET 边沿触发 event_register(clientfd, EPOLLIN | EPOLLET);
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("client finished: %d, time_used:%d \n", clientfd, time_used);
}
return 0;
}
int recv_cb(int fd)
{
// 收数据之前将其清空
memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
// 阻塞的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); // unfinished
return 0;
// continue;
}
else if(count < 0) // 返回-1是因为 对方发出rst包(tcp包)正常现象
{
printf("count : %d, errno: %d, %s\n", count, errno, strerror(errno));
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
return 0;
}
conn_list[fd].wLength = count;
// printf("recv:%s", 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 = 0;
#if 1
if(conn_list[fd].status == 1)
{
count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wLength, 0);
// 继续设置该文件描述符为 输出事件
set_event(fd, EPOLLOUT, 0);
}else if(conn_list[fd].status == 2){
set_event(fd, EPOLLOUT, 0);
}else if(conn_list[fd].status == 0)
{
// 如果wlength不等于0, 调用一下send
if(conn_list[fd].wLength !=0)
{
count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wLength, 0);
}
set_event(fd, EPOLLIN, 0);
}
#else
if(conn_list[fd].wLength !=0)
{
count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wLength, 0);
}
set_event(fd, EPOLLIN, 0);
#endif
// printf("111111111111111111");
return count;
}
```
4、设置回调函数
回调函数在连接池中,我们可以哦通过访问fd关联的连接池。在listenfd和clientfd创建时,设置他们的回调函数。
listenfd:
int sockfd = init_Server(port + i);
conn_list[sockfd].fd = sockfd;
conn_list[sockfd].r_action.accept_callback = accept_cb;
set_event(sockfd, EPOLLIN, 1);
clientfd:
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);
}
5、主循环
// mainloop 主循环 7*24小时一直运行
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);
}
}
}
通过以上5个步骤实现了一个简易的reactor,并且可通过该rreactor实现百万级并发
五、百万级并发实测
影响服务器并发量的因素有下面几个
1.socket :too many open files 操作系统限制文件打开数量
通过 ulimit -a 查看数量 通过 ulimit -n 1048576来修改
2.修改/etc/security/limits.conf
3.系统资源限制
通常一个tcp连接由五元组组成
服务器ip 服务器端口 客户端端ip 客户端端口 4层网络协议
可以在服务器端开放多个端口,也就是创建多个listenfd, 这样可以提高服务器的并发数量
4. net.nf_conntrack_max 修改网络防火墙允许链路建立的个数
先修改 conntrack_max 数量 然后执行 sysctl -p 使其生效
modprobe nf_conntrack 修改,使其启动 nf_conntrack 模块
5.实测截图
六、 百万并发 reactor的优点:
- 由之前对IO的管理,转变为对事件的管理
- 即 不同的io事件,对应不同的回调函数
这里主要有两步:
1、register 首先注册事件
2、callbcak 调用回调函数
io —> event ---->callback
listenfd ----> EPOLLIN -----> accept_cb
clientfd -----> EPOLLIN ------> recv_cb
clientfd ------> EPOLLOUT ------> send_cb
七、 epoll的两种工作模式:
1、水平触发(LT)
对于读事件:只要socket上有未读完的数据,EPOLLIN 就会一直触发;
对于写事件:只要socket可写,指的是tcp缓冲区未满,EPOLLOUT就会一直触发
2、边沿触发(ET)
对于读事件:只要socket的读缓冲区有数据,从无到有,EPOLLIN才会读
对于写事件:只有在socket的写缓冲区从不可写变为可写,EPOLLOUT 才会触发
举例:客户端发送32个字节,服务端接收10个字节,
a. 边沿只触发一次,底层内核socket的读缓冲区有32个字节内容,但是通知应用层这里去读,只通知一次,而应用层的缓冲区大小只有10个字节;最后只读了10个字节。
b. 水平触发,只要底层内核socket的读缓冲区,有未读完的数据,就会一直读,socket读缓冲区有32个字节内容,读了10个字节,没有读完,会继续触发读事件,直到缓冲区事件读完。
注意:边沿触发时 比如触发读事件后必须把数据收取干净,因为你不一定有下一次机会再收取数据了,
即使不采用一次读取干净的方式,也要把这个激活状态记下来,后续接着处理,
否则如果数据残留到下一次消息来到时就会造成延迟现象。
所以一般边沿触发处理读事件时需要用while(1)循环处理