背景
redis是一个高性能的KV数据库,在工作中经常用到,可被用作缓存、分布式锁等,作为被高频使用的组件,了解其实现对工作有很大帮助(包括面试)。为此,在对redis有一定的使用、了解之后,希望通过阅读其源码,理解redis实现过程,由简单到复杂地分析其源码,并尝试对源码从零到一地复现,以此来加深对redis的理解。
由于时间原因,无法一次性将所有的代码在短时间内理解,所以在学习过程中,尝试在每个阶段制定一个简单的目标,然后根据源码去实现。
redis版本:5.0.13,redis各版本源码下载链接。
目标
此次学习目标为:实现redis server,接收redis client请求。
原理
redis client与server之间基于tcp通信,且使用IO复用的技术。
实现细节
redisServer
redis定义了一个数据结构:redisServer,用来管理server相关的信息。其定义如下:
cpp
struct redisServer {
aeEventLoop *el;
/* 网络 */
int port; /* TCP监听端口 */
int tcp_backlog; /* TCP listen() backlog */
int ipfd[CONFIG_BINDADDR_MAX]; /* TCP socket文件描述符 */
int ipfd_count; /* 正在使用的ipfd的索引 */
char neterr[ANET_ERR_LEN]; /* anet.c中错误信息的buffer */
list *clients; /* 活跃客户端 */
/* 限制 */
unsigned int maxclients; /* 最多能同时存在的最大clients数 */
/* 配置 */
int verbosity; /* 日志等级, redis.conf中配置 */
int tcpkeepalive; /* 如果非0,则设置SO_KEEPALIVE */
};
本次实现只考虑redisServer能接收client的请求,只将一些必要的数据列出,实际的数据远不止这些。
整体流程
先来看下整体流程:
主流程包括3步:
- 初始化配置initServerConfig:初始化redsiServer中的各数据默认值
- 初始化服务initServer:创建时间处理对象,监听端口,设置收到client连接时的回调函数
- 进入事件处理主函数aeMain:调用epoll,监听待处理的事件,当有事件就绪时处理事件
初始化默认值,就是对服务运行需要的一些参数设置默认值,比较简单,此处不做过多介绍,具体细节可参考代码。下面主要说一下initServer和aeMain是如何使用IO复用的技术来处理网络请求。
initServer
initServer主要流程如下,标红部分为IO多路复用关键代码:
initServer主要包含以下步骤:
- 初始化clients:clients实际是一个列表,这里初始化仅仅是创建了一个空列表。
- aeCreateEventLoop:初始化事件处理相关的数据,调用epoll_create来创建IO复用相关的文件描述符。
- listenToPort:创建socket,监听对应的端口(默认6379,在initServerConfig时赋值),同时将其设置为非阻塞IO(O_NONBLOCK)。
- aeCreateFileEvent:调用epoll_ctl,将socket文件描述符可读事件注册到epoll中。同时,将文件可读时的处理函数设置为acceptTcpHandler。
其实,InitServer中的关键代码涉及的知识点是socket编程、IO多路复用。如果对IO多路复用不了解,建议学习这本书:《Linux高性能服务器编程》。
aeMain
aeMain为一个while(true)的循环(实际是while(!server.el->stop),在没有收到停止的信号是,即为while(true)),循环里面调用aeProcessEvents来处理,其主要步骤有:
- aeApiPoll:调用epoll_wait,获取注册到epfd中文件描述符的就绪事件,并设置事件的类型
- rfileProc:如果为读事件,则调用该函数
- wfileProc:如果为写事件,则调用该函数
初始状态下,epfd中仅监听了server.ipfd,即server socket的可读事件,该事件是在initServer时注册,rfileProc被设置为acceptTcpHandler。以下是acceptTcpHandler的主要流程:
主要步骤为:
- anetTcpAccept:调用accept,获取client的ip和端口,以及client对应的socket(cfd)
- acceptCommonHandler:创建一个client对象,用于管理对应的client状态和数据,创建client对象的主要步骤:
- anetNonBlock:将client的连接设置为非阻塞连接。
- anetEnableTcpNoDelay:调用setsockopt,设置TCP_NODELAY。该标志具体含义可自行查阅,个人理解为:redis为了能尽快响应每个请求,在处理完成之后,需要立即返回client数据,舍弃了整体网络的最优化,因为如果TCP每次传输的数据过少,可能会导致网络传输数据的利用率低,如果未设置TCP_NODELAY,则server端处理完成之后,可能不能立即返回client数据,需要积攒一波数据之后一次发出,这可能导致单个请求耗时增加。
- anetKeepAlive:通过setsockopot,设置tcp keepalive相关的参数,具体含义自行查阅,代码中也有相应的注释。
- aeCreateFileEvent:将client对应的文件描述符cfd可读事件注册到epfd中,并设置可读事件处理函数为readQueryFromClient。
- linkClient:将client链接到server.clients这个链表的尾部。
当有client请求与server建立连接时,acceptTcpHandler被执行,同时这个连接通过struct client进行管理,与client关联的socket(cfd)可读事件被注册到epfd中,如果client发送数据到server,在aeMain中,epoll_wait会返回cfd可读,在aeApiPoll中将该事件记录在server.fired中,然后调用cfd对应的rfileProc,即:readQueryFromClient。
本次学习,仅考虑接收到client发送的数据,并不处理,所以readQueryFromClient,本次实现时,只打印接收到的client数据。
数据结构
这里对上述的代码中,总结所有用到的数据结构。对每一个数据结构,仅列出与本文相关的部分,便于理解。
redisServer
cpp
struct redisServer {
aeEventLoop *el;
/* 网络 */
int port; /* TCP监听端口 */
int tcp_backlog; /* TCP listen() backlog */
int ipfd[CONFIG_BINDADDR_MAX]; /* TCP socket文件描述符 */
int ipfd_count; /* 正在使用的ipfd的索引 */
char neterr[ANET_ERR_LEN]; /* anet.c中错误信息的buffer */
list *clients; /* 活跃客户端 */
/* 限制 */
unsigned int maxclients; /* 最多能同时存在的最大clients数 */
/* 配置 */
int verbosity; /* 日志等级, redis.conf中配置 */
int tcpkeepalive; /* 如果非0,则设置SO_KEEPALIVE */
};
aeEventLoop
cpp
typedef struct aeEventLoop {
int maxfd; /* 当前注册的文件描述符中的最大的 */
int setsize; /* 最多能管理的文件描述符数量 */
aeFileEvent *events; /* 注册的事件 */
aeFiredEvent *fired; /* 激活的事件 */
int stop;
void *apidata; /* polling API需要使用的数据 */
} aeEventLoop;
aeFileEvent
cpp
typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);
typedef struct aeFileEvent {
int mask; /* 可选值: AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
aeFiredEvent
cpp
/* 激活的事件 */
typedef struct aeFiredEvent {
int fd;
int mask;
} aeFiredEvent;
client
cpp
typedef struct client {
int fd; /* client socket */
listNode *client_list_node; /* client在server clients列表中的哪个节点 */
} client;
list
cpp
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
typedef struct list {
listNode *head;
listNode *tail;
void (*free)(void *ptr);
unsigned long len;
} list;
aeApiState
cpp
typedef struct aeApiState {
int epfd;
struct epoll_event *events;
} aeApiState;
总览
代码结构
效果
client端:
server端:
client端通过redis源码编译得到,server端是本次实现的redis代码。可以看到,server端可以收到client端传过来的数据,具体的数据含义待后续学习去理解。
附录
本次功能实现的代码链接。