如果让你给 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 终于在网络层做了一次优雅的“大手术”——抽象出了 connListener 与 ConnectionType。它不仅代表一个物理端口,更代表了一套网络行为契约。
拆解核心骨架 struct connListener
这个 struct connListener 结构体是 Redis 网络抽象层中用于统一描述一个连接类型的监听实例的数据结构,在引入该结构体前,Redis 的普通 TCP 监听、TLS 加密监听和本地 Unix Domain Socket 监听的代码逻辑是分散且硬编码的。connListener 的核心目标是将网络监听行为与底层传输协议彻底解耦。
它的设计逻辑非常精妙:它不仅代表一个“监听地址”,更代表了一类“连接行为”。定义在 src/connection.h 的 connListener,职责是将监听配置、内核监听 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 早期版本的耦合问题:
-
解耦协议与逻辑:主循环(Event Loop)不需要关心这个 FD 的连接类型,只需注册
ct->accept_handler;真正的连接接收逻辑再由对应的ConnectionType实现处理。 -
统一管理:通过
listen_fds += listener->count;这种逻辑,可以非常方便地统计全局监听状态。 -
扩展性:新增连接类型可以复用
connListener和监听注册流程,但仍需实现该类型所需的ConnectionType回调,并接入类型注册和配置流程。 -
connListen(listener)
- 是真正干活:调用系统的 socket() -> bind() -> listen()
- 结果:此时,操作系统的内核已经开始在对应的端口上排队等待连接了,文件描述符(FD)被存入
listener->fd[]。
-
createSocketAcceptHandler:
- 注册事件:把上面拿到的 FD 注册到 Redis 的 AE 事件驱动模型(Event Loop)中。
- 绑定回调:告诉 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(),它会依次执行以下关键动作:
- 地址遍历与双栈绑定:根据 listener->bindaddr 传入的 IP 地址数组,分别处理 IPv4 和 IPv6 地址。
- 物理套接字系统调用:底层调用 anetTcpServer / anetTcp6Server,为每一个绑定的 IP 依次执行操作系统的 socket() 创建套接字、bind() 绑定端口与地址,以及 listen() 开启监听队列。
- 设置套接字属性:将创建好的监听套接字设置为非阻塞模式(O_NONBLOCK),并开启 SO_REUSEADDR 等选项。
- 回填物理 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;
}
核心作用与定位:
- 它是一个取值函数(Getter),不执行任何 I/O 操作,也不调用系统底层
accept()。 - 它的核心作用:从当前的连接类型虚函数表(
ConnectionType *ct)中,取出符合 AE 事件循环回调签名(aeFileProc)的函数指针。
| 连接类型 (ConnectionType) | connAcceptHandler(ct) 实际返回的函数 | 核心差异 |
|---|---|---|
标准 TCP Socket CT_Socket | connSocketAcceptHandler | 底层调用标准 accept(),创建明文通信套接字。 |
TLS 加密连接 CT_TLS | tlsAcceptHandler | 接收 Socket 后,还需要进一步初始化 SSL 上下文并启动 TLS 握手状态机。 |
本地 Unix Socket CT_Unix | connUnixAcceptHandler | 针对本地 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;
}
这个函数完成了三项核心工作:
- 多 FD 批量注册:一个逻辑监听器可能同时绑定了多个物理 IP(如 IPv 4 的
0.0.0.0和 IPv 6 的::),产生多个 Socket FD。该函数通过for循环遍历sfd->fd[0 ... count-1]。 - 挂载事件与回调:对每个 FD 调用
aeCreateFileEvent:- 注册事件类型:
AE_READABLE(监听 Socket 只要有客户端完成三次握手,内核全连接队列非空,就表现为“可读”)。 - 绑定处理函数:
accept_handler(对于普通 TCP,传入的就是connSocketAcceptHandler)。 - 附带上下文:把
sfd(当前的connListener指针)作为clientData存入,供后续回调提取私有配置。
- 注册事件类型:
一个耐人寻味的细节:
很多人写注册循环时,一旦中间某一个挂了(比如触发了系统的 epoll 限制),往往直接拍屁股
return C_ERR完事,留下前面几个已经注册到 Event Loop 的孤儿 FD 随风飘摇。 Redis 这里的处理极其严密:一旦第 个 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 对网络抽象层的改造,收益是立竿见影的:
- 核心逻辑彻底解耦:消除了
initServer()和事件处理中分散的if (is_tls) ... else if (is_unix)特化硬编码,完全基于虚函数表进行多态分发。 - 插件化扩展能力:如果未来需要支持某种私有加密协议或新的传输协议,开发者只需实现一套
ConnectionType虚函数表并注册,核心网络监听主循环不需要做任何改动。






