Redis 请求的一次网络旅程
Redis 不必多说,可以说是目前互联网使用最广泛的内存数据库,源于其丰富的数据结构、支持数据持久化、集群、高性能的这些特性。
本文从 Redis 单线程模式,对于 Redis 客户端从登录到发送消息和收到消息的连续的行为,以 Redis 源码的角度做出解释,详细分析一个 Redis 连接的完整旅程,也作为我在 Redis 学习过程中的一个深入思考的过程。
要解释 Redis 连接的完整过程,事实上主要是对 Redis 网络模型进行分析,因此会分为这几个部分(服务端角度):注册事件、建立连接、接收请求、解析请求、 执行请求、响应请求等。
注册事件
Redis Server 首先会在 initServer()
函数中注册自身的 listenfd。
在 IO 多路复用模型中,如果要监听事件,需要将服务端的 listenfd 注册到 IO 多路复用模型中,这个行为在启动 aeMain
之前通过 createSocketAcceptHandler
已经注册到事件循环器中,这里忽略监听套接字 listenfd 的生成过程,直接进入注册监听事件的流程。
createSocketAcceptHandler
为服务端注册 listenfd 和 accept 事件,也就是接收客户端连接的处理事件,代码如下:
int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler) {
int j;
for (j = 0; j < sfd->count; j++) {
// 注册可读事件
if (aeCreateFileEvent(server.el, sfd->fd[j], AE_READABLE, accept_handler,NULL) == AE_ERR) {
/* Rollback */
for (j = j-1; j >= 0; j--) aeDeleteFileEvent(server.el, sfd->fd[j], AE_READABLE);
return C_ERR;
}
}
return C_OK;
}
createSocketAcceptHandler
通过 aeCreateFileEvent
在网络模型中注册可读事件和事件回调函数 accept_handler
,该读事件回调中调用 accept
来接收客户端连接。因此所有新连接的 Redis 客户端都需要经过该函数,来建立与 Redis 服务器的连接。
上图表明了每次的连接到来事件,都会调用 acceptTcpHandler
进行处理,起到了事件监听的效果。
事件循环
注册好监听事件后,需要启动事件循环器来监听网络事件,Redis 服务端的 main
(server.c)函数通过 aemain
(ae.c) 会开启一个事件循环,从这可以看出来 Redis 的默认网络模型是单线程的IO多路复用。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 处理活跃事件,事件类型默认为所有事件、before_sleep与after_sleep
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
}
其中真正处理事件是在 aeProcessEvents
中,默认 flag 会开启所有事件、before sleep 和 after sleep,aeProcessEvents
是 Redis 网络模块中最重要的函数,属于调度核心,源码如下:
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
// 即非定时事件也非网络事件,什么也不做
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
// 如果有注册的网络事件或者有定时事件
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
struct timeval tv, *tvp;
int64_t usUntilTimer = -1;
// 如果有定时事件,取出最近要触发的定时事件时间
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
usUntilTimer = usUntilEarliestTimer(eventLoop);
// 如果最近定时时间大于零则记录到 tv 中,作为事件循环的 timeout
if (usUntilTimer >= 0) {
tv.tv_sec = usUntilTimer / 1000000;
tv.tv_usec = usUntilTimer % 1000000;
tvp = &tv;
// 如果最近定时时间小于零,也就是有待触发的定时事件
} else {
// 如果是 AE_DONT_WAIT 事件,设置io模型 timeout 为 0,轮询模式
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
// 一般情况设置io模型 timeout 为 NULL,也就是阻塞模式
tvp = NULL; /* wait forever */
}
}
// 如果是 AE_DONT_WAIT 事件,设置io模型 timeout 为 0,轮询模式
if (eventLoop->flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
}
// 处理 beforesleep 回调函数
if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
eventLoop->beforesleep(eventLoop);
// 调用 IO 多路复用网络模型,并设置其 timeout,返回目前活跃网络事件个数
numevents = aeApiPoll(eventLoop, tvp);
// 处理 aftersleep 回调函数
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
eventLoop->aftersleep(eventLoop);
// 循环处理活跃网络事件
for (j = 0; j < numevents; j++) {
int fd = eventLoop->fired[j].fd;
// 取出活跃网络事件
aeFileEvent *fe = &eventLoop->events[fd];
// 取出活跃网络事件的事件类型
int mask = eventLoop->fired[j].mask;
int fired = 0; /* 当前 fd 已处理的事件个数 */
// 是否设置反转标志
int invert = fe->mask & AE_BARRIER;
// 如果未反转,则先处理可读事件
if (!invert && fe->mask & mask & AE_READABLE) {
// 调用当前事件的读回调函数
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
}
// 处理可写事件
if (fe->mask & mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
// 调用当前事件的写回调函数
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
// 如果设置反转,则后处理可读事件
if (invert) {
fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
if ((fe->mask & mask & AE_READABLE) &&
(!fired || fe->wfileProc != fe->rfileProc))
{
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
processed++;
}
}
// 处理定时事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
// 返回已处理的网络/定时事件个数
return processed;
}
通过 aeProcessEvents
可以理清 Redis 网络处理的主要逻辑,大致会处理两类事件:网络事件、定时事件,对于网络事件,则是会通过 IO 多路复用器 aeApiPoll
来检测活跃事件,并依次处理这些事件的回调函数,其次对于网络事件默认会优先处理可读事件,其次处理可写事件,保证对读事件的高响应;对于定时事件,会根据最近定时触发时间来设置 IO 多路复用模型的 timeout,使得能够及时唤醒 IO 多路复用模型,防止定时事件无法及时得到响应。如果最近定时时间小于零, 会将 timeout 设为 0 以便及时处理定时事件。从该函数中看出,Redis 总是优先处理网络事件,其次处理定时事件。
事件循环器启动后,如果无时间处理,默认会阻塞在 aeApiPoll
这里,等待事件的到达或者 timeout 时间耗尽才会唤醒并处理事件。
建立连接
Redis 客户端发起连接请求,通常情况下服务器会在 aeApiPoll
阻塞中被唤醒(如果空闲),产生读事件,因此会调用 fe->rfileProc(eventLoop,fd,fe->clientData,mask)
,来进行处理该事件。
新连接事件的处理函数在 Redis 服务器 initServer 阶段被注册为 acceptTcpHandler
,因此 rfileProc 最终调用的函数是 acceptTcpHandler
,通过该函数来与客户端建立连接,代码如下:
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
UNUSED(el);
UNUSED(mask);
UNUSED(privdata);
// max为每次最大接受连接的数量
while(max--) {
// 通过 accept 接收连接,得到客户端文件描述符 cfd
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK)
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
return;
}
// 开启 cloexec 选项
anetCloexec(cfd);
serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
// 创建客户端对象和连接对象
acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
}
}
这里由于服务器 listenfd 是非阻塞套接字,因此可以使用一个参数 max 来限制每次accept的连接数量。 acceptTcpHandler
中通过 anetTcpAccept
对客户端事件进行响应,得到客户端的文件描述符。其次打开了 cfd 的 cloexec 选项,该选项防止在执行 exec 系函数的进程中会继承 cfd。然后调用 acceptCommonHandler
来创建客户端对象,其中 connCreateAcceptedSocket
创建 cfd 对应的连接对象 connection:
connection *connCreateAcceptedSocket(int fd) {
connection *conn = connCreateSocket();
conn->fd = fd;
conn->state = CONN_STATE_ACCEPTING;
return conn;
}
connection *connCreateSocket() {
connection *conn = zcalloc(sizeof(connection));
// 设置回调函数
conn->type = &CT_Socket;
conn->fd = -1;
return conn;
}
创建新的连接对象 conn,主要的工作是注册了回调函数结构体 conn->type,该结构定义了 read、write 等常规系统调用回调函数,其次设置连接的 fd、state。
创建好连接对象,便通过 acceptCommonHandler
创建客户端对象:
#define MAX_ACCEPTS_PER_CALL 1000
static void acceptCommonHandler(connection *conn, int flags, char *ip) {
client *c;
char conninfo[100];
UNUSED(ip);
// 如果客户端连接状态为 accepting,则出错处理
if (connGetState(conn) != CONN_STATE_ACCEPTING) {
// 代码省略...
}
// 如果客户端连接超出限制,出错处理
if (listLength(server.clients) + getClusterConnectionsCount()
>= server.maxclients)
{
// 代码省略...
}
/* 创建客户端对象 */
if ((c = createClient(conn)) == NULL) {
// 代码省略...
}
// 设置客户端对象的状态
c->flags |= flags;
//
if (connAccept(conn, clientAcceptHandler) == C_ERR) {
// 代码省略...
}
}
acceptCommonHandler
中除了一些异常处理之外,最主要的是调用了 createClient
来生成客户端对象,主要代码如下:
client *createClient(connection *conn) {
client *c = zmalloc(sizeof(client));
if (conn) {
// 设置非阻塞模式
connNonBlock(conn);
// 打开TCPNoDelay选项
connEnableTcpNoDelay(conn);
// 开启 keepalive 选项
if (server.tcpkeepalive)
connKeepAlive(conn,server.tcpkeepalive);
// 绑定客户端读事件回调
connSetReadHandler(conn, readQueryFromClient);
// 将连接对象 private_data 设为 客户端对象 c
connSetPrivateData(conn, c);
}
// 初始化 client 参数
// 代码省略...
}
createClient
主要对客户端对象client进行创建,需要注意的是,这里考虑了 conn 为空,也就是空连接的情况,这是因为有一些客户端无需处理网络连接,比如 lua 脚本,只需要使用到客户端对象的上下文。创建客户端步骤如下:
- 设置连接为非阻塞模式。
- 打开 TCPNoDelay 选项,禁用了Nagle算法,允许小包的发送。
- 开启 tcp keepalive 选项,用于开启TCP探活机制。
- 向网络模型注册客户端读事件和其回调函数
readQueryFromClient
,保证了客户端有读事件到来会调用该函数。 - 将客户端对象设为连接对象的 private_data 成员。
- 初始化客户端各项参数。
这里最重要的是 connSetReadHandler
注册读事件,这里最终注册的是 callHandler
(connhelper.c)函数:
static inline int callHandler(connection *conn, ConnectionCallbackFunc handler) {
connIncrRefs(conn);
// 真正调用回调函数的地方(readQueryFromClient)
if (handler) handler(conn);
connDecrRefs(conn);
if (conn->flags & CONN_FLAG_CLOSE_SCHEDULED) {
if (!connHasRefs(conn)) connClose(conn);
return 0;
}
return 1;
}
在上述函数中对连接对象 conn 使用了引用计数,通过 connIncrRef 和 connDecrRef,防止连接事件未处理完就被关闭。
到此为止一个新的客户端连接已经建立完毕,接下来是等待客户端的请求。
处理请求
建立完客户端连接,服务器便等待客户端的请求到来。如果此时有请求到来,同样服务器会在 aeApiPoll
阻塞中被唤醒(如果空闲),产生读事件,调用 fe->rfileProc(eventLoop,fd,fe->clientData,mask)
,其中 rfileProc 最终调用的是 readQueryFromClient
函数处理客户端请求。
void readQueryFromClient(connection *conn) {
// 得到客户端对象
client *c = connGetPrivateData(conn);
int nread, readlen;
size_t qblen;
// 用于多线程 I/O
if (postponeClientRead(c)) return;
// 记录服务器读处理次数
atomicIncr(server.stat_total_reads_processed, 1);
// 每次读取的最大字节数
readlen = PROTO_IOBUF_LEN;
if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
&& c->bulklen >= PROTO_MBULK_BIG_ARG)
{
ssize_t remaining = (size_t)(c->bulklen+2)-sdslen(c->querybuf);
/* Note that the 'remaining' variable may be zero in some edge case,
* for example once we resume a blocked client after CLIENT PAUSE. */
if (remaining > 0 && remaining < readlen) readlen = remaining;
}
// 计算读缓冲区已有数据大小
qblen = sdslen(c->querybuf);
if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
// 扩大缓冲区至 readlen
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
// 读取客户端数据
nread = connRead(c->conn, c->querybuf+qblen, readlen);
// 省略读结果处理代码
// 更新接收缓冲区长度
sdsIncrLen(c->querybuf,nread);
// 省略代码
// 处理读缓冲区中的数据
processInputBuffer(c);
}
从 readQueryFromClient
函数可以看出其主要工作是通过 connRead 将客户端请求的数据读入到客户端读缓存区 c->querybuf,connRead 是调用了 read系统调用函数来读取数据,在读取数据之前需要对客户端读缓冲区大小进行调整,防止缓冲区过小,Redis 的字符串结构 sds 有提供这类接口 sdsMakeRoomFor
用来扩充缓冲区的可用空间。获取到客户端数据后,通过 processInputBuffer
对读缓存区数据进行处理。
void processInputBuffer(client *c) {
// 循环处理客户端请求,通过标识 c->qb_pos 位判断
while(c->qb_pos < sdslen(c->querybuf)) {
// 省略代码,客户端处于某些 flag 状态下无需处理请求,直接退出
// 判断请求协议是RESP还是纯字符串inline(未编码)格式
if (!c->reqtype) {
if (c->querybuf[c->qb_pos] == '*') {
c->reqtype = PROTO_REQ_MULTIBULK;
} else {
c->reqtype = PROTO_REQ_INLINE;
}
}
// 处理 inline 格式
if (c->reqtype == PROTO_REQ_INLINE) {
if (processInlineBuffer(c) != C_OK) break;
// 如果开启gopher模式且非多线程模式,处理gopher协议格式
if (server.gopher_enabled && !server.io_threads_do_reads &&
((c->argc == 1 && ((char*)(c->argv[0]->ptr))[0] == '/') ||
c->argc == 0))
{
processGopherRequest(c);
resetClient(c);
c->flags |= CLIENT_CLOSE_AFTER_REPLY;
break;
}
// 处理RESP格式协议
} else if (c->reqtype == PROTO_REQ_MULTIBULK) {
if (processMultibulkBuffer(c) != C_OK) break;
} else {
serverPanic("Unknown request type");
}
// 参数为0,
if (c->argc == 0) {
resetClient(c);
} else {
// Redis多线程模式, IO 线程中不立即执行命令,直接退出
if (c->flags & CLIENT_PENDING_READ) {
c->flags |= CLIENT_PENDING_COMMAND;
break;
}
// 终于在这里可以执行客户端命令
if (processCommandAndResetClient(c) == C_ERR) {
// 客户端已经不存在会返回ERR,直接退出
return;
}
}
}
// 重置客户端请求位标识
if (c->qb_pos) {
sdsrange(c->querybuf,c->qb_pos,-1);
c->qb_pos = 0;
}
}
processInputBuffer
中需要判断客户端请求的协议格式,默认情况下是 RESP 协议,该协议能序列化整型、字符串等结构,是一种文本协议。RESP 是二进制安全的,并且不需要处理从一个进程发到另外一个进程的批量数据,因为它使用前缀长度来传输批量数据。下图说明 RESP 协议的字段含义:
Redis官方协议文档:Redis协议详细规范
请求的具体解析由 processMultibulkBuffer
函数来执行,这里就不展开该函数,其工作是根据 RESP 协议格式,把请求内容解析为 Redis 字符串格式,存放到客户端对象的 argc、argv 中。
解析完成后,调用 processCommandAndResetClient
来执行客户端命令,其主要调用 processCommand
来执行命令,函数定义如下:
int processCommand(client *c) {
// 代码省略
// 找到相应命令的执行函数
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
// 处理命令异常情况,省略
// 根据命令类型设置相应字段
int is_read_command = (c->cmd->flags & CMD_READONLY) ||
(c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_READONLY));
int is_write_command = (c->cmd->flags & CMD_WRITE) ||
(c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_WRITE));
int is_denyoom_command = (c->cmd->flags & CMD_DENYOOM) ||
(c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_DENYOOM));
int is_denystale_command = !(c->cmd->flags & CMD_STALE) ||
(c->cmd->proc == execCommand && (c->mstate.cmd_inv_flags & CMD_STALE));
int is_denyloading_command = !(c->cmd->flags & CMD_LOADING) ||
(c->cmd->proc == execCommand && (c->mstate.cmd_inv_flags & CMD_LOADING));
int is_may_replicate_command = (c->cmd->flags & (CMD_WRITE | CMD_MAY_REPLICATE)) ||
(c->cmd->proc == execCommand && (c->mstate.cmd_flags & (CMD_WRITE | CMD_MAY_REPLICATE)));
// 处理认证模式,代码省略
// 处理集群模式,代码省略
// 处理最大内存使用限制功能,如果上限会拒绝某类请求
// 处理其他情况,代码省略
// 处理事务
if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand &&
c->cmd->proc != resetCommand)
{
queueMultiCommand(c);
addReply(c,shared.queued);
} else {
// 执行命令
call(c,CMD_CALL_FULL);
c->woff = server.master_repl_offset;
if (listLength(server.ready_keys))
handleClientsBlockedOnKeys();
}
return C_OK;
}
可以看到 processCommand
事实上主要是调用 lookupCommand
来根据请求内容查找到对应的命令执行函数,来填充客户端 cmd 字段。
其次最重要的就是调用 call
函数,该函数是真正执行客户端命令的地方,属于 Redis 执行命令的核心函数,其中调用了 c->cmd->proc(c)
,来调用 lookupCommand
绑定好的命令执行函数来真正执行命令。
响应请求
在调用相应的命令执行函数并执行完成后,会调用 addReply
生成响应数据,填入到客户端的写缓冲区。其函数定义如下:
void addReply(client *c, robj *obj) {
if (prepareClientToWrite(c) != C_OK) return;
if (sdsEncodedObject(obj)) {
if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)
_addReplyProtoToList(c,obj->ptr,sdslen(obj->ptr));
} else if (obj->encoding == OBJ_ENCODING_INT) {
char buf[32];
size_t len = ll2string(buf,sizeof(buf),(long)obj->ptr);
if (_addReplyToBuffer(c,buf,len) != C_OK)
_addReplyProtoToList(c,buf,len);
} else {
serverPanic("Wrong obj->encoding in addReply()");
}
}
addReply
首先会调用 prepareClientToWrite
,该函数主要是判断客户端是否还有未发送完的数据,如果存在,将未发送完的数据附加到未发送数据尾部并注册写事件,其函数定义如下:
int prepareClientToWrite(client *c) {
// 省略代码
// 判断当前客户端发送缓冲区是否有未发送完的数据
if (!clientHasPendingReplies(c) && !(c->flags & CLIENT_PENDING_READ))
// 注册写事件,将数据附加到未发送数据尾部
clientInstallWriteHandler(c);
return C_OK;
}
其次 addReply
会调用 _addReplyToBuffer
函数,该函数将返回消息写入客户端发送缓冲区 c->buf,如果发送缓冲区已有数据,则调用 _addReplyProtoToList
将待发送数据添加到上一次未发送数据 c->reply 尾部。
到此可写事件已经注册完毕,整个的客户端数据从接收到解析到处理,已经全部执行完毕,就等待响应消息发出。
Redis 如何将消息发出?是使用 beforeSleep,在文章开头介绍的事件循环函数 aeProcessEvents
中可以知道, beforeSleep 是在监听事件前执行的 hook 函数,Redis server 在初始化阶段已将 beforeSleep 绑定到 ae->before_sleep。该函数定义如下:
void beforeSleep(struct aeEventLoop *eventLoop) {
// 省略代码
handleClientsWithPendingWritesUsingThreads();
// 省略代码
}
这里我们省略其他代码,只考虑发送消息相关代码。发送消息在 handleClientsWithPendingWritesUsingThreads
中,Redis 默认是单线程,我们不考虑多线程情况,这个函数会判断是否开启了多线程模式,如果未开启会直接调用 handleClientsWithPendingWrites
。该函数定义如下:
int handleClientsWithPendingWrites(void) {
listIter li;
listNode *ln;
// 获取未发送消息客户端个数
int processed = listLength(server.clients_pending_write);
listRewind(server.clients_pending_write,&li);
// 逐个获取客户端
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 取消可写标志
c->flags &= ~CLIENT_PENDING_WRITE;
// 从未发送消息客户端list删除当前客户端
listDelNode(server.clients_pending_write,ln);
if (c->flags & CLIENT_PROTECTED) continue;
if (c->flags & CLIENT_CLOSE_ASAP) continue;
// 真正发送消息
if (writeToClient(c,0) == C_ERR) continue;
// 如果数据未全部发送完
if (clientHasPendingReplies(c)) {
int ae_barrier = 0;
// 设置 ae_barrier 确保可写事件先与可读事件执行
if (server.aof_state == AOF_ON &&
server.aof_fsync == AOF_FSYNC_ALWAYS)
{
ae_barrier = 1;
}
// 注册可写事件回调 sendReplyToClient
if (connSetWriteHandlerWithBarrier(c->conn, sendReplyToClient, ae_barrier) == C_ERR) {
freeClientAsync(c);
}
}
}
return processed;
}
handleClientsWithPendingWrites
会从 clients_pending_write 链表中依次取出待发送消息的客户端,然后分别给每个客户端发送消息。从 handleClientsWithPendingWrites
函数可以看出,Redis 发送消息的逻辑是先直接调用 writeToClient
将数据发出,如果数据因为网络原因或者客户端原语未全部发送完毕,再通过 connSetWriteHandlerWithBarrier
注册可写事件。
writeToClient
负责发送数据,writeToClient
函数定义如下:
int writeToClient(client *c, int handler_installed) {
atomicIncr(server.stat_total_writes_processed, 1);
ssize_t nwritten = 0, totwritten = 0;
size_t objlen;
clientReplyBlock *o;
while(clientHasPendingReplies(c)) {
if (c->bufpos > 0) {
nwritten = connWrite(c->conn,c->buf+c->sentlen,c->bufpos-c->sentlen);
if (nwritten <= 0) break;
c->sentlen += nwritten;
totwritten += nwritten;
// 如果全部发送完毕,清零相关变量
if ((int)c->sentlen == c->bufpos) {
c->bufpos = 0;
c->sentlen = 0;
}
} else {
// 缓冲区数据发送完毕,从 reply 中拿待发送数据
o = listNodeValue(listFirst(c->reply));
objlen = o->used;
if (objlen == 0) {
c->reply_bytes -= o->size;
listDelNode(c->reply,listFirst(c->reply));
continue;
}
// 发送数据
nwritten = connWrite(c->conn, o->buf + c->sentlen, objlen - c->sentlen);
// 发送失败或者未发送出任何数据则退出
if (nwritten <= 0) break;
c->sentlen += nwritten;
totwritten += nwritten;
// 如果全部发送完毕
if (c->sentlen == objlen) {
c->reply_bytes -= o->size;
listDelNode(c->reply,listFirst(c->reply));
c->sentlen = 0;
// 断言确保数据全部发送
if (listLength(c->reply) == 0)
serverAssert(c->reply_bytes == 0);
}
}
// 防止写操作写过多数据,slave 服务除外
if (totwritten > NET_MAX_WRITES_PER_EVENT &&
(server.maxmemory == 0 ||
zmalloc_used_memory() < server.maxmemory) &&
!(c->flags & CLIENT_SLAVE)) break;
}
atomicIncr(server.stat_net_output_bytes, totwritten);
// 处理写异常
if (nwritten == -1) {
if (connGetState(c->conn) != CONN_STATE_CONNECTED) {
serverLog(LL_VERBOSE,
"Error writing to client: %s", connGetLastError(c->conn));
freeClientAsync(c);
return C_ERR;
}
}
if (totwritten > 0) {
if (!(c->flags & CLIENT_MASTER)) c->lastinteraction = server.unixtime;
}
if (!clientHasPendingReplies(c)) {
c->sentlen = 0;
// 多线程相关
if (handler_installed) connSetWriteHandler(c->conn, NULL);
// 关闭发送完毕的客户端连接
if (c->flags & CLIENT_CLOSE_AFTER_REPLY) {
freeClientAsync(c);
return C_ERR;
}
}
return C_OK;
}
writeToClient
会先把写缓冲区 c->buf 中的数据发送出去,然后再依次发送 c->reply 中所有的数据,如果数据未全部发送完直接退出 reply 循环。
数据未发送完毕,会将 sendReplyToClient
注册为写事件回调,其函数定义如下:
void sendReplyToClient(connection *conn) {
client *c = connGetPrivateData(conn);
writeToClient(c,1);
}
其实还是调用了 writeToClient
函数,直到数据发送完为止,会取消可写事件。
Redis 整个从建立连接到发送响应的流程: