TCP KeepAive机制
什么是KeepAlive机制
KeepAlive的意思是存活,用于监控两个设备之前的数据链路是否正常工作,防止链路中断的信息。所以TCP KeepAlive机制发生数据链路层,意思就是只要链路层连接正常,KeepAlive机制就正常运行。
TCP存活包是默认关闭的,存货包内没有数据。一般上,存货包的大小为最小长度的几帧(64字节)。协议中与存活包相关的参数:
- 存活时长即空闲时,两次传输存活包的持续时间。TCP存活包时长可手动配置,默认不少于2个小时。
- 存活间隔即未收到上个存活包时,两次连续传输存活包的时间间隔。
- 存活重试次数即在判断远程主机不可用前的发送存活包次数。当两个主机透过TCP/IP协议相连时,TCP存活包可用于判断连接是否可用,并按需中断。
KeepAlive机制开启
引用KeepAlive机制是为了跳过应用层的心跳帧设计,要开启KeepAlive机制必须要在程序中设置开启KeepAlive,使用Qt可以使用以下方法,但不能设置存活时间
QTcpSocket socket;
socket.setSocketOption(QAbstractSocket::KeepAliveOption, true);
socket.connectToHost("127.0.0.1", 8080);
也可以使用socket操作函数,利用socket描述符进行开启并设置超时时间和间隔:
#include <QTcpSocket>
#include <QApplication>
const int keepalive = 1;
const int keepidle = 5;
const int keepinterval = 3;
const int keepcount = 3;
#if defined (Q_OS_LINUX) || (Q_OS_MACOS)
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/tcp.h>
#include <netinet/in.h>
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>
#pragma comment (lib, "Ws2_32.lib")
#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;
in_keep_alive.keepaliveinterval = keepinterval * 1000;
in_keep_alive.keepalivetime = keepidle * 1000;
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
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QTcpSocket tcpSocket;
tcpSocket.connectToHost("127.0.0.1", 8080);
tcpSocket.waitForConnected();
enableKeepalive(tcpSocket.socketDescriptor());
return app.exec();
}
通过Wireshark工具可以抓取到Keep-Alive的相关通信,每隔5秒进行一次通信,只存在TCP/IP协议,不存在数据。
KeepAlive机制存在的问题
但KeepAlive机制存在于链路层,所以应用层发生问题并不会反映到链路层中。如果某端层异常退出而未完成四次挥手,数据链路层并会察觉到问题,KeepAlive的通信会继续保持。只要在物理层的异常切断连接,KeepAlive才会停止工作。
QTcpSocket跨线程通信总结
使用过QTcpSocket的小伙伴都了解,QTcpSocket是不支持跨线程进行读写操作,因为无法对读写缓存进行线程安全的处理,所以Qt限制了QTcpSocket的跨线程使用。
#include <QTcpSocket>
#include <thread>
#include <mutex>
class SocketTest
{
public:
SocketTest();
~SocketTest();
void sendTest();
private:
void handleSocketThread();
private:
QTcpSocket m_socket;
std::thread m_thread;
std::mutex m_mutex;
bool m_isRun;
};
#include "SocketTest.h"
#include <chrono>
SocketTest::SocketTest()
: m_isRun(true)
{
m_socket.connectToHost("127.0.0.1", 8080);
m_socket.waitForConnected();
m_thread = std::thread(&SocketTest::handleSocketThread, this);
}
SocketTest::~SocketTest()
{
m_isRun = false;
if (m_thread.joinable())
m_thread.join();
m_socket.disconnectFromHost();
}
void SocketTest::sendTest()
{
std::lock_guard<std::mutex> locker(m_mutex);
m_socket.write(QByteArray("MainThread Send"));
m_socket.waitForBytesWritten();
}
void SocketTest::handleSocketThread()
{
while (m_isRun)
{
{
std::lock_guard<std::mutex> locker(m_mutex);
m_socket.write(QByteArray("std::thread send"));
m_socket.waitForBytesWritten();
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
#include <QtCore/QCoreApplication>
#include "SocketTest.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
SocketTest socketTest;
socketTest.sendTest();
return a.exec();
}
运行上述代码,会出现以下警告,因为在跨线程进行write调用,但是通信是正常的。Qt会对跨线程进行QThread::currentThread的检查,发现在不同线程就会发出上面提到警告。
利用std::thread进行QSocket的跨线程读写
QTcpSocket不能跨线程的原因是对象创建在主线程,且跨的线程也在QThread创建的线程。利用std::thread将QTcpSocket创建于std::thread的线程中,然后将线程跨到std::thread中,就可以把上述的警告消除。
#include <QtCore/QCoreApplication>
#include "SocketTest.h"
void test()
{
SocketTest socketTest;
while (1) {
socketTest.sendTest();
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
std::thread thread(&test);
return a.exec();
}
运行上述代码,发现控制台不再发出警告,且通信正常。
使用std::thread跨线程操作存在的问题
服务器异常退出,客户端程序ASSERT
使用std::thread进行跨线程操作时候,如果服务端软件崩溃先断开连接,会导致以下问题: 这是因为服务器退出时,waitForBytesWritten被跨线程调用,导致socketEngine析构时尝试抛事件失败导致的。
在std::thread跨线程通信会导致,readyRead等依赖事件机制发送的信号失效
SocketTest::SocketTest()
: m_isRun(true)
{
m_socket.connectToHost("127.0.0.1", 8080);
m_socket.waitForConnected();
m_thread = std::thread(&SocketTest::handleSocketThread, this);
connect(&m_socket, &QTcpSocket::readyRead, this, []() {
qDebug() << "QTcpSocket Ready Read";
});
}
线程定时发送正常,但是调试软件发送信息无法触发readyRead信号。这是因为readyRead是通过事件驱动发送信号的,只有QThread才会有QEventLoop的事件处理,而std::thread没有,所以无法发送readyRead信号。(P.S. QTimer也是依赖事件驱动的,只能在QThread下才能使用)
在std::thread编程会导致QueuedConnection连接形式的槽函数失效
稍微修改SocketTest代码如下:
#include <QTcpSocket>
#include <thread>
#include <mutex>
#include <QThread>
class SocketTest : public QObject
{
Q_OBJECT
public:
SocketTest();
~SocketTest();
void sendTest();
Q_SIGNALS:
void signalTest();
private:
void handleSocketThread();
private:
QTcpSocket m_socket;
std::thread m_thread;
std::mutex m_mutex;
bool m_isRun;
};
#include "SocketTest.h"
#include <chrono>
#include <QThread>
SocketTest::SocketTest()
: m_isRun(true)
{
m_socket.connectToHost("127.0.0.1", 8080);
m_socket.waitForConnected();
m_thread = std::thread(&SocketTest::handleSocketThread, this);
connect(&m_socket, &QTcpSocket::readyRead, this, []() {
qDebug() << "QTcpSocket Ready Read";
});
connect(this, &SocketTest::signalTest, this, []() {
qDebug() << QThread::currentThreadId() << "signal test";
});
}
SocketTest::~SocketTest()
{
m_isRun = false;
if (m_thread.joinable())
m_thread.join();
m_socket.disconnectFromHost();
}
void SocketTest::sendTest()
{
qDebug() << QThread::currentThreadId() << "SocketTest::sendTest";
emit signalTest();
}
void SocketTest::handleSocketThread()
{
while (m_isRun)
{
qDebug() << QThread::currentThreadId() << "SocketTest::handleSocketThread";
emit signalTest();
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
如上图可以发现,只有在0x5b64的线程内发送信号,才会正常出发槽函数,0x5d88的线程发送并触发信号。是因为对于0x5d88信号发送是属于跨线程发送,会将其通过QCoreApplication::postEvent进行处理,SocketTest在std::thread中没有事件循环,所以不会触发该信号。 继续稍作修改,将SocketTest对象的m_thread对象修改QThread形式:
#include <QTcpSocket>
#include <thread>
#include <mutex>
#include <QThread>
class SocketTest : public QObject
{
Q_OBJECT
public:
SocketTest();
~SocketTest();
void sendTest();
Q_SIGNALS:
void signalTest();
private:
void handleSocketThread();
private:
QTcpSocket m_socket;
QThread m_thread;
std::mutex m_mutex;
bool m_isRun;
};
#include "SocketTest.h"
#include <chrono>
#include <QThread>
SocketTest::SocketTest()
: m_isRun(true)
{
m_socket.connectToHost("127.0.0.1", 8080);
m_socket.waitForConnected();
this->moveToThread(&m_thread);
connect(&m_thread, &QThread::started, this, &SocketTest::handleSocketThread);
m_thread.start();
connect(&m_socket, &QTcpSocket::readyRead, this, []() {
qDebug() << "QTcpSocket Ready Read";
});
connect(this, &SocketTest::signalTest, this, []() {
qDebug() << QThread::currentThreadId() << "signal test";
});
}
SocketTest::~SocketTest()
{
m_isRun = false;
if (m_thread.isRunning()) {
m_thread.quit();
m_thread.wait();
}
m_socket.disconnectFromHost();
}
void SocketTest::sendTest()
{
qDebug() << QThread::currentThreadId() << "SocketTest::sendTest";
emit signalTest();
}
void SocketTest::handleSocketThread()
{
while (m_isRun)
{
qDebug() << QThread::currentThreadId() << "SocketTest::handleSocketThread";
emit signalTest();
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
如上图所示,由于利用moveToThread,SocketTest对象与handleSocketThread处于同一线程,结果SocketTest::handleSocketThread的信号被触发时槽函数被调用。而在std::thread调用的Socket::sendTest触发的信号未调用槽函数。
总结
- 使用std::thread进行编程时,可能导致需要QEventLoop事件驱动的信号无法正常使用,如QTcpSocket的readyRead信号。
- 使用std::thread进行编程时,需要考虑跨线程的信号槽连接的recevier对象是否存在QThread中,不然会导致QueuedConnection连接的信号无法正常调用槽函数。
- 如果考虑使用Qt的信号槽机制,尽量不要使用std::thread进行线程的开启,可以是用QThread的实现。
- QTcpSocket可以进行跨线程操作,但需要注意线程安全的问题,且需要解决连接端异常关闭时,如果调用waitFor系列函数会导致程序ASSERT退出问题。
番外篇
解决QTcpSocket跨线程后导致异常退出问题
利用winsock的socket原生函数,结合select函数来模拟waitForBytesWritten。
void SocketTest::handleSocketThread()
{
while (m_isRun)
{
if (QAbstractSocket::ConnectedState == m_socket.state()) {
std::lock_guard<std::mutex> locker(m_mutex);
m_socket.write(QByteArray("std::thread send"));
fd_set fds;
int ret = 0;
memset(&fds, 0, sizeof(fd_set));
fds.fd_count = 1;
fds.fd_array[0] = (SOCKET)m_socket.socketDescriptor();
struct timeval tv;
int timeout = 100;
tv.tv_sec = timeout / 1000;
tv.tv_usec = (timeout % 1000) * 1000;
fd_set fdexception;
FD_ZERO(&fdexception);
FD_SET((SOCKET)m_socket.socketDescriptor(), &fdexception);
ret = select(0, 0, &fds, &fdexception, timeout < 0 ? 0 : &tv);
if (ret > 0 && FD_ISSET((SOCKET)m_socket.socketDescriptor(), &fdexception))
ret--;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
|