redis中epoll模型-IO多路复用详情解析

阅读完redis中的网络模型源码,该文章会基于源码是如何去实现的角度,去介绍epoll模型、事件处理。以及基于此,自己实现的多人聊天终端系统,并附带源码。

1、epoll模型介绍

epoll 是Linux内核为处理大量并发网络连接而提出的解决方案,能显著提升系统CPU利用率。epoll使用非常简单,总共只有3个API :

  • epoll_create函数创建一个epoll专用的文件描述符,用于后续epoll相关API调用;
  • epoll_ctl函数向epoll注册、修改或删除需要监控的事件;
  • epoll_wait 函数会阻塞进程,直到监控的若干网络连接有事件发生。
1.1 int epoll_create(int size)

输人参数size通知内核程序期望注册的网络连接数目,内核以此判断初始分配空间大小;注意在Linux 2.6.8版本以后,内核动态分配空间,此参数会被忽略。返回参数为epoll专用的文件描述符,不再使用时应该及时关闭此文件描述符。

1.2 int epoll_ctl(int epfd, int op,int fd,struct epoll_event *event )

函数执行成功时返回0,否则返回-1,错误码设置在变量errno,输入参数含义如下。

  • epfd:函数epoll_create返回的epoll文件描述符。
  • op :需要进行的操作,EPOLL_CTL_ADD表示注册事件,EPOLL_CTL_MOD表示修改网络连接事件,EPOLL_CTL_DEL表示删除事件。
  • fd:网络连接的socket文件描述符。
  • event:需要监控的事件,结构体epoll_event定义如下:
struct epoll_event {
___uint32_t events;
epoll_data_t data;
};
typedef union epoll_data {
void *ptr;
int fd;
_uint32_t u32;
__uint64_t u64 ;
} epoll_data_t ;

其中events表示需要监控的事件类型,比较常用的是EPOLLIN文件描述符可读事件,EPOLLOUT 文件描述符可写事件; data保存与文件描述符关联的数据。

1.3 int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)

函数执行成功时返回0,否则返回-1,错误码设置在变量errno;输入参数含义如下:

  • epfd:函数epoll_create返回的epoll文件描述符;
  • epoll_event:作为输出参数使用,用于回传已触发的事件数组;
  • lmaxevents:每次能处理的最大事件数目;
  • timeout : epoll_wait 函数阻塞超时时间,如果超过timeout时间还没有事件发生,函数不再阻塞直接返回;当timeout等于0时函数立即返回,timeout等于-1时函数会一直阻塞直到有事件发生。
1.4 epoll模型剖析

在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。

  • 执行epoll_create时,创建了红黑树和就绪链表;
  • 执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;
  • 执行epoll_wait时立刻返回准备就绪链表里的数据即可。

以下看一个epoll模型实例:

(1)服务端epoll_create创建红黑树
(2)服务端监听端口,文件描述符fd1
(3)在epoll_ctl中注册fd1事件
(4)当客户端发起连接时,epoll_wait获取该fd1的事件,创建文件描述符fd2,在epoll_ctl中注册fd2事件(与当前客户端数据交互的通道);当客户端发送数据,epoll_wait获取该fd2的事件,接收数据。

gcc -o client client.c 、gcc -o server server.c、./server 127.0.0.1 7777、./client127.0.0.1 7777,后即可发送数据

//client.c客户端代码
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <netdb.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
        int connfd;
        struct sockaddr_in serveraddr;
        char buf[1024];

        connfd = socket(AF_INET, SOCK_STREAM, 0);

        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(argv[2]));
        inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);

        connect(connfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

        while(fgets(buf, 1024, stdin) != NULL){
                write(connfd, buf, strlen(buf));
        }

        close(connfd);
        return 0;
}

//server.c服务端代码
#include <sys/epoll.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#define MAX_EVENT_NUM           1024
#define BUFFER_SIZE             10
#define true                    1
#define false                   0

int setnonblocking(int fd)
{
        int old_opt = fcntl(fd, F_GETFD);
        int new_opt = old_opt | O_NONBLOCK;
        fcntl(fd, F_SETFD, new_opt);

        return old_opt;
}//将文件描述符设置为非阻塞的

void addfd(int epollfd, int fd, int enable_et)
{
        struct epoll_event event;
        event.data.fd = fd;
        event.events = EPOLLIN;
        if(enable_et){
                event.events |= EPOLLET;
        }
        epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
//      setnonblocking(fd);
}//将文件描述符fd的EPOLLIN注册到epollfd指示的epoll内核事件表中,enable_et表示是否对fd启用ET模式

void lt(struct epoll_event *events, int num, int epollfd, int listenfd)
{
        char buf[BUFFER_SIZE];
        for(int i = 0; i < num; i++){
                int sockfd = events[i].data.fd;
                if(sockfd == listenfd){
                        struct sockaddr_in clientaddr;
                        socklen_t clilen = sizeof(clientaddr);
                        int connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);
                        addfd(epollfd, connfd, false);//对connfd使用默认的lt模式
                }else if(events[i].events & EPOLLIN){//只要socket读缓存中还有未读的数据,这段代码就会触发
                        printf("event trigger once\n");
                        memset(buf, '\0', BUFFER_SIZE);
                        int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
                        if(ret <= 0){
                                close(sockfd);
                                continue;
                        }
                        printf("get %d bytes of content:%s\n", ret, buf);
                }else{
                        printf("something else happened\n");
                }
        }
}

void et(struct epoll_event *event, int num, int epollfd, int listenfd)
{
        char buf[BUFFER_SIZE];
        for(int i = 0; i < num; i++){
                int sockfd = event[i].data.fd;
                if(sockfd == listenfd){
                        struct sockaddr_in clientaddr;
                        int clilen = sizeof(clientaddr);
                        int connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);
                        addfd(epollfd, connfd, true);//多connfd开启ET模式
                }else if(event[i].events & EPOLLIN){
                        printf("event trigger once\n");
                        while(1){//这段代码不会重复触发,所以要循环读取数据
                                memset(buf, '\0', BUFFER_SIZE);
                                int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
                                if(ret < 0){
                                        if((errno == EAGAIN) || (errno == EWOULDBLOCK)){
                                                printf("read later\n");
                                                break;
                                        }
                                        close(sockfd);
                                        break;
                                }else if(ret == 0){
                                        close(sockfd);
                                }else{
                                        printf("get %d bytes of content:%s\n", ret, buf);
                                }
                        }
                }else{

                        printf("something else happened \n");
                }
        }
}

int start_ser(char *ipaddr, char *port)
{
        int sock = socket(AF_INET, SOCK_STREAM, 0);

        struct sockaddr_in serveraddr;
        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(port));
        inet_pton(AF_INET, ipaddr, &serveraddr.sin_addr);

        bind(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

        listen(sock, 128);

        return sock;
}

int main(int argc, char *argv[])
{
        int listenfd = start_ser(argv[1], argv[2]);

        struct epoll_event events[MAX_EVENT_NUM];
        int epollfd = epoll_create(5);
        addfd(epollfd, listenfd, true);
        while(1){
                int ret = epoll_wait(epollfd, events, MAX_EVENT_NUM, -1);
                if(ret < 0){
                        printf("epoll failure\n");
                        break;
                }

                lt(events, ret, epollfd, listenfd);//lt模式
                //et(events, ret, epollfd, listenfd);//et模式
        }
        close(listenfd);
        return 0;
}

2、redis事件处理机制介绍

2.1 socket可读、写条件

通俗的讲,socket接收缓冲区有数据,就可读;socket发送缓冲区数据未溢出,就可写。

1、可读条件

  • socket的接收缓冲区中的【已用】数据字节大于等于该socket的接收缓冲区低水位标记的当前大小。对这样的socket的读操作将不阻塞并返回一个大于0的值(也就是返回准备好读入的数据)。我们可以用SO_RCVLOWAT socket选项来设置该socket的低水位标记。对于TCP和UDP .socket而言,其缺省值为1.
  • 该连接的读这一半关闭(也就是接收了FIN的TCP连接)。对这样的socket的读操作将不阻塞并返回0
  • socket是一个用于监听的socket,并且已经完成的连接数为非0.这样的soocket处于可读状态,是因为socket收到了对方的connect请求,执行了三次握手的第一步:对方发送SYN请求过来,使监听socket处于可读状态;正常情况下,这样的socket上的accept操作不会阻塞;
  • 有一个socket有异常错误条件待处理.对于这样的socket的读操作将不会阻塞,并且返回一个错误(-1),errno则设置成明确的错误条件.这些待处理的错误也可通过指定socket选项SO_ERROR调用getsockopt来取得并清除;

2、可写条件

  • socket的发送缓冲区中的【剩余】数据字节大于等于该socket的发送缓冲区低水位标记的当前大小。对这样的socket的写操作将不阻塞并返回一个大于0的值(也就是返回准备好写入的数据)。我们可以用SO_SNDLOWAT socket选项来设置该socket的低水位标记。对于TCP和UDP socket而言,其缺省值为2048
  • 该连接的写这一半关闭。对这样的socket的写操作将产生SIGPIPE信号,该信号的缺省行为是终止进程。
  • 有一个socket异常错误条件待处理.对于这样的socket的写操作将不会阻塞并且返回一个错误(-1),errno则设置成明确的错误条件.这些待处理的错误也可以通过指定socket选项SO_ERROR调用getsockopt函数来取得并清除;
2.2 事件处理

redis服务器是典型的事件驱动程序,而事件又分为文件事件(socket的可读可写事件)与时间事件(定时任务)两大类。无论是文件事件还是时间事件都封装在结构体aeEventLoop 中:

typedef struct aeEventLoop {
int stop ;
aeFileEvent *events;aeFiredEvent * fired;
aeTimeEvent *timeEventHead;
void *apidata
aeBeforesleepProc * beforesleep;
aeBeforesleepProc *aftersleep ; 
}aeEventLoop;

redis 并没有直接使用epoll提供的API,而是同时支持4种I/O多路复用模型,并将这些模型的API进一步统一封装,由文件ae_evport.c、ae_epoll.c、ae_kqueue.c和 ae_select.c实现。

而Redis在编译阶段,会检查操作系统支持的I/O多路复用模型,并按照一定规则决定使用哪种模型。

以epoll为例,aeApiCreate函数是对epoll_create的封装;aeApiAddEvent函数用于添加事件,是对epoll_ctl 的封装;aeApiDelEvent函数用于删除事件,是对epoll_ctl的封装;aeApiPoll是对epoll_wait的封装。

2.3 文件事件
  • redis服务器启动时需要创建socket并监听,等待客户端连接;
  • 客户端与服务器建立socket连接之后,服务器会等待客户端的命令请求;
  • 服务器处理完客户端的命令请求之后,命令回复会暂时缓存在client结构体的 buf缓冲区,待客户端文件描述符的可写事件发生时,才会真正往客户端发送命令回复。这些都需要创建对应的文件事件:
aeCreateFileEvent (server.el,server.ipfd[j],AE_READABLE,
acceptTcpHandler , NULL) ;
aeCreateFileEvent (server.el , fd,AE__READABLE,
readQueryFromClient , c);
aeCreateFileEvent (server.el, c->fd, ae_flags,
sendReplyToclient , c);

当socket可读可写时,通过回调函数处理对应流程。

  • 在redis初始化时,就对监听端口创建了可读事件,回调函数为acceptTcpHandler,当客户端连接redis服务器,触发可读事件,执行回调函数,该流程主要创建对应client客户端对象,并为该连接创建可读事件。
  • 客户端连接到redis之后,发送数据,触发可读事件,调函数为readQueryFromClient,该流程主要接收数据,处理逻辑,并创建一个可写事件,用于回复客户端
  • 当前发送缓冲区为空,触发可写事件,回调函数为sendReplyToClient,向客户端回复数据
2.4. 时间事件

时间事件主要处理定时函数

  • redis轮询时,会优先处理即将到来的时间事件
  • 时间事件还未到,查找最早会发生的时间事件,根据该时间差阻塞文件事件。即文件事件未能有可读、可写处理时,到时会退出阻塞,下一个轮询时,去处理即将到来的时间事件

3、epoll多路复用-多人聊天终端实战

1、代码结构

github地址 基于epoll的事件处理实现多人聊天终端
关注myserver.c 、networking.c、myclient.c这几个就行

(1)ae_epoll.c #redis API,对epoll的封装
(2)ae.c/ae.h  #redis API,对事件处理封装,包含时间/文件事件的创建、添加、删除
(3)anet.c/anet.h  #redis API,对socket底层bind、accept、listen函数的封装
(4)myserver.c     #入口
(5)networking.c    #自己编写的处理逻辑
(6)zmalloc.c/zmalloc.h  #redis API,对内存malloc等的封装
(7)Makefile #用于编译
(8)ip.sh #自己写的脚本,用于方便替换myclient.c中char ip[20]

2、运行
make之后,再执行ip.sh,即可出现myservermyclien可执行文件,直接启动可执行文件就完成了。

3、功能讲解
(1)群发。默认情况下,每一个客户端连接上,都会有一个唯一name标识。当客户端发送数据时,当前所有连接上的客户端都会接收到。
(2)私聊。可以通过setname liliya,先将客户端name标识更新为liliya。其他用户通过pchat liliya Are you ok,即可将消息私发消息给liliya

4、并发量测试

服务端运行在虚拟机上,利用windwos的jmeter测试

当QPS为2000时,性能完全ok,在2ms内便能完成响应
当QPS达到1w时,也能处理,但性能会急速下降,平均需要2.5s才能完成响应

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值