redis版本
redis6.0+
socket连接建立的要素
- 网络连接的建立离不开socket,socket在不同操作系统下有不同的接口,这里以epoll为例。
- socket连接建立关键点1:在指定的端口创建监听套接字,并且将该套接字通过epoll_ctl加入epoll_fd中。
- 关键点2:定时通过epoll_wait查看是否有连接请求过来,如果有则调用accpet创建client的套接字并且将其加入进程的套接字管理的数据结构中,以便后续查找和管理套接字。
- 关键点3:上层业务处理数据异常时,主动关闭下层socket。
- 关键点4:连接超时,主动关闭socket。
- 关键点5:socket发现对方的关闭请求,把业务层的所有相关数据一并清除并关闭。
redis中上述要素对应的操作
epoll接口封装
redis封装epoll的接口。在文件ae_epoll.c中, 对应关系如下
aeApiCreate ==> epoll_create
aeApiAddEvent ==> epoll_ctl (EPOLL_CTL_ADD, EPOLL_CTL_MOD)
aeApiDelEvent ==> epoll_ctl(EPOLL_CTL_MOD, EPOLL_CTL_DEL)
aeApiPoll ==> epoll_wait
接收请求并建立连接
- 创建监听套接字,并设置处理函数。在这之前先简要介绍redis的事件驱动框架。redis进程的主线程调用aeMain函数,在while循环中调用aeProcessEvents处理触发的事件。网络事件当然也会在其中被调用,大致框架代码如下:
void aeMian() {
while (!stop) {
aeProcessEvents()
}
}
void aeProcessEvents() {
events = aeApiPoll()
for (event in events) {
if (event.mask & AE_READ) {
event.read_handler(event)
}
if (event.mask & AE_WRITE) {
event.write_handler(event)
}
}
}
每个fd会绑定一个处理函数,当读写事件触发时,调用对应的处理函数。在server.c中,我们可以看到监听套接字绑定的处理函数是acceptTcpHandler.代码大致如下:
void initServer() {
// ......
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
{
serverPanic(
"Unrecoverable error creating server.ipfd file event.");
}
}
// ......
}
在accept过程中,会创建三个结构体:client, connection, aeFileEvent用于绑定文件描述符fd。最终调用的处理函数是clientAcceptHandler。最后附上调用关系的堆栈图:
连接关闭
首先附上连接关闭时的堆栈截图,由上面的分析可知,连接关闭必然会调用aeApiDelEvent,因此在此断点就可以通过堆栈看到调用关系。
读者可以去看下#7 beforeSleep中调用freeClientsInAsyncFreeQueue的代码。连接的关闭由主线程执行,并且是异步的关闭,在处理IO事件或者命令时会调用freeClientAsync函数将要关闭的连接加入server.clients_to_close队列中,然后由主线程去关闭。
主动关闭连接
当服务器认为该连接已经无效时,调用freeClientAsync,将该连接加入队列中,稍后统一关闭。这里列举几个可能调用freeClientAsync的地方:写数据失败,读数据失败。
socket读取异常关闭连接
通常在client主动关闭连接或者client出现异常时触发。通过在freeClientAsync设置断点,然后client主动关闭,查看堆栈调用图。
由此可知,当client主动关闭连接时,触发读事件,读的数据长度为0,说明连接关闭,则服务器这边也将连接关闭。
client超时时关闭连接
ps:如何找到redis超时关闭连接的调用?断点不是一个好选择,等待的时间太长了。首先通过Google知道redis可配置连接的超时时间,因此现在config.c中找到配置项对应的进程内的变量,搜索该变量被引用的地方,找到了clientsCronHandleTimeout函数,通过断点该函数,找到了调用的堆栈关系图。
由调用堆栈图可知,会创建一个定时事件在定时事件中检查连接上次交互的时间是否超过超时时间。(关于定时事件,在后面有机会再详谈)
总结
- 异步连接的关闭。好处:a、加快了命令处理的效率,降低延时,可以理解为削峰处理。b、将连接的关闭延后到统一处理,避免了可能存在的并发。
- 笔者曾分析过自己项目C++实现的网络底层库,也曾分析过Golang通过epoll实现的网络库。个人的理解是,无论何时,抓住底层的socket接口,就能很好理解上层框架的处理逻辑了。而且这些框架大同小异,归根结底是因为底层接口的性质决定了框架的结构。笔者后续再分析别的源码的网络库时,应该不会再专门写博客了。
- gdb断点分析源码是个非常好的手段,先找到最重要调用的底层接口,然后断点查看堆栈调用图,即可快速确定调用关系。
有疑问加站长微信联系(非本文作者)