IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> QTcpSokcet网络通信开发总结 -> 正文阅读

[网络协议]QTcpSokcet网络通信开发总结

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的跨线程使用。

// SocketTest.h
#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;
};
// SocketTest.cpp
#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);

	// 在主线程创建对象, 实际就是在QThread里创建对象
    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()
{
	// 将QTcpSocket放到std::thread中创建
    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);
	// 尝试使用获取readyRead信号
    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;
            // select for write

            // Windows needs this to report errors when connecting a socket ...
            fd_set fdexception;
            FD_ZERO(&fdexception);
            FD_SET((SOCKET)m_socket.socketDescriptor(), &fdexception);

            ret = select(0, 0, &fds, &fdexception, timeout < 0 ? 0 : &tv);

            // ... but if it is actually set, pretend it did not happen
            if (ret > 0 && FD_ISSET((SOCKET)m_socket.socketDescriptor(), &fdexception))
                ret--;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
}
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-03-30 19:06:09  更:2022-03-30 19:09:19 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 5:52:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码