为什么需要心跳检测
当通过TCP建立连接的服务端A与客户端B,客户端B突然异常退出,比如断网、断电、程序异常终止等等,服务端A无法知道客户端B的非正常断开,可能仍然保持着与客户端的连接状态,仍会等待接收来自客户端B的消息,这会占用服务器资源(如内存),尤其是在有大量客户端连接的情况下,造成资源浪费。
这是因为TCP建立连接都是通过发送数据实现的,即三次握手和四次挥手,当客户端异常退出时,是无法发送数据给服务端,无法通知服务端断开连接,导致服务端误以为仍与客户端保持健康连接。
因此需要心跳检测来监测连接状态。心跳检测允许服务器或另一端的应用程序定期检查客户端的存活状态。如果客户端不再响应心跳包,服务器可以采取适当的措施,如关闭连接、释放资源或通知其他相关方。
什么是心跳检测
心跳检测通常通过发送心跳包(也称为心跳消息或保持活动消息)来实现。这是一种小型的数据包或消息,它在连接上定期发送,用于验证连接的存活状态。
客户端发起心跳:客户端每隔一段时间发送心跳包,如果客户端在设定时间没有收到服务端返回的应答消息,经重试机制后,客户端认为服务器异常,断开与服务器的连接。
服务器发起心跳:服务器设置超时机制,主动向客户端发送心跳包,客户端未在规定时间内返回应答包导致超时,服务器将认为客户端异常,关闭与客户端的连接。
心跳检测步骤
- 服务器设置超时机制,服务器每隔一个时间间隔发送一个心跳包给服务器客户端
- 客户端接收到检测包,应该回应一个应答包给服务器端
- 如果服务器端收到客户端的应答包,则说明客户端正常,重置超时倒计时
- 在一定时间内未收到来自客户端的应答包,则说明客户端断开了连接
心跳包分类
1、应用层自己实现的心跳包
服务器设置超时机制,向客户端发送一个心跳包,然后启动一个低级别的线程,在该线程中不断检测客户端的回应,一段时间内未收到回应,则判定客户端掉线,同时在客户端来说,一定时间内收不到来自服务器的心跳包,则认为服务器异常,也将主动断开连接。这种方式将会在服务器端使用两个线程维护一个客户端,当客户端数量增多时,会成倍增加资源消耗,增大服务器负载。
另一种方式可以在心跳包中增加标志位,以区分该消息为心跳包还是正常消息交流,在心跳包发送频率较低的情况下将不会影响正常的消息交流,同时无需额外增加一个线程来维护心跳检测
2、TCP的保活机制KeepAlive
考虑到一个服务器会连接多个客户端,因此在应用层自己实现心跳包,代码较多且复杂。TCP协议层内置了KeepAlive功能来实现心跳功能,且发送的是数据长度为零的零心跳包。
服务端或是客户端,一方开启KeepAlive功能后,将会自动在规定时间内向对方发送心跳包, 而另一方在收到心跳包后就会自动回复,以告诉对方仍正常连接在线。
然而开启KeepAlive功能将会牺牲额外的宽带和流量,因此TCP协议层默认关闭了KeepAlive功 能。此外,如果超时时间设置的过短,将会因为短暂的网络波动而断开健康的TCP连接。
实例
实际上,当客户端或是服务器异常退出时,另一端的接收函数会出现两种情况,一种recv将会因为连接被破坏一直收到最后一次消息相同内容,另一种情况则是连接被破坏后,被检测出,recv将会返回错误值,-1或0。
通过返回值,可以很好的判断出连接异常,但是当返回值无法判断时,则需要引入心跳检测。以下示例中,服务器与客户端设置超时机制,服务器在规定时间内接收不到来自客户端的消息,将会判断为客户端异常而断开连接,而在客户端中,则是启用了两个线程,一个专门接收来自服务器的消息,再心跳检测时,一定时间内接收不到应答包将会判断为服务器异常,从而断开连接,另一个则是向服务器发送心跳包,来重置服务器的超时时间。当客户端发送心跳包给服务器后,服务器也将返回一个应答包给客户端,以表明服务器仍正常连接。
以下代码的主要核心如图所示:
//Server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <signal.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/time.h>
int acceptSocket = 0;
struct Message
{
int flag;
char content[1024];
};
struct TcpServer
{
int sock;
};
struct StdThread
{
pthread_t threadID;
};
//维护客户端线程
void* thread_handle(void* arg)
{
//acceptSock
int sock = *(int*)arg;
struct timeval timeout;
timeout.tv_sec = 10; // 设置超时时间为5秒
timeout.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
while (1)
{
struct Message m;
int recv_value;
recv_value = recv(sock, &m, sizeof(struct Message), 0);
printf("%d\n", recv_value);
if (recv_value == -1 || recv_value == 0)
{
printf("客户端异常退出!\n");
return NULL;
}
if (m.flag == 1)
{
if (send(sock, &m, sizeof(struct Message), 0) < 0)
{
perror("TcpServerSend send");
return NULL;
}
printf("heart check!\n");
}
else
printf("content from client: %s\n", m.content);
}
}
int main()
{
struct TcpServer* s = (struct TcpServer*)malloc(sizeof(struct TcpServer));
if (s == NULL)
{
printf("InitTcpServer error!\n");
free(s);
return -1;
}
s->sock = socket(AF_INET, SOCK_STREAM, 0);
if (s->sock < 0)
{
perror("InitTcpServer sock");
free(s);
return -1;
}
int isUse = 1;
//允许端口和地址复用
if (setsockopt(s->sock, SOL_SOCKET, SO_REUSEADDR, &isUse, 4) != 0)
{
perror("InitTcpServer setsockopt");
free(s);
return -1;
}
struct sockaddr_in addr;
//协议族
addr.sin_family = AF_INET;
//端口号
addr.sin_port = htons(8199);
//IP地址
addr.sin_addr.s_addr = inet_addr("192.168.254.133");
if (bind(s->sock, (struct sockaddr*)&addr, sizeof(addr)) != 0)
{
perror("InitTcpServer bind");
free(s);
return -1;
}
if (listen(s->sock, 10) < 0)
{
perror("InitTcpServer listen");
free(s);
return -1;
}
while (1)
{
//等待客户端接入
struct sockaddr_in ClientAddr;
socklen_t len = 1;
int acceptSock = accept(s->sock, (struct sockaddr*)&ClientAddr, &len);
if (acceptSock < 0)
{
perror("TcpServerAccept accept");
free(s);
return -1;
}
struct StdThread* t = (struct StdThread*)malloc(sizeof(struct StdThread));
if (t == NULL)
{
printf("InitThread malloc error!\n");
free(s);
return -1;
}
if (pthread_create(&t->threadID, NULL, thread_handle, &acceptSock) != 0)
{
printf("InitThread pthread_create error!\n");
free(s);
return -1;
}
if (pthread_detach(t->threadID) != 0)
{
printf("DetchThread error!\n");
}
}
return 0;
}
//Client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <signal.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <sys/time.h>
struct Message
{
int flag;
char content[1024];
};
struct TcpClient
{
int sock;
};
struct StdThread
{
pthread_t threadID;
};
//创建接收服务器消息线程
void* thread_handle(void* arg)
{
struct TcpClient* c_temp = (struct TcpClient*)arg;
struct timeval timeout;
timeout.tv_sec = 5; // 设置超时时间为5秒
timeout.tv_usec = 0;
setsockopt(c_temp->sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
while (1)
{
struct Message message;
int recv_value;
recv_value = recv(c_temp->sock, &message, sizeof(struct Message), 0);
printf("%d\n", recv_value);
if (recv_value == -1 || recv_value == 0)
{
printf("服务器异常退出!\n");
exit(0);
}
if (message.flag == 1)
{
printf("heart check!\n");
}
else
printf("Message Content: %s\n", message.content);
}
}
//心跳检测线程
void* thread_heart(void* arg)
{
struct TcpClient* c_temp = (struct TcpClient*)arg;
struct Message temp_msg;
temp_msg.flag = 1;
while (1)
{
if (send(c_temp->sock, &temp_msg, sizeof(temp_msg), 0) < 0)
{
perror("TcpServerSend send");
exit(0);
}
sleep(3);
}
}
int main()
{
struct TcpClient* c = (struct TcpClient*)malloc(sizeof(struct TcpClient));
if (c == NULL)
{
printf("InitTcpClient error!\n");
free(c);
return -1;
}
c->sock = socket(AF_INET, SOCK_STREAM, 0);
if (c->sock < 0)
{
perror("InitTcpClient sock");
free(c);
return -1;
}
struct sockaddr_in addr;
//协议族
addr.sin_family = AF_INET;
//端口号
addr.sin_port = htons(8199);
//IP地址
addr.sin_addr.s_addr = inet_addr("192.168.254.133");
if (connect(c->sock, (struct sockaddr*)&addr, sizeof(addr)) != 0)
{
perror("InitTcpClient connect");
}
//创建接收服务器消息线程
struct StdThread* t = (struct StdThread*)malloc(sizeof(struct StdThread));
if (t == NULL)
{
printf("InitThread malloc error!\n");
free(c);
return -1;
}
if (pthread_create(&t->threadID, NULL, thread_handle, c) != 0)
{
printf("InitThread pthread_create error!\n");
free(c);
return -1;
}
if (pthread_detach(t->threadID) != 0)
{
printf("DetchThread error!\n");
}
//创建心跳检测线程
struct StdThread* t_heart = (struct StdThread*)malloc(sizeof(struct StdThread));
if (t_heart == NULL)
{
printf("InitThread malloc error!\n");
free(c);
free(t);
return -1;
}
if (pthread_create(&t_heart->threadID, NULL, thread_heart, c) != 0)
{
printf("InitThread pthread_create error!\n");
free(t);
free(c);
return -1;
}
if (pthread_detach(t_heart->threadID) != 0)
{
printf("DetchThread error!\n");
}
struct Message m;
while (1)
{
printf("please input your message :");
scanf("%s", m.content);
while (getchar() != '\n');
if (send(c->sock, &m, sizeof(m), 0) < 0)
{
perror("TcpServerSend send");
return -1;
}
}
return 0;
}