Qt使用Keepalive机制实现断线重连

[upl-image-preview url=https://caw-guaik-io.oss-cn-shanghai.aliyuncs.com/2021-12-23/1640238038-240349-what-is-tcp-ip-hero.jpg]

QTcpSocket能够检测到Socket的连接与断开状态,并触发相关信号,我们只需要关联信号与槽就能够知道连接状态。

connect(&m_socket, &QTcpSocket::connected, this, &TcpClient::onConnected, Qt::QueuedConnection);
connect(&m_socket, &QTcpSocket::disconnected, this, &TcpClient::onDisconnected, Qt::QueuedConnection);

还有一些特殊情况是无法触发QTcpSocket::disconnected信号,比如说:网线突然拔了、对端设备突然爆掉了等。这类情况由于对端socket未正常调用close()方法而导致的。

我们可以定义一个心跳包去定期检查对端的存活状态,这种做法在协议还未指定的初期是比较适合的,客户端与服务端制定一套心跳请求与应答机制来判断对端的存活状态。但是往往下位机的程序已经存在(开发者不愿意修改或增加现有协议),这时候如何能够在特殊情况下检测到网络断开呢?

主角登场:Keepalive机制

keepalive简介(摘自维基百科)

传输控制协议(TCP)存活包为可选特性,且默认关闭。存活包内没有数据。在以太网网络中,存活包的大小为最小长度的几帧(64字节)。协议中,还有三个与存活包相关的参数:

  • 存活时长(英语:Keepalive time)即空闲时,两次传输存活包的持续时间。TCP存活包时长可手动配置,默认不少于2个小时。
  • 存活间隔(英语:Keepalive interval)即未收到上个存活包时,两次连续传输存活包的时间间隔。
  • 存活重试次数(英语:Keepalive retry)即在判断远程主机不可用前的发送存活包次数。当两个主机透过TCP/IP协议相连时,TCP存活包可用于判断连接是否可用,并按需中断。
    多数支持TCP协议的主机也同时支持TCP存活包。每个主机按一定周期向其他主机发送TCP包来请求回应。若发送主机未收到特定主机的回应(ACK),则将从发送主机一侧中断连接。 若其他主机在连接关闭后发送TCP存活包,关闭连接的一方将发送RST包来表明旧连接已不可用。其他主机将关闭它一侧的连接以新建连接。

空闲的TCP连接通常会隔每45秒或60秒发送一次存活包。在未连续收到三次ACK包时,连接将中断。此行为因主机而异,如默认情况下的Windows主机将在7200000ms(2小时)后发送首个存活包,随后再以1000ms的间隔发送5个存活包。若任意存活包未收到回应,连接将被中断。

Qt开启Keepalive(Linux与Windows)

#include "keepalive.h"
#include <QObject>

const int keepalive = 1;    // 开启keepalive属性
const int keepidle = 5;     // 如果连接在5秒内没有任何数据来往则进行探测
const int keepinterval = 3; // 探测时发包的时间间隔为3秒
const int keepcount = 3;    // 尝试探测的次数, 如果第一次探测包就收到响应,则不在继续探测

#if defined (Q_OS_LINUX) || defined (Q_OS_MACOS)
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/tcp.h>
#include <netinet/in.h>

// 开启TCP心跳检测机制
int enableKeepalive(int fd) {
    if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)) < 0) return -1;
    if (setsockopt(fd, SOL_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle)) < 0) return -1;
    if (setsockopt(fd, SOL_TCP, TCP_KEEPINTVL, &keepinterval, sizeof(keepinterval)) < 0) return -1;
    if (setsockopt(fd, SOL_TCP, TCP_KEEPCNT, &keepcount, sizeof(keepcount)) < 0) return -1;
    return 0;
}
#elif defined (Q_OS_WIN)
#include <winsock2.h>

#define SIO_KEEPALIVE_VALS _WSAIOW(IOC_VENDOR,4)

struct tcp_keepalive {
    unsigned long onoff;
    unsigned long keepalivetime;
    unsigned long keepaliveinterval;
};

int enableKeepalive(int fd) {
    if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (char*)&keepalive, sizeof(keepalive)) < 0) return -1;

    struct tcp_keepalive in_keep_alive;
    memset(&in_keep_alive, 0, sizeof(in_keep_alive));
    unsigned long ul_in_len = sizeof(struct tcp_keepalive);
    struct tcp_keepalive out_keep_alive;
    memset(&out_keep_alive, 0, sizeof(out_keep_alive));
    unsigned long ul_out_len = sizeof(struct tcp_keepalive);
    unsigned long ul_bytes_return = 0;

    in_keep_alive.onoff = 1;                                // 打开keepalive
    in_keep_alive.keepaliveinterval = keepinterval * 1000;  // 发送keepalive心跳时间间隔-单位为毫秒
    in_keep_alive.keepalivetime = keepidle * 1000;          // 多长时间没有报文开始发送keepalive心跳包-单位为毫秒

    if (WSAIoctl(fd, SIO_KEEPALIVE_VALS, (LPVOID)&in_keep_alive, ul_in_len,
                               (LPVOID)&out_keep_alive, ul_out_len, &ul_bytes_return, NULL, NULL) < 0) return -1;

    return 0;
}
#else
int enableKeepalive(int fd) {
    Q_UNUSED(fd);
    return -1;
}
#endif

Socket文件描述符的获取

QAbstractSocket::socketDescriptor() ,在socket连接成功后可使通过m_socket->socketDescriptor();获取到QTcpSocket的文件描述符(FD),失败时返回-1,这边获取到的fd可以提供给int enableKeepalive(int fd);作为参数用于启用keepalive

文档介绍:

Returns the native socket descriptor of the QAbstractSocket object if this is available; otherwise returns -1.

If the socket is using QNetworkProxy, the returned descriptor may not be usable with native socket functions.

The socket descriptor is not available when QAbstractSocket is in UnconnectedState.

断线重连

在开启keepalive后我们就可以在链路断开时触发QTcpSocket::disconnected信号了,这时候我们只需要开启一个定时器去试试检查QTcpSocket的状态,当状态为QAbstractSocket::UnconnectedState时清理QTcpSocket资源并尝试重新与服务端建立连接即可。

// 状态检查定时器槽函数
void TcpClient::onCheckState()
{
    switch (m_socket.state()) {
    case QAbstractSocket::UnconnectedState:
        m_socket.close();
        m_socket.abort();
        m_socket.connectToHost(m_remoteIp, m_remotePort);
        break;

    case QAbstractSocket::HostLookupState:
        break;
    case QAbstractSocket::ConnectingState:
        break;
    case QAbstractSocket::ConnectedState:
        break;
    case QAbstractSocket::BoundState:
        break;
    case QAbstractSocket::ListeningState:
        break;
    case QAbstractSocket::ClosingState:
        break;
    }
}
  • 1
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值