上一章我们介绍了redis处理用户连接请求的过程,如下图所示,redis为一个连接建立了一个client,当用户发送命令时,监听用户命令的fd有事件响应,调用fd绑定的aeFileEvent->rfileProc,就是readQueryFromClient()
本文先忽略一些分布式机制,后面单独开篇讲解(其实是懒得看)
另外,我没全程以set命令举例!
readQueryFromClient()
我们直接看这个函数实现的关键部分
void readQueryFromClient(connection *conn) {
// 多线程模式,下一篇文章讲
if (postponeClientRead(c)) return;
qblen = sdslen(c->querybuf);
//***** 读取数据
int nread = connRead(c->conn, c->querybuf+qblen, readlen);
// ***** 增加当前数据长度
sdsIncrLen(c->querybuf,nread);
// **** 正式处理读到的数据(执行命令)
processInputBuffer(c);
}
这个函数总共分为三部分:
- 读取数据
- 增加当前数据长度
- 处理数据(执行命令)
读取数据
读取数据通过调用connRead()函数
static inline int connRead(connection *conn, void *buf, size_t buf_len) {
return conn->type->read(conn, buf, buf_len);
}
conn->type->read是connSocketRead()函数
static int connSocketRead(connection *conn, void *buf, size_t buf_len) {
int ret = read(conn->fd, buf, buf_len);
conn->state = xxx //一些状态设置
return ret;
}
调用链最后就是这句最关键
read(conn->fd, c->querybuf+qblen, readlen)
可以看到,其实就是调用网络编程里的read()函数,从 fd 对应的socket的接收缓冲区中读取数据到 client 中的 querybuf ,再设置一些conn的状态
clent.
这里可以看到,client->querybuf用来缓存用户发送的数据,这是一个sds结构,其实就是redis自己实现的string,后面文章会详细介绍
增加数据长度
sds是redis的字符串类型,buf是为一个sds分配的空间指针,alloc表示分配空间大小,len表示已用空间大小,如下图:
sdsIncrLen(c->querybuf,nread);
querybufs是一个sds结构,上面这句就是把querybuf.len加上nread,因为上一步调用read()函数向querybuf里添加了数据,已用的空间变大了
处理数据(执行命令)
执行命令的过程在processInputBuffer()中实现,processInputBuffer()函数外层是个while循环,循环的判断是client的读缓冲区(querybuf)还有未处理的数据,那么就进入循环对数据进行处理
void processInputBuffer(client *c) {
while(c->qb_pos < sdslen(c->querybuf)) {
***处理数据的过程***
}
}
下面看看while循环里都做了什么
/***前面是一堆条件判断,满足就break退出while循环,省略***/
/* 判断命令请求类型 telnet发送的命令和redis-cli发送的命令请求格式不同,见后面补充知识 */
if (c->querybuf[0] == '*') {
c->reqtype = PROTO_REQ_MULTIBULK;
} else {
c->reqtype = PROTO_REQ_INLINE;
}
/***** 解析命令,根据请求类型调用下面函数中的一个 ******/
processInlineBuffer(c)
processMultibulkBuffer(c)
processCommandAndResetClient(c)
/****** *执行命令 *******/
processCommand(c)
/******* 命令后处理*******/
commandProcessed(c)
可以看到上面的伪代码主要分为3步
- 解析命令
- 执行命令
- 命令后处理
,
补充知识:请求协议的类型。因为Redis服务器支持
Telnet的连接,因此Telnet命令请求协议类型是PROTO_REQ_INLINE,而redis-cli命令请求的协议类型是PROTO_REQ_MULTIBULK。输入命令 telnet 127.0.0.1 6379 就用telnet登陆本地redis了
解析命令
解析命令由函数processMultibulkBuffer()或者processInlineBuffer()实现 当前命令存在读缓冲区,也就是client.querybuf中,是一串连续字符,解析命令就是解析这一串字符,填写client->agrv、client->argc 例如:当我们输入的命令是set mykey myvalue,此时缓冲区的字符串是 :
"*3\r\n5\r\nmykey\r\n$7\r\nmyvalue\r\n"
分割成字符数组保存在redisCLient数组中:
c->argv[0]="set"
c->argv[1]="mykey"
c->argv[2]="myvalue"
c->argc=3
执行命令
processCommand(c)开始执行命令了,这里面有很多条件判断,详情我们先省略,只看常规的执行
如果命令是quit,就返回退出
从命令表中查找命令
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
异常情况的检测,包括是否超过内存限制、是否合法命令等
如果是事务执行命令, 除 EXEC 、 DISCARD 、 MULTI 和 WATCH 命令之外其他命令都会被函数 queueMultiCommand() 入队到事务队列中处理
queueMultiCommand(c);
如果是常规命令,调用 call() 函数执行命令
call(c,CMD_CALL_FULL);
对于普通命令,processCommand(c)主要做了两件事:
- 首先通过lookupCommand从server.commands命令表中查找命令,设置client->cmd
- 最后就是调用一个call()函数执行命令
我们这次只看call()中这一句,这是执行命令
c->cmd->proc(c);
set命令对应的proc()是setCommand()
struct redisCommand redisCommandTable[] = {
...
{"set",setCommand,-3,
"write use-memory @string",
0,NULL,1,1,1,0,0,0},
...
setCommand()的代码如下:
void setCommand(client *c) {
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_NO_FLAGS;
parseExtendedStringArgumentsOrReply(c,&flags,&unit,
&expire,COMMAND_SET);
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
这里expire是传入的超时时间,setCommand最后调用setGenericCommand()函数
void setGenericCommand(client *c, int flags, robj *key,
robj *val, robj *expire, int unit,
robj *ok_reply, robj *abort_reply) {
前面各种解析flag和超时事件
// 写入数据库
genericSetKey(c,c->db,key, val,flags & OBJ_KEEPTTL,1);
// 设置超时
setExpire(c,c->db,key,when);
// 填写返回给客户端的信息
addReply(c, ok_reply ? ok_reply : shared.ok);
setGenericCommand()函数通过genericSetKey()把用户输入的kv对写入数据库,setExpire()把超时时间写入数据库,这两个函数这里就不展开说了。
最后通过addReply()函数将把服务端对客户端的响应写入到缓冲区,发送给客户端
addReply 方法做了两件事情:
- prepareClientToWrite 判断是否需要返回数据,并且将当前 client 添加到等待写返回数据队列中。
- 调用 _addReplyToBuffer 和 _addReplyObjectToList 方法将返回值写入到输出缓冲区中,等待写入 socekt。
这里简单介绍,后面一节单独讲一下返回客户端的过程
命令后处理
命令后处理就是执行完命令的一些收尾工作,主要在resetClient()函数里,就是把client的各种标志位初始化,准备接收用户的下一条命令
服务端返回
写入输出缓冲区
上一节说过,redis服务端最后通过addReply()函数将把服务端对客户端的响应写入到缓冲区,发送给客户端,我们这一节主要看看addReply()和返回的过程
如果命令执行成功,都会执行这一句
addReply(c,shared.ok);
shared是一个obj类型,redis把所有数据都包装成一个obj,obj指向实际的数据,并且记录了编码方式。shared是一个全局的obj对象
struct sharedObjectsStruct shared;
shared对象成员的填写在createSharedObjects()函数中
void createSharedObjects(void) {
int j;
shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n"));
shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"));
...
}
可以看到shared.ok也在这个函数里被初始化,就是一个"+OK"字符串
我们看看addReply()做了什么
void addReply(client *c, robj *obj) {
prepareClientToWrite(c);
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()");
}
}
其实就做了两件事
- prepareClientToWrite 判断是否需要返回数据,并且将当前 client 添加到等待写返回数据队列中(server.clients_pending_write)。最后实现的是下面这句:
listAddNodeHead(server.clients_pending_write,c);
- 调用 _addReplyToBuffer 和 _addReplyObjectToList 方法将返回值写入到输出缓冲区中,等待写入 socekt。
client的输出缓冲区有两个,一个固定大小的 buffer 和一个响应内容数据的链表。在链表为空并且 buffer 有足够空间时,则将响应添加到 buffer 中。如果 buffer 满了则创建一个节点追加到链表上。_addReplyToBuffer 和 _addReplyObjectToList 就是分别向这两个空间写数据的方法。
到此,要返回给用户的数据就到了输出缓冲区了,接下来要做的就是写入socket
将命令返回值从输出缓冲区写入 socket
上一篇文章说过,在redis的事件循环中,一直循环调用aeProcessEvents()
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
eventLoop->beforesleep(eventLoop);
numevents = aeApiPoll(eventLoop, tvp);
eventLoop->aftersleep(eventLoop);
for (j = 0; j < numevents; j++) {
处理事件
}
}
可以看到每次事件循环等待事件触发前后都会执行例行函数beforesleep()和aftersleep()
beforeSleep 函数会调用 handleClientsWithPendingWritesUsingThreads()函数,如果redis未开启多线程模式,就会调用handleClientsWithPendingWrites()函数来来处理 clients_pending_write 列表,写socket的操作就在这里面。
关于单线程和多线程,下一篇文章解释,本文只看单线程
我们看看handleClientsWithPendingWrites()函数
int handleClientsWithPendingWrites(void) {
// li指向client链表头
listRewind(server.clients_pending_write,&li);
// 遍历链表开始处理
while((ln = listNext(&li))) {
// 从链表中删除然后处理
listDelNode(server.clients_pending_write,ln);
// 写数据
if (writeToClient(c,0) == C_ERR) continue;
// 没写完,注册写事件
if (clientHasPendingReplies(c)) {
connSetWriteHandlerWithBarrier(c->conn,
sendReplyToClient, ae_barrier)
}
}
}
handleClientsWithPendingWrites()函数遍历待处理client链表server.clients_pending_write,先通过writeToClient()函数把数据从输出缓冲区写入socket
int writeToClient(client *c, int handler_installed) {
while(clientHasPendingReplies(c)) { // client缓冲区有东西
if (c->bufpos > 0) {
// buf缓冲区有数据,就把buf里的数据写入socket
connWrite(c->conn,c->buf+c->sentlen,c->bufpos-c->sentlen);
} else {
//如果buf没有数据,就从reply链表中取一个写
o = listNodeValue(listFirst(c->reply));
nwritten = connWrite(c->conn, o->buf + c->sentlen, objlen - c->sentlen);
}
如果写的太多了,就break等下次再写,注意这里会造成这次没写完的情况
if (totwritten > NET_MAX_WRITES_PER_EVENT &&。。。){ break; }
如果client上没有数据了,就释放client
}
}
connWrite()函数最后调用的是connSocketWrite()函数,最后就是调用write()
static int connSocketWrite(connection *conn, const void *data, size_t data_len) {
int ret = write(conn->fd, data, data_len);
if (ret < 0 && errno != EAGAIN) {
conn->last_errno = errno;
if (conn->state == CONN_STATE_CONNECTED)
conn->state = CONN_STATE_ERROR;
}
return ret;
}
到此,数据终于写回了socket!
没写完的情况
如果一次没写完,会设置写事件,给connection的write_handler绑定为sendReplyToClient()
connSetWriteHandlerWithBarrier(c->conn, sendReplyToClient, ae_barrier)
static inline int connSetWriteHandlerWithBarrier(connection *conn, ConnectionCallbackFunc func, int barrier) {
return conn->type->set_write_handler(conn, func, barrier);
}
调用的connSocketSetWriteHandler
static int connSocketSetWriteHandler(connection *conn, ConnectionCallbackFunc func, int barrier) {
if (func == conn->write_handler) return C_OK;
conn->write_handler = func;
if (barrier)
conn->flags |= CONN_FLAG_WRITE_BARRIER;
else
conn->flags &= ~CONN_FLAG_WRITE_BARRIER;
if (!conn->write_handler)
aeDeleteFileEvent(server.el,conn->fd,AE_WRITABLE);
else
if (aeCreateFileEvent(server.el,conn->fd,AE_WRITABLE,
conn->type->ae_handler,conn) == AE_ERR) return C_ERR;
return C_OK;
}
这一套操作下来,又aeCreateFileEvent()往el.events里注册了一写个事件,等同于下面这句:
conn->write_handler = connSocketSetWriteHandler
aeCreateFileEvent(server.el,conn->fd,AE_WRITABLE,
connSocketEventHandler,conn)
注册完以后,因为刚向socket里写了数据,beforesleep()之后的epoll_wait()会触发fd绑定的写事件,connSocketEventHandler()函数,这个函数就看connection有无绑定读写函数,进行调用
tatic void connSocketEventHandler(struct aeEventLoop *el, int fd, void *clientData, int mask) {
...
callHandler(conn, conn->write_handler);
...
}
这里调用的就是sendReplyToClient(),函数里面调用的也是writeToClient(c,1);就是尽可能多的写,如果写完了,会删除注册的write_handler,这样下次事件循环就不会再调用了!下次循环发现client没有数据,就不会write(),再下次循环就不会触发写事件了。
到此,数据已经全部写进了socket!后续就是计算机网络自己的底层工作,我们就不用care了