libhv提供了设置心跳和保活机制的接口
HV_EXPORT void hio_set_heartbeat(hio_t* io, int interval_ms, hio_send_heartbeat_fn fn);
HV_EXPORT void hio_set_keepalive_timeout(hio_t* io, int timeout_ms DEFAULT(HIO_DEFAULT_KEEPALIVE_TIMEOUT));
在分析源码之前,先说下心跳机制的重要意义,首先,通过检测心跳可以处理以下这些情况(摘抄自陈硕大神的《Linux 多线程服务端编程》):
- 如果操作系统崩溃导致机器重启,没有机会发送FIN分节
- 服务器硬件故障导致机器重启,也没有机会发送FIN分节
- 并发连接数很高时,操作系统或进程如果重启,可能没有机会断开全部连接。换句话说,FIN分节可能出现丢包,但这时没有机会重试
- 网络故障,连接双方得知这个情况的唯一方案是检测心跳超时
第二,心跳除了说明应用程序还活着,更重要的是表明应用程序还能正常工作。
当然了,心跳也可以发送与业务相关的信息,不仅仅用来判断连接状态。
--------------------------------------------下面开始源码分析--------------------------------------------------------
上面两个对外提供的接口很简单,只是设置io的相关成员变量
void hio_set_keepalive_timeout(hio_t* io, int timeout_ms) {
io->keepalive_timeout = timeout_ms;
}
void hio_set_heartbeat(hio_t* io, int interval_ms, hio_send_heartbeat_fn fn) {
io->heartbeat_interval = interval_ms;
io->heartbeat_fn = fn;
}
实际生效是在其他地方设置的,第一个是在__accept_cb接口,即服务端accept成功后,调用注册回调函数的位置:
static void __accept_cb(hio_t* io) {
if (io->accept_cb) {
io->accept_cb(io);
}
if (io->keepalive_timeout > 0) {
io->keepalive_timer = htimer_add(io->loop, __keepalive_timeout_cb, io->keepalive_timeout, 1);
io->keepalive_timer->privdata = io;
}
if (io->heartbeat_interval > 0) {
io->heartbeat_timer = htimer_add(io->loop, __heartbeat_timer_cb, io->heartbeat_interval, INFINITE);
io->heartbeat_timer->privdata = io;
}
}
上面的accept_cb就是用户注册的回调函数,在执行完accept_cb后,确认是否设置了keepalive和heartbeat。所以对于服务端而言,如果要设置keepalive或者heartbeat,在注册的accept回调函数中调用上面的两个接口就可以了。
第二个是在__connect_cb,即客户端connect成功后,调用注册回调函数的位置:
static void __connect_cb(hio_t* io) {
if (io->connect_timer) {
htimer_del(io->connect_timer);
io->connect_timer = NULL;
io->connect_timeout = 0;
}
if (io->connect_cb) {
io->connect_cb(io);
}
if (io->keepalive_timeout > 0) {
io->keepalive_timer = htimer_add(io->loop, __keepalive_timeout_cb, io->keepalive_timeout, 1);
io->keepalive_timer->privdata = io;
}
if (io->heartbeat_interval > 0) {
io->heartbeat_timer = htimer_add(io->loop, __heartbeat_timer_cb, io->heartbeat_interval, INFINITE);
io->heartbeat_timer->privdata = io;
}
}
上面的connect_cb就是用户注册的回调函数,所以客户端想要设置keepalive或者heartbeat,可以在注册的connect回调函数中调用上面的两个接口。
心跳的实现比较简单,设置一个无限次的定时器,每隔heartbeat_interval时间调用__heartbeat_timer_cb:
static void __heartbeat_timer_cb(htimer_t* timer) {
hio_t* io = (hio_t*)timer->privdata;
if (io && io->heartbeat_fn) {
io->heartbeat_fn(io);
}
}
__heartbeat_timer_cb执行用户注册的回调函数
keepalive相对复杂一些,当keepalive设置的定时器到期时,会调用__keepalive_timeout_cb,而该接口则会关闭连接:
static void __keepalive_timeout_cb(htimer_t* timer) {
hio_t* io = (hio_t*)timer->privdata;
if (io) {
char localaddrstr[SOCKADDR_STRLEN] = {0};
char peeraddrstr[SOCKADDR_STRLEN] = {0};
hlogw("keepalive timeout [%s] <=> [%s]",
SOCKADDR_STR(io->localaddr, localaddrstr),
SOCKADDR_STR(io->peeraddr, peeraddrstr));
io->error = ETIMEDOUT;
hio_close(io);
}
}
为了不触发keepalive,就必须在定时器到期前重置定时器,重置定时器的位置有两个,第一个是在读数据时调用__read_cb
static void __read_cb(hio_t* io, void* buf, int readbytes) {
// printd("> %.*s\n", readbytes, buf);
if (io->keepalive_timer) {
htimer_reset(io->keepalive_timer);
}
if (io->read_cb) {
// printd("read_cb------\n");
io->read_cb(io, buf, readbytes);
// printd("read_cb======\n");
}
}
__read_cb是在读到数据后,调用用户注册的读回调函数read_cb。在调用读回调前,如果keepalive存在,重置定时器。
第二个是在写数据时,调用__write_cb
static void __write_cb(hio_t* io, const void* buf, int writebytes) {
// printd("< %.*s\n", writebytes, buf);
if (io->keepalive_timer) {
htimer_reset(io->keepalive_timer);
}
if (io->write_cb) {
// printd("write_cb------\n");
io->write_cb(io, buf, writebytes);
// printd("write_cb======\n");
}
}
__write_cb是在写数据后,调用用户注册的写回调write_cb。在调用写回调前,如果keepalive存在,重置定时器。
根据上面的描述,可以看出,这里的keepalive就是在服务端和客户端之间既没有读也没有写一段时间后,断开连接,在libhv的HttpServer.cpp文件中,有对于keepalive机制的使用:
static void on_accept(hio_t* io) {
printd("on_accept connfd=%d\n", hio_fd(io));
hio_setcb_close(io, on_close);
hio_setcb_read(io, on_recv);
hio_read(io);
hio_set_keepalive_timeout(io, HIO_DEFAULT_KEEPALIVE_TIMEOUT);
// new HttpHandler
// delete on_close
HttpHandler* handler = new HttpHandler;
handler->service = (HttpService*)hevent_userdata(io);
handler->files = &s_filecache;
sockaddr_ip((sockaddr_u*)hio_peeraddr(io), handler->ip, sizeof(handler->ip));
handler->port = sockaddr_port((sockaddr_u*)hio_peeraddr(io));
hevent_set_userdata(io, handler);
}
在accept的回调中,设置了keepalive,当客户端与服务端HIO_DEFAULT_KEEPALIVE_TIMEOUT时间没有通信后,服务端会断开客户端的连接。