1

免责声明:我对 Qt 和任何围绕线程和网络的编程都比较陌生。我还采用了很多来自 Qt 示例、API 和其他在线示例的代码。

所有代码都可以在 GitHub 上找到。这段代码相对简单,因为它可以减去剥离 GUI。我认为以这种方式提供它也会有所帮助,而不是仅仅粘贴下面的代码。

我想使用并相信我需要使用线程,因为我需要多个客户端向服务器发送请求,服务器运行一些 SQL 代码,然后将结果吐回客户端(基本上派生一个 MySQL 服务器,但具体到什么我在做)。不过现在,我只是在努力学习这一切的运作方式。

话虽如此,正如标题所述。我的客户端可以连接到服务器,服务器设置线程,并将通过 readReady 接收数据(字符串)。读入数据后,现在我只是想将其回显给客户端。它会这样做,但只有一次。然后它吐出:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QNativeSocketEngine(0x266cca92ea0), parent's thread is serverThread(0x266cca9ed60), current thread is QThread(0x266cac772e0)

除非我让客户端重新连接,否则我无法向服务器发送任何进一步的数据,然后在发送数据后,它将完成其工作,但随后会吐出相同的错误并停止运行。我尝试了很多不同的东西,但似乎无法解决问题。我什至尝试按照 API 中的建议为此设置一个 SIGNAL/SLOT:

重要的是要记住 QThread 实例存在于实例化它的旧线程中,而不是调用 run() 的新线程中。这意味着 QThread 的所有排队槽都将在旧线程中执行。因此,希望在新线程中调用槽的开发人员必须使用工作对象方法;不应将新插槽直接实现到子类 QThread 中。

无论如何,任何帮助将不胜感激!我的代码如下..

服务器

服务器线程.cpp

// Project
#include "ServerDialog.h"
#include "ServerThread.h"

ServerThread::ServerThread(qintptr _socketDiscriptor, QObject *parent /*= 0*/)
    : QThread(parent)
{
    socketDiscriptor = _socketDiscriptor;
}

void ServerThread::run()
{
    emit threadStarted(socketDiscriptor);

    // Start Thread
    clientSocket = new QTcpSocket;

    // Set SocketDisc
    if (!clientSocket->setSocketDescriptor(socketDiscriptor))
    {
        emit error(clientSocket->error());
        return;
    }
    
    // Connect Socket and Signal
    connect(clientSocket, SIGNAL(readyRead()), this, SLOT(readyRead()));
    connect(clientSocket, SIGNAL(disconnected()), this, SLOT(disconnected()));

    //// Loop Thread to Stay Alive for Signals and Slots
    exec();
}

void ServerThread::readyRead()
{
    QDataStream in(clientSocket);
    in.setVersion(QDataStream::Qt_5_7);
    in.startTransaction();
    QString dataReceived;
    in >> dataReceived;
    if (!in.commitTransaction())
    {
        emit readyReadError(socketDiscriptor);
        return;
    }
    emit readyReadMessage(socketDiscriptor, dataReceived);
    echoData(dataReceived);
}

void ServerThread::disconnected()
{
    emit threadStopped(socketDiscriptor);
    clientSocket->disconnect();
    clientSocket->deleteLater();
    this->exit(0);
}

void ServerThread::echoData(QString &data)
{
    QByteArray block;
    QDataStream out(&block, QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_5_7);
    out << data;
    clientSocket->write(block);
}

因此,ServerThread.cpp在调用 echoData 时,即出现错误并且 Socket 停止运行。

任何和所有的帮助将不胜感激。我知道还有一些关于线程的“不能为...创建孩子”的其他帖子。但我没有发现其中任何一个有帮助。我确实觉得有趣但不理解的一件事可能是使用 moveToThread() 但对此有很多不同的评论。

我最好通过代码示例和解释来学习,而不是仅仅通过解释或指向 API 的指针。谢谢!

4

2 回答 2

3

大多数 Qt 网络功能是异步的;它们不会阻塞调用线程。如果您使用s ,则无需弄乱线程QTcpSocket。事实上,为每个套接字创建一个线程是多余的,因为该线程将花费大部分时间只是等待一些网络操作完成。以下是我将如何在 Qt 中实现单线程回显服务器:

#include <QtNetwork>
#include <QtCore>

//separate class for the protocol's implementation
class EchoSocket : public QTcpSocket{
    Q_OBJECT
public:
    explicit EchoSocket(QObject* parent=nullptr):QTcpSocket(parent){
        connect(this, &EchoSocket::readyRead, this, &EchoSocket::EchoBack);
        connect(this, &EchoSocket::disconnected, this, &EchoSocket::deleteLater);
    }
    ~EchoSocket() = default;

    Q_SLOT void EchoBack(){
        QByteArray receivedByteArray= readAll();
        write(receivedByteArray);
        disconnectFromHost();
    }
};

class EchoServer : public QTcpServer{
public:
    explicit EchoServer(QObject* parent= nullptr):QTcpServer(parent){}
    ~EchoServer() = default;

    //override incomingConnection() and nextPendingConnection()
    //to make them deal with EchoSockets instead of QTcpSockets
    void incomingConnection(qintptr socketDescriptor){
        EchoSocket* socket= new EchoSocket(this);
        socket->setSocketDescriptor(socketDescriptor);
        addPendingConnection(qobject_cast<QTcpSocket*>(socket));
    }

    EchoSocket* nextPendingConnection(){
        QTcpSocket* ts= QTcpServer::nextPendingConnection();
        return qobject_cast<EchoSocket*>(ts);
    }
};

int main(int argc, char* argv[]){
    QCoreApplication a(argc, argv);

    EchoServer echoServer;
    echoServer.listen(QHostAddress::Any, 9999);

    QObject::connect(&echoServer, &EchoServer::newConnection, [&](){
        EchoSocket* socket= echoServer.nextPendingConnection();
        qDebug() << "Got new connection from: " << socket->peerAddress().toString();
    });

    return a.exec();
}

#include "main.moc"

笔记:

  • 该服务器能够同时处理多个客户端,因为没有阻塞。线程将只响应发生的事件,并采取适当的行动;因此,如果该事件是一个新连接,它将创建一个新EchoSocket对象来处理它并打印一条语句qDebug()到联系。它永远不会阻塞等待数据到达的单个连接,也不会阻塞等待新连接到达
  • 由于您提到在项目后期使用一些 SQL 查询来响应某些连接。请避免使用线程,因为 Qt 中的 SQL 数据库连接只能从创建它的线程中使用,请参阅此处的文档。因此,您必须为应用程序中的每个线程(因此也为每个连接)创建一个新的数据库连接(这不仅仅是矫枉过正),或者稍后切换到单线程设计。

在本节中,我将解释为什么线程不能以您的方式为您工作:

应该在子类中声明插槽QThread,而是使用 workerQObject并根据需要将它们移动到QThreads

您在问题中提供的报价是您收到此警告的确切解释。您创建的ServerThread实例将存在于主线程(或创建它的任何线程)中。现在让我们从您的代码中考虑这一行:

connect(clientSocket, SIGNAL(readyRead()), this, SLOT(readyRead()));

信号readyRead()将从当前ServerThread实例发出(因为clientSocket发出它的对象存在于那里),但是,接收者对象是当前ServerThread实例,但它存在于主线程中。这是文档所说的:

如果接收器位于发出信号的线程中,则使用 Qt::DirectConnection。否则,使用 Qt::QueuedConnection

现在,主要的一点Qt::QueuedConnection是在接收者对象的线程中执行槽。这意味着,的插槽将在主线程中执行ServerThread::readyRead()ServerThread::disconnected。这很可能不是您想要做的,因为您最终会clientSocket从主线程访问。之后,任何导致创建clientSocketchildQObject的调用都将导致您收到警告(您可以在此处QTcpSocket::write()看到这样做)。

于 2016-12-11T10:08:48.973 回答
0

movetothread 的混合注释主要与使用它将线程对象移动到自身有关。

引用暗示 QThread 的成员并非旨在从工作人员调用。调用信号的严格正确方法是使用工作对象模型,这在 Qt 示例中显示并在 QT 相关博客上解释了几次:

class Worker : public QObject
{
    Q_OBJECT
private slots:
    void onTimeout()
    {
        qDebug()<<"Worker::onTimeout get called from?: "<<QThread::currentThreadId();
    }
};

class Thread : public QThread
{
    Q_OBJECT

private:
    void run()
    {
        qDebug()<<"From work thread: "<<currentThreadId();
        QTimer timer;
        Worker worker;
        connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));
        timer.start(1000);

        exec();
    }
};

在 run() 内部构造的 worker 是它创建的线程的“属性”,所以形象地说,它从属于它的上下文。如果您在其他线程中创建工作者,然后在建立连接之前将其移动到该线程,可能会达到相同的效果。当您将信号连接到 QThread 本身的插槽时,您将子线程连接到创建它的线程。

用于

connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()), Qt::DirectConnection);

或从您的线程创建连接有时似乎可以达到正确的结果,但在这种情况下,您尝试将不同线程中构造的对象一起使用。不建议在构造函数中调用 moveToThread(this)。

于 2016-12-11T09:20:04.693 回答