Redis是一个典型的一对多的服务程序,一个服务器可以和多个客户端建立网络连接,对于每个与服务器连接的客户端,服务器都为这些客户端连接了相应的redisClient结构,这个结构保存了客户端当前的状态信息,以及执行相关功能所需要的数据结构。
1. 客户端状态:struct redisClient
Redis服务器的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构,查找某个指定的客户端可以通过遍历clients链表来完成。
struct redisServer {
// ......
list *clients; // 一个链表,保存了所有客户端状态
// ......
};
客户端状态redisClient的数据结构定义如下:
typedef struct redisClient {
int fd; // 套接字描述符
redisDb *db; // 当前正在使用的数据库
int dictid; // 当前正在使用的数据库的 id (号码)
robj *name; // 客户端的名字,可以通过命令CLIENT SETNAME 进行设置
sds querybuf; // 查询缓冲区
size_t querybuf_peak; // 查询缓冲区长度峰值
int argc; // 参数数量
robj **argv; // 参数对象数组
struct redisCommand *cmd, *lastcmd; // 记录被客户端执行的命令
int reqtype; // 请求的类型:内联命令还是多条命令
int multibulklen; // 剩余未读取的命令内容数量
long bulklen; // 命令内容的长度
list *reply; // 回复链表
unsigned long reply_bytes; // 回复链表中对象的总大小
int sentlen; // 已发送字节,处理 short write 用
time_t ctime; // 创建客户端的时间
time_t lastinteraction; // 客户端最后一次和服务器互动的时间
time_t obuf_soft_limit_reached_time; // 客户端的输出缓冲区超过软性限制的时间
int flags; // 客户端状态标志 REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ...
int authenticated; // 当 server.requirepass 不为 NULL 时,代表认证的状态,0 代表未认证, 1 代表已认证
int replstate; // 复制状态
int repldbfd; // 用于保存主服务器传来的 RDB 文件的文件描述符
off_t repldboff; // 读取主服务器传来的 RDB 文件的偏移量
off_t repldbsize; /* replication DB file size */
sds replpreamble; /* replication DB preamble. */
long long reploff; // 主服务器的复制偏移量
long long repl_ack_off; // 从服务器最后一次发送 REPLCONF ACK 时的偏移量
long long repl_ack_time; // 从服务器最后一次发送 REPLCONF ACK 的时间
char replrunid[REDIS_RUN_ID_SIZE+1]; // 主服务器的 master run ID,保存在客户端,用于执行部分重同步
int slave_listening_port; // 从服务器的监听端口号
multiState mstate; // 事务状态
int btype; // 阻塞类型
blockingState bpop; // 阻塞状态
long long woff; // 最后被写入的全局复制偏移量
list *watched_keys; // 被监视的键
dict *pubsub_channels; // 这个字典记录了客户端所有订阅的频道,键为频道名字,值为 NULL,也即是,一个频道的集合
list *pubsub_patterns; // 链表,包含多个 pubsubPattern 结构,记录了所有订阅频道的客户端的信息,新 pubsubPattern 结构总是被添加到表尾
sds peerid; // 缓存的 peer ID
int bufpos; // 回复偏移量
char buf[REDIS_REPLY_CHUNK_BYTES]; // 回复缓冲区
} redisClient;
1.1 标志flags
客户端的标志属性flags记录了客户端的角色,以及客户端目前所处的状态。flags的属性值可以是单个标志,也可以是多个标志的二进制组成。每个标志使用一个常量表示,一个部分标志记录了客户端的角色,另一部分标志则记录了客户端目前所处的状态。flags = <flag1> | <flag2> | ...
客户端的角色:
- REDIS_MASTER:标志客户端代表的是一个主服务器
- REDIS_SLAVE:标志客户端代表的是一个从服务器
- REDIS_PER_PSYNC:标志客户端代表的是一个版本低于Redis2.8的从服务器
- REDIS_LUA_CLIENT:标志客户端是专门用于处理Lua脚本里面包含的Redis命令的伪客户端
- REDIS_MONITOR:标志客户端是一个slave monitor
客户端所处的状态:
- REDIS_UNIX_SOCKET:标志服务器使用UNIX套接字来连接客户端
- REDIS_BLOCKED:标志客户端正在被BRPOP、BLPOP等命令阻塞
- REDIS_UNBLOCKED:标志客户端已经从阻塞状态中脱离出来
- REDIS_MULTI:标志客户端正在执行事务
- REDIS_DIRTY_EXEC:标志事务在命令入队时出现错误
- REDIS_DIRTY_CAS:表示事务使用WATCH命令监视的数据库键已经被修改
- REDIS_CLOSE_ASAP:标志客户端的输入缓冲区超过了服务器允许的范围,服务器会在下一次执行serverCron函数时关闭这个客户端
- REDIS_CLOSE_AFTER_REPLY:表示有用户对这个客户端执行了CLIENT KILL命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容
- REDIS_ASKING:标志客户端向集群节点发送了ASKING命令
- REDIS_FORCE_AOF:标志表示强制服务器将当前的执行命令写入AOF文件里面
- REDIS_FORCE_REPL:表示强制主服务器将当前执行命令复制给所有从服务器。执行SCRIPT LOAD命令会使得客户端打开以上这两个标志。
1.2 输入缓冲区querybuf
客户端的输入缓冲区用于保存客户端发送的命令请求,输入缓冲区会根据输入内容动态地缩小或者扩大,但是它的大小不能超过1GB,否则服务器将关闭这个客户端。
1.3 命令argv和命令参数个数argc
服务器将客户端发送的命令请求保存到客户端状态的querybuf属性之后,服务器将对命令请求的内容进行分析,并将得出的命令参数和命令参数个数分别保存到客户端状态的argv和argc属性中。argv是一个数组,数组中每一项保存了一个字符串对象,argv[0]是要执行的命令,之后则是传给命令的参数。argc则负责记录argv数组的长度。
1.4 命令实现函数cmd
解析的到argv[0]之后,服务器将在命令表中查找对应的命令实现函数。并将客户端状态的cmd指针指向这个结构。之后,服务器就可以使用cmd属性所指向的redisCommand结构,以及argv和argc属性中保存的命令参数信息,调用命令实现函数,执行客户端指定的指令。
1.5 输出缓冲区buf和reply
执行命令得到命令回复会被保存在客户端的输入缓冲区里面,每个客户端有两个输出缓冲区,一个缓冲区大小固定,buf默认大小为16KB,用于保存那些长度比较小的回复。另外一个缓冲区大小可变,reply是一个链表,用于保存那些长度比较大的回复。
1.6 身份验证authenticated
authenticated属性用于保存客户端是否通过了了身份验证,0表示验证通过,1表示验证未通过。
2. 客户端的创建
2.1 客户端redis-cli创建流程
- 填写配置信息到config中:填写hostip、port、dbnum等信息到struct config结构体中
- 解析命令行参数到config中:解析redis client启动时的命令行参数,解析参数并保存到struct config结构体中
- 创建socket、连接server:在cliConnect()函数中创建套接字,并且连接服务器
- 设置Nagle和TCP的keepalive:客户端禁用Nagle算法,这样可以避免出现粘包问题;开启TCP的keepalive字段,用于在长连接时进行心跳检测
- 客户端认证auth:如果config结构体中auth字段保存了密码,则需要进行认证,发送命令:auth passwd
- 选择server数据库:根据config结构体中配置的dbnum,选择服务器的数据库,发送命令:select dbnum
- 进入交互界面:进入一个无限循环,不断从命令行中读取命令,解析参数,发送命令,并且打印出命令的执行结果
2.2 相关数据结构
/* 连接redis的上下文信息 */
typedef struct redisContext {
int err; /* Error flags, 0 when there is no error */
char errstr[128]; /* String representation of error when applicable */
int fd; // socket文件描述符
int flags;
char *obuf; /* Write buffer */
redisReader *reader; /* Protocol reader */
} redisContext;
static redisContext *context; // 如果context为NULL,则表示需要重新连接
/* 保存连接redis服务器所需要的一些配置信息 */
static struct config {
char *hostip; // IP地址
int hostport; // 端口号
char *hostsocket;
long repeat;
long interval;
int dbnum; // 选择使用哪个数据库
int interactive;
... ...
} config;
2.3 主函数
int main(int argc, char **argv) {
int firstarg;
config.hostip = sdsnew("127.0.0.1");
config.hostport = 6379;
config.hostsocket = NULL;
config.repeat = 1;
config.interval = 0;
config.dbnum = 0;
... ...
if (!isatty(fileno(stdout)) && (getenv("FAKETTY") == NULL))
config.output = OUTPUT_RAW;
else
config.output = OUTPUT_STANDARD;
config.mb_delim = sdsnew("\n");
cliInitHelp();
firstarg = parseOptions(argc,argv); // 解析命令行参数到config中
argc -= firstarg;
argv += firstarg;
... ...
/* 如果命令行没有任何参数,直接进入交互模式 */
if (argc == 0 && !config.eval) {
cliConnect(0); // 连接服务器
repl(); // 进入交互界面
}
... ...
}