跳到主要内容
数据与中间件redis, source-code从 Redis 7.0 网络层的硬编码演进出发,解析 connListener 与 ConnectionType 如何统一 TCP、TLS 和 Unix Socket 的监听配置、连接接收及 AE 事件注册。

Redis网络层从硬编码到优雅的connListener抽象

从 Redis 7.0 网络层的硬编码演进出发,解析 connListener 与 ConnectionType 如何统一 TCP、TLS 和 Unix Socket 的监听配置、连接接收及 AE 事件注册。

Redis网络层从硬编码到优雅的connListener抽象

Vitah Lin

创建于 更新于 16 分钟阅读

如果让你给 Redis 7.0 的网络层动手术,你会怎么做?

在早期的 Redis 里,网络初始化代码相当“野生”。随着 Redis 6.0 引入 TLS,加上已有的 TCP 和本地 Unix Domain Socket,initServer() 充斥着浓浓的坏味道:

C
/* 早期 Redis 的网络初始化:充斥着协议特化的硬编码分支 */
if (server.port != 0 &&
    listenToPort(server.port, &server.ipfd) == C_ERR) {
    serverLog(LL_WARNING,
        "Failed listening on port %u (TCP), aborting.", server.port);
    exit(1);
}

if (server.tls_port != 0 &&
    listenToPort(server.tls_port, &server.tlsfd) == C_ERR) {
    serverLog(LL_WARNING,
        "Failed listening on port %u (TLS), aborting.", server.tls_port);
    exit(1);
}

/* Open the Unix socket. */
if (server.unixsocket != NULL) {
    server.sofd = anetUnixServer(server.neterr, server.unixsocket,
        server.unixsocketperm, server.tcp_backlog);
    if (server.sofd == ANET_ERR) {
        serverLog(LL_WARNING,
            "Failed opening Unix socket: %s", server.neterr);
        exit(1);
    }
}

这段代码的问题不只是 if-else 多,而且协议差异泄漏到了启动主流程:TCP 和 TLS 各自维护一套 socketFds,Unix socket 使用单独的 sofd;后续注册 AE 事件、关闭 FD、处理错误时,整个主循环必须时刻记着这三套各异的状态。

于是,Redis 7.0 终于在网络层做了一次优雅的“大手术”——抽象出了 connListenerConnectionType。它不仅代表一个物理端口,更代表了一套网络行为契约

拆解核心骨架 struct connListener

这个 struct connListener 结构体是 Redis 网络抽象层中用于统一描述一个连接类型的监听实例的数据结构,在引入该结构体前,Redis 的普通 TCP 监听、TLS 加密监听和本地 Unix Domain Socket 监听的代码逻辑是分散且硬编码的。connListener 的核心目标是将网络监听行为底层传输协议彻底解耦。

它的设计逻辑非常精妙:它不仅代表一个“监听地址”,更代表了一类“连接行为”。定义在 src/connection.hconnListener,职责是将监听配置内核监听 FD 以及具体连接类型的处理逻辑绑定在一起:

C
struct connListener {  
    int fd[CONFIG_BINDADDR_MAX];  /* 物理监听文件描述符数组 (默认上限 16) */
    int count;                    /* 成功开启的物理 FD 数量 */
    char **bindaddr;              /* 绑定的地址列表 */
    int bindaddr_count;           /* 绑定地址数 */
    int port;                     /* 端口号 (TCP/TLS 生效;Unix Socket 忽略) */
    ConnectionType *ct;           /* 核心:行为抽象表 (C 风格虚函数表) */
    void *priv;                   /* 连接类型私有数据 (如 Unix Socket 权限) */
};

理解这个结构体,可以从以下 3 个维度切入:

物理监听 FD(为什么是个数组?)

C
int fd[CONFIG_BINDADDR_MAX];  
int count;  

很多人潜意识里认为一个监听端口 = 一个 Socket FD。但在实际生产环境中,当你在 redis.conf 里配置了 bind 127.0.0.1 192.168.1.100,或者操作系统同时启用了 IPv 4/IPv 6 双栈时,同一个逻辑端口(如 6379)在内核中会对应多个独立的 Socket 文件描述符

  • fd[]:保存该监听器名下所有的底层 Socket FD。
  • count:记录当前监听器实际上成功开启了多少个 FD。

绑定地址与端口

C
char **bindaddr;
int bindaddr_count;
int port;
  • bindaddr :指向字符串数组,存储了配置要求绑定的 IP 地址。
  • bindaddr 通常直接指向 server.bindaddr;Unix socket 场景则指向只包含 socket 路径的字符串指针。它供具体连接类型的 listen 实现读取,并不是监听器自己拥有的字符串副本。
  • port 该监听器对应的端口号,对 TCP/TLS 监听有效;Unix socket 不使用该字段。

连接类型与私有数据

C
ConnectionType *ct;
void *priv;

这是 Redis 实现“插件化”网络协议的关键:

  • ConnectionType *ct:这是一个函数指针表(可视为 C 风格的虚函数表),决定监听、接收、连接创建和 I/O 的实现。
  • priv:由具体连接类型解释的私有数据。当前 Unix socket 监听器把它设为 &server.unixsocketperm,供 Unix 监听实现设置文件权限;TCP/TLS 通常不使用它。

行为契约:ConnectionType 的多态分发

redisServer 全局结构体中,为所有连接类型预分配了一个静态槽位数组:

C
struct redisServer {
    connListener listeners[CONN_TYPE_MAX]; /* 为 TCP/Unix/TLS 预留槽位 */
};

这属于静态数组。一旦 server 这个全局变量被定义(或者通过 malloc 为整个 redisServer 分配空间时),这块内存就已经存在了。大小固定: 它的空间大小等于 sizeof(struct connListener) * CONN_TYPE_MAX

每种协议的具体行为,都由 ConnectionType 虚函数表决定。ConnectionType 具体代码如下,这里使用了 C99 的指定初始化器(Designated Initializers) 静态装配不同的协议行为

C
typedef struct ConnectionType {  
    const char *(*get_type)(struct connection *conn);  
    
    /* 监听与接入核心函数指针 */
    int (*listen)(connListener *listener);  
    aeFileProc *accept_handler;  
    int (*accept)(struct connection *conn, ConnectionCallbackFunc accept_handler);  

    /* ... 省略具体的 read/write/writev 等传输层函数 ... */
} ConnectionType;

不同的连接类型会设置不同的 accept_handler,例如普通的 socket:

C
static ConnectionType CT_Socket = {  
    .get_type = connSocketGetType,  
    /* ae & accept & listen & error & address handler */  
    .ae_handler = connSocketEventHandler,  
    .accept_handler = connSocketAcceptHandler,  
    .addr = connSocketAddr,  
    .is_local = connSocketIsLocal,  
    .listen = connSocketListen,  
};

TLS 类型的 accept_handler

C
static ConnectionType CT_TLS = {
    .get_type = connTLSGetType,
    .ae_handler = tlsEventHandler,
    .accept_handler = tlsAcceptHandler,
    .listen = connTLSListen,
    /* 省略其他字段 */
};

网络监听总门户:initListeners

在服务初始化阶段,server.c/initListeners() 扮演了统一调度中心。有了前面的抽象,它的逻辑被极度浓缩。

它的核心职责是:读取并解析用户的网络配置(redis.conf),为所有需要监听的协议创建并绑定底层的操作系统 Socket,然后将这些 Socket 挂载到 AE 事件驱动主循环中,正式开始 接受客户端连接。

server.c/initListeners() 的核心逻辑是这段代码:

C
/* 1. 设置 TCP 监听器参数 */
if (server.port != 0) {  
    conn_index = connectionIndexByType(CONN_TYPE_SOCKET);  
    listener = &server.listeners[conn_index];  
    listener->bindaddr = server.bindaddr;  
    listener->bindaddr_count = server.bindaddr_count;  
    listener->port = server.port;  
    listener->ct = connectionByType(CONN_TYPE_SOCKET);  
}

/* 2. 统一多态遍历:无论什么协议,完全走同一套注册逻辑 */
int listen_fds = 0;  
for (int j = 0; j < CONN_TYPE_MAX; j++) {  
    listener = &server.listeners[j];  
    if (listener->ct == NULL)  
        continue;  
  
    /* 调用具体协议的系统监听 (socket -> bind -> listen) */
    if (connListen(listener) == C_ERR) {  
        serverLog(LL_WARNING, "Failed listening on port %u (%s), aborting.", 
                  listener->port, listener->ct->get_type(NULL));  
        exit(1);  
    }  
  
    /* 批量挂载到 AE 事件驱动主循环 */
    if (createSocketAcceptHandler(listener, connAcceptHandler(listener->ct)) != C_OK)  
        serverPanic("Unrecoverable error creating %s listener accept handler.", 
                    listener->ct->get_type(NULL));  
  
    listen_fds += listener->count;  
}

整个初始化过程分成了两步核心推演:

Code
[initListeners]
       │
       ▼
[调用 connListen(listener)] -> 内核层排队:系统调用 socket -> bind -> listen,产生处于 LISTEN 的 FD
       │
       ▼
[调用 createSocketAcceptHandler] -> 事件驱动挂载:将 listener->fd[] 注入 AE 循环,等待客户端握手

这样就解决了 Redis 早期版本的耦合问题:

  1. 解耦协议与逻辑:主循环(Event Loop)不需要关心这个 FD 的连接类型,只需注册 ct->accept_handler;真正的连接接收逻辑再由对应的 ConnectionType 实现处理。

  2. 统一管理:通过 listen_fds += listener->count; 这种逻辑,可以非常方便地统计全局监听状态。

  3. 扩展性:新增连接类型可以复用 connListener 和监听注册流程,但仍需实现该类型所需的 ConnectionType 回调,并接入类型注册和配置流程。

  4. connListen(listener)

    1. 是真正干活:调用系统的 socket() -> bind() -> listen()
    2. 结果:此时,操作系统的内核已经开始在对应的端口上排队等待连接了,文件描述符(FD)被存入 listener->fd[]
  5. createSocketAcceptHandler:

    1. 注册事件:把上面拿到的 FD 注册到 Redis 的 AE 事件驱动模型(Event Loop)中。
    2. 绑定回调:告诉 Redis:“一旦这个 FD 有新连接进来,请立刻调用 connAcceptHandler 去处理它”。

这里的 TLS 语义也要单独说明:connTLSListen() 当前复用 TCP 的 listenToPort(),监听 FD 本身并不执行 TLS 握手;客户端连接被 accept() 后,才由 tlsAcceptHandler 创建/驱动 TLS 握手状态机。因此“监听协议”和“连接建立后的加密握手”是两个阶段。

在旧版本的 Redis 中,TCP 和 Unix Socket 的开启逻辑是分开写的,代码很冗余。这个函数体现了 Redis 7.0 之后的高度抽象化:

  • 统一化:不管是网线过来的 TCP,还是内存里的 Unix Socket,都走这一套逻辑。
  • 插件化:如果你想让 Redis 支持一种新的协议(比如某种私有加密协议),你只需要在 connectionIndexByType 里加个类型,而不需要改动这个 initListeners 的主流程。

创建监听套接字:connListen

connListen 定义在 src/connection.h 中。它的核心定位是:调用具体协议的监听实现,完成从用户配置到内核套接字(socket -> bind -> listen)的物理开启过程。

它的源码实现只有一行代码:

C
static inline int connListen(connListener *listener) {
    return listener->ct->listen(listener);
}

connListen() 自身不写具体的系统调用,而是作为一个多态代理,把传入的 connListener 指针转交给具体的连接类型虚函数 listener->ct->listen

以最常见的普通 TCP 协议为例,listener->ct->listen 指向的是 connSocketListen(),它会依次执行以下关键动作:

  1. 地址遍历与双栈绑定:根据 listener->bindaddr 传入的 IP 地址数组,分别处理 IPv4 和 IPv6 地址。
  2. 物理套接字系统调用:底层调用 anetTcpServer / anetTcp6Server,为每一个绑定的 IP 依次执行操作系统的 socket() 创建套接字、bind() 绑定端口与地址,以及 listen() 开启监听队列。
  3. 设置套接字属性:将创建好的监听套接字设置为非阻塞模式(O_NONBLOCK),并开启 SO_REUSEADDR 等选项。
  4. 回填物理 FD 数组:将所有成功 listen 的套接字描述符存入 listener->fd[] 数组中,并累加记录有效数量 listener->count

connListen 处于“配置已就绪”与“事件已注册”之间的承前启后位置:

Code
[initListeners]
       │
       ▼
[填充 connListener 配置与 ct]
       │
       ▼
[调用 connListen(listener)] ──────> 内核中产生处于 LISTEN 状态的 FD,存入 listener->fd[]
       │
       ▼
[调用 createSocketAcceptHandler] ──> 将 listener->fd[] 注册到 AE 事件循环 (AE_READABLE)

通过 connListen 的这层封装,不同协议的监听细节被彻底屏蔽:

连接类型 (ConnectionType)实际执行的函数核心行为差异
标准 TCP (CT_Socket)connSocketListen绑定 IP/Port,创建标准 TCP 监听套接字。
TLS 加密 (CT_TLS)connTLSListen当前实现同样复用 listenToPort 创建 TCP 监听 FD;TLS 握手在接受连接后由 tlsAcceptHandler/TLS 状态机完成。
本地 IPC (CT_Unix)connUnixListen调用 anetUnixServer 创建 Unix Domain Socket 文件(如 /tmp/redis.sock),并设置文件访问权限(umask)。

简而言之, connListen 负责在操作系统内核中把“大门打开并排队”(产生处于 LISTEN 状态的 FD),后续的流程才负责把这扇大门接进 Redis 的事件循环系统。

接收连接回调:connAcceptHandler

Code
[客户端发起 TCP 三次握手]
         │
         ▼
[Reactor 事件循环检测到 Server Socket 可读]
         │
         ▼
[触发 connAcceptHandler]
         │
         ├──> 调用 anetTcpAccept (底层调用 accept())
         ├──> 检查是否超过 maxclients
         ├──> 创建 client 结构体并绑定连接
         └──> 将通信 fd 的 AE_READABLE 事件注册到事件循环(绑定 readQueryFromClient)

connAcceptHandler 是 Redis 网络抽象层中的一个轻量内联多态取值函数(Accessor Function),定义在 src/connection.h 中,它的定义极其精简:

C
static inline aeFileProc *connAcceptHandler(ConnectionType *ct) {
    if (ct)
        return ct->accept_handler;
    return NULL;
}

核心作用与定位:

  1. 它是一个取值函数(Getter),不执行任何 I/O 操作,也不调用系统底层 accept()
  2. 它的核心作用:从当前的连接类型虚函数表(ConnectionType *ct)中,取出符合 AE 事件循环回调签名(aeFileProc)的函数指针。
连接类型 (ConnectionType)connAcceptHandler(ct) 实际返回的函数核心差异
标准 TCP Socket CT_SocketconnSocketAcceptHandler底层调用标准 accept(),创建明文通信套接字。
TLS 加密连接 CT_TLStlsAcceptHandler接收 Socket 后,还需要进一步初始化 SSL 上下文并启动 TLS 握手状态机。
本地 Unix Socket CT_UnixconnUnixAcceptHandler针对本地 IPC 本地域套接connAcceptHandler 通常是网络编程或底层网络框架(如基于 Reactor 模型的 Redis、Netty 等事件驱动架构)中的新连接接收处理器(Connection Accept Handler)。

将监听 FD 注册到事件循环:createSocketAcceptHandler

因此,下面这段代码:

C
createSocketAcceptHandler(listener, connAcceptHandler(listener->ct));

可以理解为:

C
aeFileProc *handler = listener->ct->accept_handler;
createSocketAcceptHandler(listener, handler);

createSocketAcceptHandler 是一个事件注册安装器(Event Registration Installer)。 它的唯一职责是:connListener 中所有处于监听状态的底层 Socket 文件描述符(FD),批量注册进 Redis 的 AE 事件循环(server.el)中,并绑定新连接到达时的回调函数。

源码如下:

C
int createSocketAcceptHandler(connListener *sfd, aeFileProc *accept_handler) {  
    int j;  
  
    for (j = 0; j < sfd->count; j++) {  
        if (aeCreateFileEvent(server.el, sfd->fd[j], AE_READABLE, accept_handler,sfd) == AE_ERR) {  
            /* 注意这个细节:倒序事务回滚 */
            for (j = j-1; j >= 0; j--) 
                aeDeleteFileEvent(server.el, sfd->fd[j], AE_READABLE);  
            return C_ERR;  
        }  
    }  
    return C_OK;  
}

这个函数完成了三项核心工作:

  1. 多 FD 批量注册:一个逻辑监听器可能同时绑定了多个物理 IP(如 IPv 4 的 0.0.0.0 和 IPv 6 的 ::),产生多个 Socket FD。该函数通过 for 循环遍历 sfd->fd[0 ... count-1]
  2. 挂载事件与回调:对每个 FD 调用 aeCreateFileEvent
    1. 注册事件类型:AE_READABLE(监听 Socket 只要有客户端完成三次握手,内核全连接队列非空,就表现为“可读”)。
    2. 绑定处理函数:accept_handler(对于普通 TCP,传入的就是 connSocketAcceptHandler)。
    3. 附带上下文:把 sfd(当前的 connListener 指针)作为 clientData 存入,供后续回调提取私有配置。

一个耐人寻味的细节:

很多人写注册循环时,一旦中间某一个挂了(比如触发了系统的 epoll 限制),往往直接拍屁股 return C_ERR 完事,留下前面几个已经注册到 Event Loop 的孤儿 FD 随风飘摇。 Redis 这里的处理极其严密:一旦第 NN 个 FD 注册失败,立马通过逆向循环 for (j = j - 1; j >= 0; j--) 把前面所有挂上去的事件逐一注销。这就是经典分布式事务在单机底层编码里的缩影——要么全部成功,要么干干净净地回滚。

这里监听 Socket 的“可读”并不是表示客户端业务数据已经到达,而是表示:

监听 Socket 的连接队列中存在等待接收的新连接,此时可以调用 accept()

执行到这里,Redis 就完成了服务启动与事件注册(只执行一次):

Code
[initListeners]
       │
       ├──> connAcceptHandler(listener->ct)  ──> 取出函数指针 connSocketAcceptHandler
       │
       └──> createSocketAcceptHandler(...)   ──> 遍历 listener->fd[],调用 aeCreateFileEvent
                                                 将 FD 和 connSocketAcceptHandler 注册到 server.el

后续就是在 aeMain 中一直做事件循环,等待客户端的连接:

C
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}

总结:优雅抽象的收益

完成事件挂载后,Redis 即可进入经典的 aeMain() 事件循环,静待请求来临:

Code
[客户端发起 TCP 三次握手]
         │
         ▼
[Reactor 检测到 Server Socket 可读 (AE_READABLE)]
         │
         ▼
[触发绑定的 accept_handler (如 connSocketAcceptHandler)]
         │
         ├──> 调用 anetTcpAccept (底层调用 accept())
         ├──> 检查是否超过 maxclients 限制
         ├──> 创建 client 结构体并绑定连接
         └──> 将新套接字挂载到 AE (绑定 readQueryFromClient)

回头审视 Redis 对网络抽象层的改造,收益是立竿见影的:

  1. 核心逻辑彻底解耦:消除了 initServer() 和事件处理中分散的 if (is_tls) ... else if (is_unix) 特化硬编码,完全基于虚函数表进行多态分发。
  2. 插件化扩展能力:如果未来需要支持某种私有加密协议或新的传输协议,开发者只需实现一套 ConnectionType 虚函数表并注册,核心网络监听主循环不需要做任何改动。