1、为何需要心跳包

问大家一个问题,如果客户端和服务端长时间没有相互发送数据的话,那么我们怎么来判断这个连接是否存在的呢?有些人可能很自然地说直接send一下不就可以了,确实可以这样进行判断,那么我们发送的时候可以选择发送任何东西,所以一般都是发送一个空包,这个就是心跳包。

跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。

所以说心跳包是一种保证服务端和客户端持续连接的一种机制,心跳包可以服务端发到客户端,当然也可以客户端发到服务端,但是一般出于效率的考虑,都是选择客户端发到服务端。当然,发送心跳包我们必须另外开一个线程,不能和发送正常的数据的线程混在一起。至于多久发送一次,可以根据自己的业务情况来判断,一般在while循环里加个sleep()函数就可以。

2、心跳包的具体实现

首先,还是先不扯其他的太多理论知识,我先扔出代码,然后结合代码讲解心跳包原理,本人是比较喜欢这种学习方式,带着疑问去学习,如果大家不习惯的话,可以先跳过以下的代码,先看代码下方的讲解部分。

2.1、服务端代码

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <vector>
#include <map>
using namespace std;

#define HEART_COUNT 5
#define BUF_SIZE 512
#define ERR_EXIT(m)         \
	do                      \
	{                       \
		perror(m);          \
		exit(EXIT_FAILURE); \
	} while (0)

typedef map<int, pair<string, int>> FDMAPIP;

enum Type
{
	HEART,
	OTHER
};

struct PACKET_HEAD
{
	Type type;
	int length;
};

//支线程传递的参数结构体
struct myparam
{
	FDMAPIP *mmap;
};

//心跳线程
void *heart_handler(void *arg)
{
	printf("the heart-beat thread started\n");
	struct myparam *param = (struct myparam *)arg;
	//Server *s = (Server *)arg;
	while (1)
	{
		FDMAPIP::iterator it = param->mmap->begin();
		for (; it != param->mmap->end();)
		{
			// 3s*5没有收到心跳包,判定客户端掉线
			if (it->second.second == HEART_COUNT)
			{
				printf("The client %s has be offline.\n", it->second.first.c_str());

				int fd = it->first;
				close(fd);				 // 关闭该连接
				param->mmap->erase(it++); // 从map中移除该记录
			}
			else if (it->second.second < HEART_COUNT && it->second.second >= 0)
			{
				it->second.second += 1;
				++it;
			}
			else
			{
				++it;
			}
		}
		sleep(3); // 定时三秒
	}
}

int main()
{
	//创建套接字
	int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (m_sockfd < 0)
	{
		ERR_EXIT("create socket fail");
	}

	//初始化socket元素
	struct sockaddr_in server_addr;
	int server_len = sizeof(server_addr);
	memset(&server_addr, 0, server_len);

	server_addr.sin_family = AF_INET;
	//server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用这个写法也可以
	server_addr.sin_addr.s_addr = INADDR_ANY;
	server_addr.sin_port = htons(39002);

	//绑定文件描述符和服务器的ip和端口号
	int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len);
	if (m_bindfd < 0)
	{
		ERR_EXIT("bind ip and port fail");
	}

	//进入监听状态,等待用户发起请求
	int m_listenfd = listen(m_sockfd, 20);
	if (m_listenfd < 0)
	{
		ERR_EXIT("listen client fail");
	}

	//定义客户端的套接字,这里返回一个新的套接字,后面通信时,就用这个m_connfd进行通信
	struct sockaddr_in client_addr;
	socklen_t client_len = sizeof(client_addr);
	int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);

	printf("client accept success\n");

	string ip(inet_ntoa(client_addr.sin_addr)); // 获取客户端IP

	// 记录连接的客户端fd--><ip, count>,暂时就一个
	FDMAPIP mmap;
	mmap.insert(make_pair(m_connfd, make_pair(ip, 0)));

	struct myparam param;
	param.mmap = &mmap;

	// 创建心跳检测线程
	pthread_t id;
	int ret = pthread_create(&id, NULL, heart_handler, ¶m);
	if (ret != 0)
	{
		printf("can't create heart-beat thread.\n");
	}

	//接收客户端数据,并相应
	char buffer[BUF_SIZE];
	while (1)
	{
		if (m_connfd < 0)
		{
			m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
			printf("client accept success again!!!\n");
		}

		PACKET_HEAD head;
		int recv_len = recv(m_connfd, &head, sizeof(head), 0); // 先接受包头
		if (recv_len <= 0)
		{
			close(m_connfd);
			m_connfd = -1;
			printf("client head lose connection!!!\n");
			continue;
		}

		if (head.type == HEART)
		{
			mmap[m_connfd].second = 0;
			printf("receive heart beat from client.\n");
		}
		else
		{
			//接收数据部分
		}
	}

	//关闭套接字
	close(m_connfd);
	close(m_sockfd);

	printf("server socket closed!!!\n");

	return 0;
}

2.2、客户端代码

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <vector>
#include <map>
using namespace std;


#define BUF_SIZE 512
#define ERR_EXIT(m)         \
	do                      \
	{                       \
		perror(m);          \
		exit(EXIT_FAILURE); \
	} while (0)

enum Type
{
    HEART,
    OTHER
};

struct PACKET_HEAD
{
    Type type;
    int length;
};

//线程传递的参数结构体
struct myparam   
{   
    int fd;  
};

void *send_heart(void *arg)
{
    printf("the heartbeat sending thread started.\n");
    struct myparam *param = (struct myparam *)arg;
    int count = 0; // 测试
    while (1)
    {
        PACKET_HEAD head;
		//发送心跳包
        head.type = HEART;
        head.length = 0;
        send(param->fd, &head, sizeof(head), 0);
		// 定时3秒,这个可以根据业务需求来设定
        sleep(3); 
    }
}

int main()
{
	//创建套接字
	int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (m_sockfd < 0)
	{
		ERR_EXIT("create socket fail");
	}

	//服务器的ip为本地,端口号
	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = inet_addr("81.68.140.74");
	server_addr.sin_port = htons(39002);

	//向服务器发送连接请求
	int m_connectfd = connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
	if (m_connectfd < 0)
	{
		ERR_EXIT("connect server fail");
	}

	struct myparam param;
	param.fd = m_sockfd;
	pthread_t id;
    int ret = pthread_create(&id, NULL, send_heart, ¶m);
    if (ret != 0)
    {
		printf("create thread fail.\n");
    }

	//发送并接收数据
	char buffer[BUF_SIZE] = "asdfg";
	int len = strlen(buffer);
	while (1)
	{
		// 发送数据部分;
		
	}

	//断开连接
	close(m_sockfd);

	printf("client socket closed!!!\n");

	return 0;
}

代码大部分都和原有的结构相同,有点不同的就是客户端在完成连接请求之后就利用pthread_create函数开启了一个线程,然后在这个线程当中每隔3秒发送一个空包到服务端。同样的服务端也是在完成连接之后开启了另一个线程,而在这个线程当中主要就是判断如果在规定时间来没有收到客户端发来的心跳包,那么就断开和客户端的连接,并处理一些断开后的事情。

说实话,心跳包的代码部分并没有很难理解的地方,主要还是在思路这一方面,掌握了思路,代码都很容易实现。