Redis 网络连接库剖析
1. Redis网络连接库介绍
Redis网络连接库对应的文件是networking.c
。这个文件主要负责
- 客户端的创建与释放
- 命令接收与命令回复
- Redis通信协议分析
- CLIENT 命令的实现
我们接下来就这几块内容分别列出源码,进行剖析。
2. 客户端的创建与释放
2.1客户端的创建
Redis 服务器是一个同时与多个客户端建立连接的程序。当客户端连接上服务器时,服务器会建立一个server.h/client
结构来保存客户端的状态信息。所以在客户端创建时,就会初始化这样一个结构,客户端的创建源码如下:
client *createClient(int fd) {
client *c = zmalloc(sizeof(client)); //分配空间
// 如果fd为-1,表示创建的是一个无网络连接的伪客户端,用于执行lua脚本的时候。
// 如果fd不等于-1,表示创建一个有网络连接的客户端
if (fd != -1) {
// 设置fd为非阻塞模式
anetNonBlock(NULL,fd);
// 禁止使用 Nagle 算法,client向内核递交的每个数据包都会立即发送给server出去,TCP_NODELAY
anetEnableTcpNoDelay(NULL,fd);
// 如果开启了tcpkeepalive,则设置 SO_KEEPALIVE
if (server.tcpkeepalive)
// 设置tcp连接的keep alive选项
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的ID
c->id = server.next_client_id++;
// 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的状态
c->flags = 0;
// 设置创建client的时间和最后一次互动的时间
c->ctime = c->lastinteraction = server.unixtime;
// 认证状态
c->authenticated = 0;
// replication复制的状态,初始为无
c->replstate = REPL_STATE_NONE;
// 设置从节点的写处理器为ack,是否在slave向master发送ack
c->repl_put_online_on_ack = 0;
// replication复制的偏移量
c->reploff = 0;
// 通过ack命令接收到的偏移量
c->repl_ack_off = 0;
// 通过ack命令接收到的偏移量所用的时间
c->repl_ack_time = 0;
// 从节点的端口号
c->slave_listening_port = 0;
// 从节点IP地址
c->slave_ip[0] = '\0';
// 从节点的功能
c->slave_capa = SLAVE_CAPA_NONE;
// 回复链表
c->reply = listCreate();
// 回复链表的字节数
c->reply_bytes = 0;
// 回复缓冲区的内存大小软限制
c->obuf_soft_limit_reached_time = 0;
// 回复链表的释放和复制方法
listSetFreeMethod(c->reply,decrRefCountVoid);
listSetDupMethod(c->reply,dupClientReplyValue);
// 阻塞类型
c->btype = BLOCKED_NONE;
// 阻塞超过时间
c->bpop.timeout = 0;
// 造成阻塞的键字典
c->bpop.keys = dictCreate(&setDictType,NULL);
// 存储解除阻塞的键,用于保存PUSH入元素的键,也就是dstkey
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);
// 初始化client的事物状态
initClientMultiState(c);
return c;
}
根据传入的文件描述符fd
,可以创建用于不同情景下的client
。这个fd
就是服务器接收客户端connect
后所返回的文件描述符。
- fd == -1。表示创建一个无网络连接的客户端。主要用于执行 lua 脚本时。
- fd != -1。表示接收到一个正常的客户端连接,则会创建一个有网络连接的客户端,也就是创建一个文件事件,来监听这个
fd
是否可读,当客户端发送数据,则事件被触发。创建客户端时,还会禁用Nagle
算法。
Nagle
算法能自动连接许多的小缓冲器消息,这一过程(称为nagling)通过减少必须发送包的个数来增加网络软件系统的效率。但是服务器和客户端的对即使通信性有很高的要求,因此禁止使用 Nagle 算法,客户端向内核递交的每个数据包都会立即发送给服务器。
创建客户端的过程,会将server.h/client
结构的所有成员初始化,接下里会介绍部分重点的成员。
- int id:服务器对于每一个连接进来的都会创建一个ID,客户端的ID从1开始。每次重启服务器会刷新。
- int fd:当前客户端状态描述符。分为无网络连接的客户端和有网络连接的客户端。
- int flags:客户端状态的标志。Redis 3.2.8 中在
server.h
中定义了23种状态。 - robj *name:默认创建的客户端是没有名字的,可以通过
CLIENT SETNAME
命令设置名字。后面会介绍该命令的实现。 - int reqtype:请求协议的类型。因为Redis服务器支持
Telnet
的连接,因此Telnet
命令请求协议类型是PROTO_REQ_INLINE
,而redis-cli
命令请求的协议类型是PROTO_REQ_MULTIBULK
。
用于保存服务器接受客户端命令的成员:
- sds querybuf:保存客户端发来命令请求的输入缓冲区。以Redis通信协议的方式保存。
- size_t querybuf_peak:保存输入缓冲区的峰值。
- int argc:命令参数个数。
- robj *argv:命令参数列表。
用于保存服务器给客户端回复的成员:
- char buf[16*1024]:保存执行完命令所得命令回复信息的静态缓冲区,它的大小是固定的,所以主要保存的是一些比较短的回复。分配
client
结构空间时,就会分配一个16K的大小。 - int bufpos:记录静态缓冲区的偏移量,也就是buf数组已经使用的字节数。
- list *reply:保存命令回复的链表。因为静态缓冲区大小固定,主要保存固定长度的命令回复,当处理一些返回大量回复的命令,则会将命令回复以链表的形式连接起来。
- unsigned long long reply_bytes:保存回复链表的字节数。
- size_t sentlen:已发送回复的字节数。
2.2 客户端的释放
客户端的释放freeClient()
函数主要就是释放各种数据结构和清空一些缓冲区等等操作,这里就不列出源码。但是我们关注一下异步释放客户端。源码如下:
// 异步释放client
void freeClientAsync(client *c) {
// 如果是已经即将关闭或者是lua脚本的伪client,则直接返回
if (c->flags & CLIENT_CLOSE_ASAP || c->flags & CLIENT_LUA) return;
c->flags |= CLIENT_CLOSE_ASAP;
// 将client加入到即将关闭的client链表中
listAddNodeTail(server.clients_to_close,c);
}
- server.clients_to_close:是服务器保存所有待关闭的client链表。
设置异步释放客户端的目的主要是:防止底层函数正在向客户端的输出缓冲区写数据的时候,关闭客户端,这样是不安全的。Redis会安排客户端在serverCron()
函数的安全时间释放它。
当然也可以取消异步释放,那么就会调用freeClient()
函数立即释放。源码如下:
// 取消设置异步释放的client
void freeClientsInAsyncFreeQueue(void) {
// 遍历所有即将关闭的client
while (listLength(server.clients_to_close)) {
listNode *ln = listFirst(server.clients_to_close);
client *c = listNodeValue(ln);
// 取消立即关闭的标志
c->flags &= ~CLIENT_CLOSE_ASAP;
freeClient(c);
// 从即将关闭的client链表中删除
listDelNode(server.clients_to_close,ln);
}
}
3. 命令接收与命令回复
3.1 命令接收
当客户端连接上Redis服务器后,服务器会得到一个文件描述符fd
,而且服务器会监听该文件描述符的读事件,这些在createClient()
函数中,我们有分析。那么当客户端发送了命令,触发了AE_READABLE
事件,那么就会调用回调函数readQueryFromClient()
来从文件描述符fd
中读发来的命令,并保存在输入缓冲区中querybuf
。而这个回调函数就是我们在Redis 事件处理实现一文中所提到的指向回调函数的指针rfileProc
和wfileProc
。那么,我们先来分析sendReplyToClient()
函数。
// 读取client的输入缓冲区的内容
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = (client*) privdata;
int nread, readlen;
size_t qblen;
UNUSED(el);
UNUSED(mask);
// 读入的长度,默认16MB
readlen = PROTO_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 == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
&& c->bulklen >= PROTO_MBULK_BIG_ARG)
{
int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);
if (remaining < readlen) readlen = remaining;
}