环境说明:redis源码版本 5.0.3;我在阅读源码过程做了注释,git地址:https://gitee.com/xiaoangg/redis_annotation 

参考书籍:《redis的设计与实现》


目录

一 客户端属性

1.套接字描述符fd

2.客户端名称name

3.flags标识

4.输入缓冲区querybuf属性

5 命令与命令参数 argv、argc属性

6.命令的实现函数 struct redisCommand *cmd属性

7.输出缓冲区

8.身份验证authenticated属性

9.时间

 

二.客户端的创建与关闭

1.创建普通客户端

2. 关闭客户端



 


Redis 服务器是典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接,接受、处理、回复 每个客户端发送命令请求。


 


通过使用由 I/O 多路复用技术实现的文件事件处理器, Redis 服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。


 


服务器都为客户端建立了相应的 server.h/redisClient结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构;


 


server.h/redisServer中clients链表中;对客户端的批操作,或者查找指定的客户端,都可以通过遍历clients链表来完成:

struct redisServer {
 
    /*
    .......
    */
    list *clients;              /* List of active clients */ //链表 保存来客户端端所有状态
    list *clients_to_close;     /* Clients to close asynchronously */ //异步关闭的客户端
    list *clients_pending_write; /* There is to write or install handler. */
    /*
    .......
    */
};

 


一 客户端属性

客户端状态包含的属性可以分为两类:

  • 一类是比较通用的属性,这些属性很少与特定功能相关,无论客户端执行的是什么工 作,它们都要用到这些属性。
  • 另外一类是和特定功能相关的属性,比如 执行事务时需要用到的mstate属性,以及执行WATCH命令时需要用到的watched_key s属性等。
/**
 * 客户端
 * 对于多路复用,我们需要记录每个客户端的状态
 */ 
/* With multiplexing we need to take per-client state.
 * Clients are taken in a linked list. */
typedef struct client {
    uint64_t id;            /* Client incremental unique ID. *///客户端的全局唯一 自增id
    int fd;                 /* Client socket. *///客户单套接字描述符
    redisDb *db;            /* Pointer to currently SELECTed DB. */ //当前选中的DB
    robj *name;             /* As set by CLIENT SETNAME. */ //客户端的名字
    sds querybuf;           /* Buffer we use to accumulate client queries. */ //输入缓冲区,用于保存客户端发送的命令请求 
    size_t qb_pos;          /* The position we have read in querybuf. */ //读到的输入缓存的位置
    sds pending_querybuf;   /* If this client is flagged as master, this buffer
                               represents the yet not applied portion of the
                               replication stream that we are receiving from
                               the master. */
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size. */
    int argc;               /* Num of arguments of current command. */ //当前执行命令的 参数的数量
    robj **argv;            /* Arguments of current command. */ //当前执行命令的 参数数组
    struct redisCommand *cmd, *lastcmd;  /* Last command executed. */
    int reqtype;            /* Request protocol type: PROTO_REQ_* */
    int multibulklen;       /* Number of multi bulk arguments left to read. */
    long bulklen;           /* Length of bulk argument in multi bulk request. */
    list *reply;            /* List of reply objects to send to the client. */ //要发送到客户端的 对象列表
    unsigned long long reply_bytes; /* Tot bytes of objects in reply list. */
    size_t sentlen;         /* Amount of bytes already sent in the current
                               buffer or object being sent. */
    time_t ctime;           /* Client creation time. */
    time_t lastinteraction; /* Time of the last interaction, used for timeout */
    time_t obuf_soft_limit_reached_time;
    int flags;              /* Client flags: CLIENT_* macros. */
    int authenticated;      /* When requirepass is non-NULL. */
    int replstate;          /* Replication state if this is a slave. */
    int repl_put_online_on_ack; /* Install slave write handler on ACK. */
    int repldbfd;           /* Replication DB file descriptor. */
    off_t repldboff;        /* Replication DB file offset. */
    off_t repldbsize;       /* Replication DB file size. */
    sds replpreamble;       /* Replication DB preamble. */
    long long read_reploff; /* Read replication offset if this is a master. */
    long long reploff;      /* Applied replication offset if this is a master. */
    long long repl_ack_off; /* Replication ack offset, if this is a slave. */
    long long repl_ack_time;/* Replication ack time, if this is a slave. */
    long long psync_initial_offset; /* FULLRESYNC reply offset other slaves
                                       copying this slave output buffer
                                       should use. */
    char replid[CONFIG_RUN_ID_SIZE+1]; /* Master replication ID (if master). */
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */
    char slave_ip[NET_IP_STR_LEN]; /* Optionally given by REPLCONF ip-address */
    int slave_capa;         /* Slave capabilities: SLAVE_CAPA_* bitwise OR. */
    multiState mstate;      /* MULTI/EXEC state */
    int btype;              /* Type of blocking op if CLIENT_BLOCKED. */
    blockingState bpop;     /* blocking state */
    long long woff;         /* Last write global replication offset. */
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid;             /* Cached peer ID. */
    listNode *client_list_node; /* list node in client list */

    /* Response buffer */ //输出缓冲区
    int bufpos;
    char buf[PROTO_REPLY_CHUNK_BYTES];
} client;

1.套接字描述符fd



fd 属性的值可以是 -1 或者是大于 -1 的整数:



  • 伪客户端(fake client)的fd属性的值为-1:伪客户端处理的命令请求来源于AOF文件或者Lua脚本,而不是网络,所以这种客户端不需要套接字连接;
  • 普通客户端的 fd 属性的值为大于 -1的整数:普通客户端使用套接字来与服务器进行通信,服务器会用 fd属性来记录客户端套接字的描述符

 



tips:执行CLIENT list 命令可以列出目前所有连接到服务器的普通客户端,命令会输出fd值;

 

2.客户端名称name

在默认情况下,客户端是没有名字的,name属性指向NULL指针;

tips:使用CLIENT setname命令可以为客户端设置一个名字

3.flags标识

客户端的标志属性flags记录了客户端的角色(role),以及客户端目前所处的状态



/**
 * Client结构体中flags属性值宏定义
 * 
 * 在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器也会成 为主服务器的客户端。
 */ 
/* Client flags */

/**
 * 表示客户端代表的是一个从服务器。
 */ 
#define CLIENT_SLAVE (1<<0)   /* This client is a slave server */
/**
 * 表示客户端代表的是一个主服务器。
 */ 
#define CLIENT_MATER (1<<1)  /* This client is a master server */
/**
 * 表示客户端正在执行MONITOR命令。
 */ 
#define CLIENT_MONITOR (1<<2) /* This client is a slave monitor, see MONITOR */
/**
 * 表示客户端正在执行事务
 */ 
#define CLIENT_MULTI (1<<3)   /* This client is in a MULTI ontext */
/**
 * 表示客户端正在被命令阻塞 如BRPOP、BLPOP等。
 */ 
#define CIENT_BLOCKED (1<<4) /* The client is waiting in a blocking operatin */
/**
 * 表示事务使用WATCH命令监视的数据库键已经被修改,
 * 事务的安全性已经被破坏,这两个标记被打开,EXEC命令必然会执行失败。
 */ 
#define CLIENT_DIRTY_CAS (1<<5) /* Watched keys modified. EXEC will fail. */
/**
 * 表示有用户对这个客户端执行了CLIENT KILL命 令,或者客户端发送给服务器的命令请求中包含了错误的协议内容。
 * 服务器会将客户端积存 在输出缓冲区中的所有内容发送给客户端,然后关闭客户端。
 */ 
#define CLIENT_CLOSE_AFTER_REPLY (1<<6) /* Close after writing entire reply. */

/** * 表示客户端已经从CIENT_BLOCKED标志所表示的阻塞状态中脱离出来,不再阻塞。
 * REDIS_UNBLOCKED标志只能在CIENT_BLOCKED标志已经打开 的情况下使用。
 */ 
#define CLIENT_UNBLOCKED (1<<7) /* This client was unblocked and is stored in
                                server.unblocked_clients *//**
 * 表示客户端是处理Lua脚本伪客户端。
 */                                
#define CLIENT_LUA (1<8) /* This is a n on connected client used by Lua */
/**
 * 表示客户端向集群节点(运行在集群模式下的服务器)发送了 ASKING命令。
 */ 
#define CLIENT_ASKING (1<<9)     /* Client issued the ASKING command */

/**
 * 客户端的输出缓冲区大小超出了服务器允许的范围,服务器会在下一次执行serverCron函数时关闭这个客户端,
 * 以免服务器的稳定性受到这个客户端影响。
 * 积存在输出缓冲区中的所有内容会直接被释放,不会返回给客户端。
 */ 
#define CLIENT_CLOSE_ASAP (1<<10)/* Close this client ASAP */
/**
 * 表示服务器使用UNIX套接字来 接客端。
 */ 
#define CLIENT_UNIX_SOCKET (1<<11) /* Client connected via Unix domain socket */
#define CLIENT_ fail for errors while queueing */
#define CLIENT_MASTER_FORCE_REPLY (1<<13)  /* Queue replies even if is master */
/**
 * 强制服务器将当前执行的命令写入到AOF文件里面
 */ 
#define CLIENT_FORCE_AOF (1<<14)   /* Force AOF propagation of current cmd. */
/**
 * 标志强制主服务器将当前执行的命令复制给所有从服务器
 */ 
#define CLIENT_FORCE_REPL (1<<15)  /* Force replication of current cmd. */
/**
* 表示客户端代表的是一个版本低于Redis2.8的从服务器,
 * 主服务器不能使用PSYNC命令与这个从服务器进行同步。这个标志只能在REDIS_SLAVE标志处 于打开状态时使用。
 */ 
#define CLIENT_PRE_PSYNC (1<<16)   /* Instance don'tunderstand PSYNC. */
#define CLIENT_READONLY (1<<17)    /* Cluster client is in read-only state. */
#define CLIENT_PUBSUB (1<<18)      /* Client is in Pub/Sub mode. */
#define CLIENT_PREVENT_AOF_PROP (1<<19)  /* Don't propagate to AOF. */
#define CLIENT_PREVENT_REPL_PROP (1<<20)  /* Don't propagate to slaves. */
#define CLENT_PREVENT_PROP (CLIENT_PREVENT_AOF_PROP|CLIENT_PREVENT_RPL_PROP)
#define CLIENT_PENDING_WRITE (1<<21) /* Client has output to send but a write
                                        handler is yet not installed. */
#define CLIENT_REPLY_OFF (1<<22)   /* Don't send replies to client. */
#define CLIENT_REPLY_SKIP_NEXT (1<<23)  /* Set CLIENT_REPLY_SKIP for next cmd */
#define CLIENT_REPLY_SKIP (1<<24)  /* Don't send just this reply.*/
#define CLIENT_LUA_DEBUG (1<<25)  /* Run EVAL in debug mode. */
#define CLIENT_LUA_DEBUG_SYNC (1<<26)  /* EVAL debugging withoutfork() */
#define CLIENT_MODULE (1<<27) /* Non connected client used by some module. */
#define CLIENT_PROTECTED (1<<28) /* Client should not be freed for now. */

4.输入缓冲区querybuf属性



输入缓冲区用于保存客户端发送的命令请求,输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但它的最大大小不能超过

1GB ,否则服务器将关闭这个客户端。



 



5 命令与命令参数 argv、argc属性



服务器将querybuf命令请求的内容进行分析,并将得出的命令参数以及命令参数的个数分别保存到客户端状态的 argv 属性和 argc属性;

argv属性是一个数组,数组中的每个项都是一个字符串对象,其中argv[0]是要执行的命令,之后的其他项是传给命令的参数。



argc 属性则负责记录 argv 数组的长度。



 

6.命令的实现函数 struct redisCommand *cmd属性

当服务器从协议内容中分析并得出argv属性和argc属性的值之后,服务器将根据项 argv[0]的值,在命令表(命令表的定义位于server.c/redisCommandTable)中查找命令所对应的命令实现函数。 将cmd属性指向该实现函数;

 

7.输出缓冲区



每个客户端都有两 个输出缓冲区可用:



  1. 固定大小的缓冲区:用于保存那些长度比较小的回复;比如“OK”,简短字符串、错误回复等;
  2.   可变大小的缓冲区用于保存那些长度比较大的回复;

固定大小缓冲区由 buf 和 bufpos两个属性组成;

buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES 字节的字节数组;bufpos属性记录buf数组目前已使用的字节数量。



 



可变大小缓冲区由 reply 链表和一个或多个字符串对象组成:



 

8.身份验证authenticated属性

authenticated的值为0,那么表示客户端未通过身份验证;
authenticated的值为 1,那么表示客户端已经通过了身份验证。

authenticated属性仅在服务器启用了身份验证功能时才有效;

9.时间

ctime属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒;

lastinteraction属性记录了客户端与服务器最后一次进行互动(interaction)的时间,这里 的互动可以是客户端向服务器发送命令请求,也可以是服务器向客户端发送命令回复。
lastinteraction属性可以用来计算客户端的空转(idle)时间



 


 




二.客户端的创建与关闭

 

1.创建普通客户端


客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用connect 函数连接到服务器时,服务器就会调用连接事件处理器,为客户端创建 相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构 clients 链表的末尾。


 


创建客户端的入口路径如下
1.server.c/main() 
2. server.c/initServer(初始化服务器)


3.ae.c/aeCreateFileEvent (注册acceptTcpHandler tcp处理函数,tcp accept() ) 


4.networking.c/acceptCommonHandler


5.networking.c/createClient(fd))  创建客户端;


 


 


2. 关闭客户端

一个普通客户端可能因为多种原因而被关闭:

  • 客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭。
  • 客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会被服务器关闭。
  • 客户端成为了CLIENTKILL命令的目标,那么它也会被关闭。
  • 服务器设置了timeout配置选项,那么当客户端的空转时间超过timeout选项设置的值时,客户端将被关闭。
  • 客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为1 么这个客户端会被服务器关闭。
  • 发送给客户端的命令回复的大小超过了输出缓冲区的限制大小,那么这个客户端会被服务器关闭。