目录
前言
一、打开监听端口,等待客户端的命令请求
二、监听描述符进行监听
三. 监听描述符回调函数
总结
通过对redis服务采用的基于epoll反应堆模型的server/client模型进行介绍。
服务器通过调用listenToPort函数,根据指定的端口port,以及Redis服务器配置中的bindaddr[REDIS_BINDADDR_MAX]数组指定的地址来构建一组监听文件描述符,并将其保存在整数数组'fds'中,它们的编号设置为'*count'。
如果服务器配置不包含需要绑定的特定地址,即服务器配置中地址数量bindaddr_count==0,则绑定通配地址。
注意:将绑定服务器套接字结构的监听文件描述符设置为非阻塞。
int listenToPort(int port, int *fds, int *count) {
int j;
if (server.bindaddr_count == 0) server.bindaddr[0] = NULL;
for (j = 0; j < server.bindaddr_count || j == 0; j++) {
if (server.bindaddr[j] == NULL) {
//如果没有给定对应的主机IP地址,则根据地址0.0.0.0,端口号port创建一个服务器套接字结构
//创建一个监听描述符,并绑定服务器套接字结构,设置最大允许连接客户端数为server.tcp_backlog
//并设置监听描述符为非阻塞,然后将该监听文件描述符存入fds数组
//IPv6
fds[*count] = anetTcp6Server(server.neterr,port,NULL,
server.tcp_backlog);
if (fds[*count] != ANET_ERR) {
anetNonBlock(NULL,fds[*count]);
(*count)++;
}
//IPv4
fds[*count] = anetTcpServer(server.neterr,port,NULL,
server.tcp_backlog);
if (fds[*count] != ANET_ERR) { //显得多余?????????或者将最后的那两行加到正常IPv6和IPv4中括号中去??????????????
anetNonBlock(NULL,fds[*count]);
(*count)++;
}
//当一个监听描述符否没有成功获得,则退出循环
if (*count) break;
//如果给定对应的主机IP地址server.bindaddr[j],则根据地址server.bindaddr[j],端口号port创建一个服务器套接字结构
//创建一个监听描述符,并绑定服务器套接字结构,设置最大允许连接客户端数为server.tcp_backlog
//并设置监听描述符为非阻塞,然后将该监听文件描述符存入fds数组
//IPv6
} else if (strchr(server.bindaddr[j],':')) {
/* Bind IPv6 address. */
fds[*count] = anetTcp6Server(server.neterr,port,server.bindaddr[j],
server.tcp_backlog);
//IPv4
} else {
/* Bind IPv4 address. */
fds[*count] = anetTcpServer(server.neterr,port,server.bindaddr[j],
server.tcp_backlog);
}
//一个监听描述符都没有绑定成功,则直接报告错误
if (fds[*count] == ANET_ERR) {
redisLog(REDIS_WARNING,
"Creating Server TCP listening socket %s:%d: %s",
server.bindaddr[j] ? server.bindaddr[j] : "*",
port, server.neterr);
return REDIS_ERR;
}
//绑定主机套接字的监听描述符设置为非阻塞
anetNonBlock(NULL,fds[*count]);//该两行是否要移动?????
(*count)++;
}
return REDIS_OK;
}
函数名:static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog)
返回值:成功,返回监听描述符,失败返回ANET_ERR。
static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog)
{
int s, rv;
char _port[6];
struct addrinfo hints, *servinfo, *p;
snprintf(_port,6,"%d",port);
memset(&hints,0,sizeof(hints));
hints.ai_family = af;//套接字协议类型AF_INET6/AF_INET
hints.ai_socktype = SOCK_STREAM;//TCP协议
hints.ai_flags = AI_PASSIVE; //如果 bindarry==NULL,返回的就是通配地址
//getaddrinfo解决了把主机名和服务名转换成套接口地址结构的问题。
//返回值:0——成功,非0——出错
//servinfo是一个存储结果的 struct addrinfo 结构体列表
if ((rv = getaddrinfo(bindaddr,_port,&hints,&servinfo)) != 0) {
anetSetError(err, "%s", gai_strerror(rv));
return ANET_ERR;
}
//根据套接口地址结构列表servinfo,生成套接字s
for (p = servinfo; p != NULL; p = p->ai_next) {
if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1)
continue;
if (af == AF_INET6 && anetV6Only(err,s) == ANET_ERR) goto error;
if (anetSetReuseAddr(err,s) == ANET_ERR) goto error;
//绑定并设置该套接字s最大允许的客户端待连接数
if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog) == ANET_ERR) goto error;
goto end;
}
if (p == NULL) {
anetSetError(err, "unable to bind socket");
goto error;
}
error:
s = ANET_ERR;
end:
freeaddrinfo(servinfo);
return s;
}
通过调用getaddrinfo函数,根据给定的端口号port以及bindaddr地址生成一个包含套接字相关信息的struct addrinfo结构。getaddrinfo函数详解参考:http://t.csdn.cn/03sit
struct addrinfo {
int ai_flags; //输入模式标志【主机名("www.baidu.com")、数字化的地址字符串(IPv4的点分十进制串("192.168.1.100")、IPv6的16进制串("2000::1:2345:6789:abcd")】
int ai_family; //套接字的协议族,AF_INET / AF_INET6 / AF_UNIX。
int ai_socktype; //套接字类型,SOCK_STREAM。
int ai_protocol; //套接字的协议,0表示默认协议。
socklen_t ai_addrlen; //套接字地址的长度。
struct sockaddr *ai_addr; //套接字地址。
char *ai_canonname; //服务位置的规范名称。
struct addrinfo *ai_next; //指向下一个列表的指针。
};
根据addrinfo结构中的套接字协议族,套接字类型以及套接字协议,使用socket函数创建一个监听描述符s,并使用anetListen函数,为监听描述符s绑定绑定服务器套接字,并设置最大允许连接客户端数。
static int anetListen(char *err, int s, struct sockaddr *sa, socklen_t len, int backlog) {
if (bind(s,sa,len) == -1) {
anetSetError(err, "bind: %s", strerror(errno));
close(s);
return ANET_ERR;
}
if (listen(s, backlog) == -1) {
anetSetError(err, "listen: %s", strerror(errno));
close(s);
return ANET_ERR;
}
return ANET_OK;
}
1.服务器遍历多个监听描述符lfd,将监听描述符加入epoll句柄中监听读事件。
2.将监听描述符lfd构造成已注册文件事件结构,并加入事件处理器状态aeEventLoop中的已注册文件事件event数组中,设置其回调函数为acceptTcpHandler。
// 将监听的文件描述符绑定文件事件结构后,配置acceptTcpHandler函数为回调函数后加入epoll句柄
// 用于接受并应答客户端的 connect() 调用
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
{
redisPanic(
"Unrecoverable error creating server.ipfd file event.");
}
}
调用 aeCreateFileEvent函数根据监听描述符fd在事件服务器eventLoop的已注册文件事件数组中索引下标为fd的元素,并根据给定的读事件掩码mask、读事件回调函数proc去初始化对应下标为fd的aeFileEvent已注册文件事件结构。
并且将监听描述符fd以及读事件掩码mask组成的epoll_event事件结构加入到epoll红黑树句柄中。
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
//文件描述符超出槽大小,返回错误,并设置errno
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
if (fd >= eventLoop->setsize) return AE_ERR;
// 取出已注册文件事件数组eventLoop->events中文件描述fd对应下标的元素指针
aeFileEvent *fe = &eventLoop->events[fd];
// 监听指定 fd 的指定事件
//将文件描述符fd以及其对应的事件结构加入到epoll句柄中
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
// 设置fd监听的事件类型,以及事件的处理器的回调函数
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
// 私有数据,将包含fd文件描述符详细信息放在fd事件结构的私有数据中
fe->clientData = clientData;
// 如果有需要,更新事件处理器的最大 fd
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}
循环max次,使用accept函数进行非阻塞等待客户端连接请求。当客户端向服务器发出连接请求,监听描述符对应的读事件满足监听条件时,服务器将调用acceptTcpHandler函数,使得服务器与客户端建立连接,获得已连接描述符cfd。并根据已连接描述符为客户端创建客户端状态。
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[REDIS_IP_STR_LEN];
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);
REDIS_NOTUSED(privdata);
while(max--) {
// accept 客户端连接
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK)
redisLog(REDIS_WARNING,
"Accepting client connection: %s", server.neterr);
return;
}
redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
// 为客户端创建客户端状态(redisClient)
acceptCommonHandler(cfd,0);
}
}
其中anetTcpAccept函数主要用于与客户端建立连接,并生成已连接文件描述符cfd。代码如下:
int anetTcpAccept(char *err, int s, char *ip, size_t ip_len, int *port) {
int fd;
struct sockaddr_storage sa;
socklen_t salen = sizeof(sa);
if ((fd = anetGenericAccept(err,s,(struct sockaddr*)&sa,&salen)) == -1)
return ANET_ERR;
if (sa.ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)&sa;
if (ip) inet_ntop(AF_INET,(void*)&(s->sin_addr),ip,ip_len);
if (port) *port = ntohs(s->sin_port);
} else {
struct sockaddr_in6 *s = (struct sockaddr_in6 *)&sa;
if (ip) inet_ntop(AF_INET6,(void*)&(s->sin6_addr),ip,ip_len);
if (port) *port = ntohs(s->sin6_port);
}
return fd;
}
static int anetGenericAccept(char *err, int s, struct sockaddr *sa, socklen_t *len) {
int fd;
while(1) {
fd = accept(s,sa,len);
if (fd == -1) {
if (errno == EINTR)
continue;
else {
anetSetError(err, "accept: %s", strerror(errno));
return ANET_ERR;
}
}
break;
}
return fd;
}
使用acceptCommonHandler(cfd,0);调用createClient为已连接描述符为cfd的客户端创建客户端状态,代码如下:
static void acceptCommonHandler(int fd, int flags) {
// 创建客户端
redisClient *c;
if ((c = createClient(fd)) == NULL) {
redisLog(REDIS_WARNING,
"Error registering fd event for the new client: %s (fd=%d)",
strerror(errno),fd);
close(fd); /* May be already closed, just ignore errors */
return;
}
// 如果新添加的客户端令服务器的最大客户端数量达到了
// 那么向新客户端写入错误信息,并关闭新客户端
// 先创建客户端,再进行数量检查是为了方便地进行错误信息写入
if (listLength(server.clients) > server.maxclients) {
char *err = "-ERR max number of clients reached\r\n";
if (write(c->fd,err,strlen(err)) == -1) {
/* 不进行任何操作,只是为了避免警告 */
}
// 更新拒绝连接数
server.stat_rejected_conn++;
//释放新客户端
freeClient(c);
return;
}
// 更新连接次数
server.stat_numconnections++;
// 设置 FLAG
c->flags |= flags;
}
调用createClient()函数,根据已连接文件描述符创建一个客户端:
(1)将已连接文件描述符cfd设置为非阻塞,并禁用Nagle算法,设置Keep alive活点检测。
(2)将已连接文件描述符cfd加入epoll句柄中监听读事件。
(3)将已连接文件描述符cfd构造成已注册文件事件结构,并加入事件处理器状态aeEventLoop中的已注册文件事件event数组中,设置其回调函数为readQueryFromClient,文件事件私有数据为客户端C。
redisClient *createClient(int fd) {
// 分配空间
redisClient *c = zmalloc(sizeof(redisClient));
// 当 fd 不为 -1 时,创建带网络连接的客户端
// 如果 fd 为 -1 ,那么创建无网络连接的伪客户端
// 因为 Redis 的命令必须在客户端的上下文中使用,所以在执行 Lua 环境中的命令时
// 需要用到这种伪终端
if (fd != -1) {
// 设置文件描述符fd为非阻塞
anetNonBlock(NULL,fd);
// 禁用 Nagle 算法,Nagle算法能自动连接许多的小缓冲器消息,
// 但是服务器和客户端的对及时通信性有很高的要求,
// 因此禁止使用 Nagle 算法,客户端向内核递交的每个数据包都会立即发送给服务器。
anetEnableTcpNoDelay(NULL,fd);
// 设置 keep alive,活点检测
if (server.tcpkeepalive)
anetKeepAlive(NULL,fd,server.tcpkeepalive);
//创建一个文件事件el,且监听读事件,开始开始接收命令请求
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
}
// 初始化各个属性
// 默认选0号数据库
selectDb(c,0);
// client的套接字
c->fd = fd;
// client的名字
c->name = NULL;
// 回复缓冲区的偏移量
c->bufpos = 0;
// 输入缓存区
c->querybuf = sdsempty();
// 输入缓存区峰值
c->querybuf_peak = 0;
// 请求协议类型,内联或者多条命令,初始化为0
c->reqtype = 0;
// 命令参数数量
c->argc = 0;
// 命令参数
c->argv = NULL;
// 当前执行的命令和最近一次执行的命令
c->cmd = c->lastcmd = NULL;
// 查询缓冲区中未读入的命令内容数量
c->multibulklen = 0;
// 读入的参数的长度
c->bulklen = -1;
// 已发送字节数
c->sentlen = 0;
// client的状态 FLAG
c->flags = 0;
// 设置创建client的时间和最后一次互动的时间
c->ctime = c->lastinteraction = server.unixtime;
// 认证状态
c->authenticated = 0;
// replication复制的状态,初始为无
c->replstate = REDIS_REPL_NONE;
// replication复制的偏移量
c->reploff = 0;
// 通过 ACK 命令接收到的偏移量
c->repl_ack_off = 0;
// 通过 AKC 命令接收到偏移量的时间
c->repl_ack_time = 0;
// 客户端为从服务器时使用,记录了从服务器所使用的端口号
c->slave_listening_port = 0;
// 回复链表
c->reply = listCreate();
// 回复链表的字节量
c->reply_bytes = 0;
// 回复缓冲区大小达到软限制的时间
c->obuf_soft_limit_reached_time = 0;
// 回复链表的释放和复制函数
listSetFreeMethod(c->reply,decrRefCountVoid);
listSetDupMethod(c->reply,dupClientReplyValue);
// 阻塞类型
c->btype = REDIS_BLOCKED_NONE;
// 阻塞超时
c->bpop.timeout = 0;
// 造成客户端阻塞的列表键
c->bpop.keys = dictCreate(&setDictType,NULL);
// 在解除阻塞时将元素推入到 target 指定的键中
// BRPOPLPUSH 命令时使用
c->bpop.target = NULL;
// 阻塞状态
c->bpop.numreplicas = 0;
// 要达到的复制偏移量
c->bpop.reploffset = 0;
// 全局的复制偏移量
c->woff = 0;
// 进行事务时监视的键
c->watched_keys = listCreate();
// 订阅的频道和模式
c->pubsub_channels = dictCreate(&setDictType,NULL);
// 订阅模式
c->pubsub_patterns = listCreate();
// 被缓存的peerid,peerid就是 ip:port
c->peerid = NULL;
// 订阅发布模式的释放和比较方法
listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid);
listSetMatchMethod(c->pubsub_patterns,listMatchObjects);
// 将真正的client放在服务器的客户端链表中
if (fd != -1) listAddNodeTail(server.clients,c);
// 初始化客户端的事务状态
initClientMultiState(c);
// 返回客户端
return c;
}
监听客户端已连接描述符读事件满足监听条件后的回调函数readQueryFromClient:
(1)设置已连接描述符cfd对应的客户端为当前客户端,设置命令的读入长度。
(2)从文件描述符cfd中读入命令到客户端的输入缓冲区c->querybuf中,使用processInputBuffer函数处理客户端输入的命令内容。
(3)processInputBuffer函数调用processCommand函数执行命令,执行成功后,重置客户端resetClient(c)。
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
redisClient *c = (redisClient*) privdata;
int nread, readlen;
size_t qblen;
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);
// 设置服务器的当前客户端
server.current_client = c;
// 设置命令读入长度(默认为 16 MB)
readlen = REDIS_IOBUF_LEN;
/* If this is a multi bulk request, and we are processing a bulk reply
* that is large enough, try to maximize the probability that the query
* buffer contains exactly the SDS string representing the object, even
* at the risk of requiring more read(2) calls. This way the function
* processMultiBulkBuffer() can avoid copying buffers to create the
* Redis Object representing the argument. */
//如果是批量命令请求,根据命令请求的长度,设置读入的长度readlen
if (c->reqtype == REDIS_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
&& c->bulklen >= REDIS_MBULK_BIG_ARG)
{
//这句什么什么意思????
int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);
if (remaining < readlen) readlen = remaining;
}
// 获取查询缓冲区当前内容的长度
// 如果读取出现 short read ,那么可能会有内容滞留在读取缓冲区里面
// 这些滞留内容也许不能完整构成一个符合协议的命令,
qblen = sdslen(c->querybuf);
// 如果有需要,更新缓冲区内容长度的峰值(peak)
if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
// 为查询缓冲区分配空间,确保至少会有readlen+1的空闲空间
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
// 读入命令内容到查询缓存
nread = read(fd, c->querybuf+qblen, readlen);
// 读入出错
if (nread == -1) {
if (errno == EAGAIN) {
nread = 0;
} else {
redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
//读入出错,释放客户端
freeClient(c);
return;
}
// 遇到 EOF,读取结束,释放客户端
} else if (nread == 0) {
redisLog(REDIS_VERBOSE, "Client closed connection");
freeClient(c);
return;
}
if (nread) {
// 根据内容,更新查询缓冲区(SDS) free 和 len 属性
// 并将 '\0' 正确地放到内容的最后
sdsIncrLen(c->querybuf,nread);
// 记录服务器和客户端最后一次互动的时间
c->lastinteraction = server.unixtime;
// 如果客户端是 master 的话,更新它的复制偏移量
if (c->flags & REDIS_MASTER) c->reploff += nread;
} else {
// 在 nread == -1 且 errno == EAGAIN 时运行
server.current_client = NULL;
return;
}
// 查询缓冲区长度超出服务器最大缓冲区长度,将客户端的信息和输入缓冲区中的信息都写进日志
// 清空缓冲区并释放客户端
if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
//将客户端所有的信息都存在ci中
sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();
//将输入缓冲区中的内容以带引号的形式保存在bytes中
bytes = sdscatrepr(bytes,c->querybuf,64);
//将客户端的信息和输入缓冲区中的信息都写进日志
redisLog(REDIS_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
//释放用于保存客户端信息,以及输入缓冲区内容的临时字符串
sdsfree(ci);
sdsfree(bytes);
//释放客户端
freeClient(c);
return;
}
// 从查询缓存重读取内容,创建参数,并执行命令
// 函数会执行到缓存中的所有内容都被处理完为止
processInputBuffer(c);
server.current_client = NULL;
}
processInputBuffer函数处理客户端输入的命令内容,将其解析后调用processCommand函数执行命令。
// 处理客户端输入的命令内容,将其解析后执行命令
// 判断请求的类型
// 简单来说,多条查询是一般客户端发送来的,
// 而内联查询则是 TELNET 发送来的
void processInputBuffer(redisClient *c) {
// 尽可能地处理查询缓冲区中的内容
// 如果读取出现 short read ,那么可能会有内容滞留在读取缓冲区里面
// 这些滞留内容也许不能完整构成一个符合协议的命令,
// 需要等待下次读事件的就绪
while(sdslen(c->querybuf)) {
// 如果客户端不是是一个从服务器,且目前服务器存在正处于暂停状态的客户端,那么直接返回
if (!(c->flags & REDIS_SLAVE) && clientsArePaused()) return;
// REDIS_BLOCKED 状态表示客户端正在被阻塞
if (c->flags & REDIS_BLOCKED) return;
// 表示有用户对这个客户端执行了CLIENT KILL命令,
// 或者客户端发送给服务器的命令请求中包含了错误的协议内容,没有必要处理命令了
if (c->flags & REDIS_CLOSE_AFTER_REPLY) return;
// 如果是未知的请求类型,则判定请求类型
if (!c->reqtype) {
// 如果是"*"开头,则是多条请求,是client发来的
if (c->querybuf[0] == '*') {
// 多条查询
c->reqtype = REDIS_REQ_MULTIBULK;
// 否则就是内联请求,是Telnet发来的
} else {
// 内联查询
c->reqtype = REDIS_REQ_INLINE;
}
}
// 根据请求命令的类型,将缓冲区中的内容转换成命令,以及命令参数
// 如果是内联请求
if (c->reqtype == REDIS_REQ_INLINE) {
// 处理Telnet发来的内联命令,并创建成对象,保存在client的参数列表中
if (processInlineBuffer(c) != REDIS_OK) break;
// 如果是多条请求
} else if (c->reqtype == REDIS_REQ_MULTIBULK) {
// 将client的querybuf中的协议内容转换为client的参数列表中的对象
if (processMultibulkBuffer(c) != REDIS_OK) break;
} else {
redisPanic("Unknown request type");
}
//多条查询可以看到参数长度<=0的情况
//当参数个数为0,直接重置客户端,无需执行命令
if (c->argc == 0) {
resetClient(c);
} else {
// 执行命令,并重置客户端
if (processCommand(c) == REDIS_OK)
resetClient(c);
}
}
}
具体请求命令的存储,命令的执行,以及命令回复给客户端,在下一篇文章中详细介绍。
。。。。。。。。。。。