1. 心跳机制简介
在长连接下,可能很长一段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际情况中,如果中间节点出现什么故障是难以知道的。更致命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。这个时候,就可以使用心跳包,来维持长连接以及保活
心跳机制就是每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息如果服务端几分钟内没有收到客户端信息则视客户端断开。发包方可以是客户也可以是服务端,具体看哪边实现更方便合理
(资料图)
心跳包的发送通常有以下两种技术:
1、应用层自已实现的心跳包:
由应用程序自己发送心跳包来检测连接是否正常,服务器每隔一定时间向客户端发送一个短小的数据包,然后启动一个线程,在线程中不断检测客户端的回应, 如果在一定时间内没有收到客户端的回应,即认为客户端已经掉线;同样,如果客户端在一定时间内没有收到服务器的心跳包,则认为连接不可用
2、使用SO_KEEPALIVE套接字选项:
在TCP的机制里面,本身是存在有心跳包的机制的,也就是TCP的选项. 不论是服务端还是客户端,一方开启KeepAlive功能后,就会自动在规定时间内向对方发送心跳包, 而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线
2. 心跳检测实现
2.1 setsockopt函数介绍
函数功能:设置与某个套接字关联的选项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。当操作套接字选项时,选项位于的层和选项的名称必须给出。为了操作套接字层的选项,应该将层的值指定为SOL_SOCKET。为了操作其它层的选项,控制选项的合适协议号必须给出。例如,为了表示一个选项由TCP协议解析,层应该设定为协议号TCP
函数原型:int setsockopt(int sock, //将要被设置选项的套接字int level, //选项所在的协议层int optname, //需要访问的选项名const void *optval, //指向包含新选项值得缓冲socklen_t optlen)//现选项的长度返 回 值:成功返回0;失败返回-1
参数(level)详细说明:level是指定控制套接字的层次,可以取如下三种值
SOL_SOCKET:通用套接字选项
选项名称(optname)
说明
数据类型========================================================================SO_BROADCAST
允许发送广播数据
intSO_DEBUG
允许调试
intSO_DONTROUTE
不查找
intSO_ERROR
获得套接字错误
intSO_KEEPALIVE
保持连接
intSO_LINGER
延迟关闭连接
struct lingerSO_OOBINLINE
带外数据放入正常数据流
intSO_RCVBUF
接收缓冲区大小
intSO_SNDBUF
发送缓冲区大小
intSO_RCVLOWAT
接收缓冲区下限
intSO_SNDLOWAT
发送缓冲区下限
intSO_RCVTIMEO
接收超时
struct timevalSO_SNDTIMEO
发送超时
struct timevalSO_REUSERADDR
允许重用本地地址和端口
intSO_TYPE
获得套接字类型
intSO_BSDCOMPAT
与BSD系统兼容
int========================================================================
IPPROTO_IP:IP选项
选项名称(optname)
说明
数据类型========================================================================IP_HDRINCL
在数据包中包含IP首部
intIP_OPTINOS
IP首部选项
intIP_TOS
类型IP_TTL
生存时间
int========================================================================
IPPROTO_TCP:TCP选项
选项名称(optname)
说明
数据类型========================================================================TCP_MAXSEG
TCP最大数据段的大小
intTCP_NODELAY
不使用Nagle算法
int========================================================================
2.2 心跳机制的实现
该实例实现的功能:在TCP客户端代码中加入心跳机制,使服务端在断网重连(与客户端)后,能自动保持连接
参考Socket API编程优化一文,在该文的工程源码基础上进行修改keepalive宏开关:将opt.h文件中的LWIP_TCP_KEEPALIVE宏定义开启
/** * LWIP_TCP_KEEPALIVE==1: Enable TCP_KEEPIDLE, TCP_KEEPINTVL and TCP_KEEPCNT * options processing. Note that TCP_KEEPIDLE and TCP_KEEPINTVL have to be set * in seconds. (does not require sockets.c, and will affect tcp.c) */#if !defined LWIP_TCP_KEEPALIVE || defined __DOXYGEN__#define LWIP_TCP_KEEPALIVE 1#endif
setsockopt参数含义:sockets.h文件中可以看到如下setsockopt参数的设置
#define SO_KEEPALIVE 0x0008 /* 保持连接 */ #define TCP_KEEPIDLE 0x03 /* 发送心跳空闲周期 S*/ #define TCP_KEEPINTVL 0x04 /* 发送心跳间隔 S */ #define TCP_KEEPCNT 0x05 /* 心跳重发次数 */
在工程中创建tcp_keepalive.c和对应的头文件
#include "socket_tcp_server.h"#include "tcp_keepalive.h"#include "socket_wrap.h"#include "ctype.h"#include "FreeRTOS.h"#include "task.h"static char ReadBuff[BUFF_SIZE];void vTcpKeepaliveTask(void){ int cfd, n, i, ret; struct sockaddr_in server_addr; int so_keepalive_val = 1; int tcp_keepalive_idle = 3; int tcp_keepalive_intvl = 3; int tcp_keepalive_cnt = 3; int tcp_nodelay = 1;again: //创建socket cfd = Socket(AF_INET, SOCK_STREAM, 0); //使能socket层的心跳检测 setsockopt(cfd, SOL_SOCKET, SO_KEEPALIVE, &so_keepalive_val, sizeof(int)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); /*连接到服务器(connect是一个阻塞接口,内部要完成TCP的三次握手; 里面有超时机制,所以需要等一段时间,才能重新连接到服务器)*/ ret = Connect(cfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); if(ret < 0){//100ms去连接一次服务器vTaskDelay(100);goto again; } //配置心跳检测参数 默认参数时间很长 setsockopt(cfd, IPPROTO_TCP, TCP_KEEPIDLE, &tcp_keepalive_idle, sizeof(int)); setsockopt(cfd, IPPROTO_TCP, TCP_KEEPINTVL, &tcp_keepalive_intvl, sizeof(int)); setsockopt(cfd, IPPROTO_TCP, TCP_KEEPCNT, &tcp_keepalive_cnt, sizeof(int)); setsockopt(cfd, IPPROTO_TCP, TCP_NODELAY, &tcp_nodelay, sizeof(int)); printf("server is connect ok\r\n"); while(1){//等待服务器发送数据n = Read(cfd, ReadBuff, BUFF_SIZE);if(n <= 0){ goto again;}//进行大小写转换for(i = 0; i < n; i++){ ReadBuff[i] = toupper(ReadBuff[i]);}//写回服务器n = Write(cfd, ReadBuff, n);if(n <= 0){ goto again;} }}
在freertos.c文件中的默认任务里面添加代码
void StartDefaultTask(void const * argument){ /* init code for LWIP */ MX_LWIP_Init(); /* USER CODE BEGIN StartDefaultTask */ printf("TCP keepalive test!\r\n"); /* Infinite loop */ for(;;){ vTcpKeepaliveTask();osDelay(100); } /* USER CODE END StartDefaultTask */}