【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(&current, NULL);
        int time_used = TIME_SUB_MS(current, begin);
        memcpy(&begin, &current, 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的优点:

  1. 由之前对IO的管理,转变为对事件的管理
  2. 即 不同的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)循环处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值