来看看客户端的数据结构:
typedef struct client {
uint64_t id; /* 客户端id */
int fd; /* socket。fd=-1时表示伪客户端 */
redisDb *db; /* 当前的数据库 */
robj *name; /* 客户端的名字 */
sds querybuf; /* 查询缓冲区 */
sds pending_querybuf;
size_t querybuf_peak; /* querybuf的峰值大小 */
int argc; /* 当前命令的参数个数 */
robj **argv; /* 当前命令的参数数组 */
struct redisCommand *cmd, *lastcmd; /* 最后执行的一个命令 */
int reqtype; /* 请求协议类型,格式为: PROTO_REQ_* */
int multibulklen; /* 需要读取的大容量参数的数量。 */
long bulklen; /* 大容量参数的长度 */
list *reply; /* 要发送给客户端的应答对象的列表 */
unsigned long long reply_bytes; /* 应答对象的字节数 */
size_t sentlen; /* 已经或正在发送到缓冲区的对象的字节数 */
time_t ctime; /* 客户端创建时间 */
time_t lastinteraction; /* 最后一次交互的时间。用于判断是否超时 */
time_t obuf_soft_limit_reached_time;
int flags; /* 标志,格式为: CLIENT_* macros. */
/* 设置了权限时使用,判断是否通过权限验证,requirepass可在redis.conf中设置 */
int authenticated;
int replstate; /* 如果是从客户端,该字段记录它的复制状态 */
int repl_put_online_on_ack;
int repldbfd; /* 复制DB文件描述符 */
off_t repldboff; /* 复制DB文件偏移量 */
off_t repldbsize; /* 复制DB文件大小 */
sds replpreamble; /* 复制数据库序 */
long long read_reploff; /* 如果是主客户端,此字段保存读取的DB文件偏移量 */
long long reploff; /* 如果是主客户端,则应用复制偏移量。 */
long long repl_ack_off; /* 如果是从客户端,此字段保存复制的ACK偏移量 */
long long repl_ack_time;/* 如果是从客户端,此字段保存复制的ACK时间 */
long long psync_initial_offset;
char replid[CONFIG_RUN_ID_SIZE+1]; /* 如果是主客户端,此字段保存主复制id */
int slave_listening_port; /* 从客户端监听端口 */
char slave_ip[NET_IP_STR_LEN];
int slave_capa;
multiState mstate; /* MULTI/EXEC 状态*/
int btype; /* 客户端阻塞类型 */
blockingState bpop; /* 阻塞状态 */
long long woff;
list *watched_keys; /* MULTI/EXEC CAS 被监听的key*/
dict *pubsub_channels; /* 客户端订阅的频道 */
list *pubsub_patterns; /* 客户端订阅的模式 */
sds peerid; /* 缓存同级id */
/* 输出缓冲区 */
int bufpos;
char buf[PROTO_REPLY_CHUNK_BYTES];
} client;
在服务器结构中,客户端属性是一个链表:
struct redisServer {
//...
list *clients;
//...
};
可以通过以下命令来查看客户端状态:
id:客户端唯一标识;
addr:客户端ip与端口;
fd:套接字描述符,fd=9,说明这是普通客户端;
name:名称。可以通过命令来设置:
age:记录从创建客户端开始,客户端与服务端连接的时间(秒);
db:当前数据库为0号数据库。
下面来看看客户端连接到服务端的过程:
在初始化配置之后,调用cliConnect方法:
static int cliConnect(int force) {
if (context == NULL || force) {
if (context != NULL) {
redisFree(context);
}
if (config.hostsocket == NULL) {/* TCP连接 */
context = redisConnect(config.hostip,config.hostport);
} else {/* Unix连接 */
context = redisConnectUnix(config.hostsocket);
}
if (context->err) {/* 记录错误信息,释放空间 */
fprintf(stderr,"Could not connect to Redis at ");
if (config.hostsocket == NULL)
fprintf(stderr,"%s:%d: %s\n",config.hostip,config.hostport,context->errstr);
else
fprintf(stderr,"%s: %s\n",config.hostsocket,context->errstr);
redisFree(context);
context = NULL;
return REDIS_ERR;
}
/* 场链接 */
anetKeepAlive(NULL, context->fd, REDIS_CLI_KEEPALIVE_INTERVAL);
/* 校验权限和选择数据库 */
if (cliAuth() != REDIS_OK)
return REDIS_ERR;
if (cliSelect() != REDIS_OK)
return REDIS_ERR;
}
return REDIS_OK;
}
来看看调用redisConnect方法,通过tcp连接:
redisContext *redisConnect(const char *ip, int port) {
redisContext *c;
c = redisContextInit();
if (c == NULL)
return NULL;
c->flags |= REDIS_BLOCK;
redisContextConnectTcp(c,ip,port,NULL);
return c;
}
关注redisContext数据结构:
typedef struct redisContext {
int err; /* 错误标志。没有错误时err=0 */
char errstr[128]; /* 错误描述 */
int fd;
int flags;
char *obuf; /* 写缓冲区 */
redisReader *reader;
enum redisConnectionType connection_type; /* 连接类型:tcp、unix */
struct timeval *timeout; /* 超时时间 */
struct {/* tcp结构 */
char *host;
char *source_addr;
int port;
} tcp;
struct { /* unix结构 */
char *path;
} unix_sock;
} redisContext;
其中包含了redisReader:
typedef struct redisReader {
int err;
char errstr[128];
char *buf; /* 读缓冲区 */
size_t pos; /* 指针 */
size_t len; /* 缓冲区长度 */
size_t maxbuf; /* 未使用的缓冲区的最大长度 */
redisReadTask rstack[9]; /* 读任务数组 */
int ridx; /* 当前读任务的下标 */
void *reply; /* 临时reply的指针 */
redisReplyObjectFunctions *fn;
void *privdata;
} redisReader;
回到redisConner方法主体,初始化redisContext:
static redisContext *redisContextInit(void) {
redisContext *c;
c = calloc(1,sizeof(redisContext));
if (c == NULL)
return NULL;
c->err = 0;
c->errstr[0] = '\0';
c->obuf = sdsempty();
c->reader = redisReaderCreate();
c->tcp.host = NULL;
c->tcp.source_addr = NULL;
c->unix_sock.path = NULL;
c->timeout = NULL;
if (c->obuf == NULL || c->reader == NULL) {
redisFree(c);
return NULL;
}
return c;
}
最后调用redisContextConnectTcp方法,通过tcp连接。
在连接成功之后,设置长连接,校验权限和选择数据库。
这里来看看校验权限:其中redisCommand方法会阻塞客户端直到服务器回复
static int cliAuth(void) {
redisReply *reply;
/* 如果没有设置权限限制,那么不校验 */
if (config.auth == NULL) return REDIS_OK;
/* 将AUTH命令发送到服务端,等待服务端的回复 */
reply = redisCommand(context,"AUTH %s",config.auth);
if (reply != NULL) {
freeReplyObject(reply);
return REDIS_OK;
}
return REDIS_ERR;
}