2013-10-09 46 views
2

我正在使用一個線程化的telnet服務器(每個連接一個線程),並且無法弄清楚如何擺脫valgrind錯誤。我將問題縮小到了我刪除tcpsocket的地方。如何在Qt中創建一個線程化的網絡服務器?

我在的QThread的run()方法創建與QTcpSocket:

void TelnetConnection::run() 
{ 
    tcpSocketPtr = new QTcpSocket(); 
    if (!tcpSocketPtr->setSocketDescriptor(socketDescriptor)) { 
     emit error(tcpSocketPtr->error()); 
     return; 
    } 
    .... 
} 

當我的應用程序要斷開客戶端我打電話:

void TelnetConnection::disconnectClient() 
{ 
    tcpSocketPtr->disconnectFromHost(); 
} 

和被要求插槽socket斷開連接是: void TelnetConnection :: clientDisconnected()

{ 
    tcpSocketPtr->deleteLater(); 
    TelnetConnection::s_clientCount--; 
    QThread::quit(); // Exit ths event loop for this thread 
} 

所以,我試過 1.刪除clientDisconnected插槽中的QTcpSocket,但會導致讀/寫不穩定。 (偶爾發生崩潰) 2.在clientDisconnected插槽中刪除重載但導致內存讀取/寫入錯誤 3.在線程的exec循環後刪除但仍導致內存讀取/寫入錯誤 4.在線程的exec循環後刪除 - 所有的錯誤都消失了。

從我讀到的內容來看,deletelater如果在exec循環終止後調用AFTER,將在線程被刪除時運行。所以,雖然這有效,但我不認爲這是正確的方式。

我試圖創建QTcpSocket與「本」作爲父母,但然後我的信號連接失敗,因爲父母與這種不匹配的錯誤。 (這將允許在線程銷燬時刪除QTcpSocket)。

什麼是解決這個問題的正確方法?

回答

5

您的問題幾乎完全來自QThread的重新實現。不要這樣做。將所有功能放入QObject,然後使用moveToThread()將其移至裸機QThread。如果您只通過信號插槽連接從外部訪問您的物體,那麼您就可以在那裏完成任務。

首先,我會經常引用TelnetConnection的某個實例作爲telnetThread。這只是爲了讓我明白我在說什麼線程。

在代碼中的錯誤,到目前爲止,你已經證明,分別是:

  1. 你從run()方法中調用emit error(tcpSocketPtr->error())。它的名字來自telnetThread,一個不同的線程QObject的信號居住在:它住在telnetThread->thread()

    run()方法在telnetThread線程內執行。但是,由moc生成的信號的實現預計將在您實例化的任何線程QThread - 即telnetThread->thread()中調用,並且該線程永遠不會等於run()執行的那個。基本上,有點混亂,以下不變成立:

    QThread * telnetThread ... 
    Q_ASSERT(telnetThread != telnetThread->thread()); 
    
  2. 你呼籲tcpSocketPtr方法,生活在telnetThread,從另一個線程執行插槽。以下成立:

    Q_ASSERT(tcpSocketPtr->thread() == telnetThread); 
    

    所有這些插槽上的telnetThreadtelnetThread本身在不同的線程正在執行申報!因此,disconnectClient的主體在GUI線程中執行,但它直接在tcpSocketPtr上調用方法。

以下是這樣做的一種方式。它偵聽端口8023.^D結束連接。接收大寫字母Q,然後按Enter/Return將乾淨地關閉服務器。

介紹

注:這個例子被重構,最後服務器現在正確地刪除。

一定要注意確保事情乾淨整潔。請注意,運行QCoreApplication只需quit()即可,自動進行總結。所以,所有的對象最終都會被破壞並釋放,而且沒有任何東西會崩潰。線程和服務器從析構函數向控制檯發送診斷消息。這種方式很明顯,事情會被刪除。

代碼支持的Qt 4和Qt 5.

StoppingThread

添加缺少的行爲QThread。通常,當你破壞一個正在運行的線程時,你會收到一條警告消息和崩潰/未定義的行爲。這個類在銷燬時告訴線程的事件循環退出並等待線程真正完成。它的使用就像QThread一樣,只是它不會在破壞時做出愚蠢的事情。

ThreadedQObjectDeleter

刪除一個給定的QObject時,它的線程被破壞。當線程邏輯上擁有它的對象時很有用。這個邏輯所有權是而不是父 - 子所有權,因爲線程和邏輯擁有的對象存在於不同的線程(!)中。

該構造函數是私有的,並提供了一個工廠方法。這是爲了強制在免費商店(又名堆)上創建刪除者。在堆棧中創建刪除器可能是一個錯誤,因此此模式使用編譯器來防止它發生。

該對象一定還沒有被移動到指定的線程,否則刪除器的構造會受競爭條件的影響 - 對象可能已經在線程中自己刪除了。這個先決條件是有效的。

ServerFactory

調用其newConnection插槽中時,產生一個新的服務器實例。構造函數通過客戶端QObjectQMetaObject創建。因此,這個類可以構建「任何」所需的QObject而不需要使用模板。只有一個對象上的要求,即它創建:

它必須有一個Q_INVOKABLE構造採取QTcpSocket*作爲第一個參數,並QObject *parent作爲第二個參數。它生成的對象是使用父設置爲nullptr創建的。

套接字的所有權被轉移到服務器。

ThreadedServerFactory

創建用於製作每個服務器專用StoppingThread,服務器移動到這個線程。否則就像ServerFactory一樣。線程由工廠擁有,並在工廠遭到破壞時妥善處理。

服務器的終止會退出線程的事件循環,從而結束線程。完成的線程被刪除。在終止服務器之前被破壞的線程將刪除現在懸掛的服務器。

TelnetServer

實現一個簡單的telnet服務器。該接口由一個可調用的構造函數組成。構造函數需要使用套接字並將套接字連接到內部插槽。該功能非常簡單,只對來自插座的信號readyReaddisconnected有反應。斷開時,它會自行刪除。

這不是一個真正的telnet服務器,因爲telnet協議並不那麼簡單。恰巧,telnet客戶端將使用這種笨的服務器。

主()

主函數創建一個服務器,服務器工廠,並且將它們連接在一起。然後它通知服務器偵聽任何地址(端口8023)上的連接,並啓動主線程的事件循環。監聽服務器和工廠位於主線程中,但所有服務器都在自己的線程中,因爲在查看歡迎消息時可以輕鬆看到。支持任意數量的服務器。

#include <QCoreApplication> 
#include <QThread> 
#include <QTcpServer> 
#include <QTcpSocket> 
#include <QAbstractEventDispatcher> 
#include <QPointer> 

#if QT_VERSION < QT_VERSION_CHECK(5,0,0) 
#define Q_DECL_OVERRIDE override 
#endif 

// A QThread that quits its event loop upon destruction, 
// and waits for the loop to finish. 
class StoppingThread : public QThread { 
    Q_OBJECT 
public: 
    StoppingThread(QObject * parent = 0) : QThread(parent) {} 
    ~StoppingThread() { quit(); wait(); qDebug() << this; } 
}; 

// Deletes an object living in a thread upon thread's termination. 
class ThreadedQObjectDeleter : public QObject { 
    Q_OBJECT 
    QPointer<QObject> m_object; 
    ThreadedQObjectDeleter(QObject * object, QThread * thread) : 
     QObject(thread), m_object(object) {} 
    ~ThreadedQObjectDeleter() { 
     if (m_object && m_object->thread() == 0) { 
      delete m_object; 
     } 
    } 
public: 
    static void addDeleter(QObject * object, QThread * thread) { 
     // The object must not be in the thread yet, otherwise we'd have 
     // a race condition. 
     Q_ASSERT(thread != object->thread()); 
     new ThreadedQObjectDeleter(object, thread); 
    } 
}; 

// Creates servers whenever the listening server gets a new connection 
class ServerFactory : public QObject { 
    Q_OBJECT 
    QMetaObject m_server; 
public: 
    ServerFactory(const QMetaObject & client, QObject * parent = 0) : 
     QObject(parent), m_server(client) {} 
    Q_SLOT void newConnection() { 
     QTcpServer * listeningServer = qobject_cast<QTcpServer*>(sender()); 
     if (!listeningServer) return; 
     QTcpSocket * socket = listeningServer->nextPendingConnection(); 
     if (!socket) return; 
     makeServerFor(socket); 
    } 
protected: 
    virtual QObject * makeServerFor(QTcpSocket * socket) { 
     QObject * server = m_server.newInstance(Q_ARG(QTcpSocket*, socket), Q_ARG(QObject*, 0)); 
     socket->setParent(server); 
     return server; 
    } 
}; 

// A server factory that makes servers in individual threads. 
// The threads automatically delete itselves upon finishing. 
// Destructing the thread also deletes the server. 
class ThreadedServerFactory : public ServerFactory { 
    Q_OBJECT 
public: 
    ThreadedServerFactory(const QMetaObject & client, QObject * parent = 0) : 
     ServerFactory(client, parent) {} 
protected: 
    QObject * makeServerFor(QTcpSocket * socket) Q_DECL_OVERRIDE { 
     QObject * server = ServerFactory::makeServerFor(socket); 
     QThread * thread = new StoppingThread(this); 
     connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); 
     connect(server, SIGNAL(destroyed()), thread, SLOT(quit())); 
     ThreadedQObjectDeleter::addDeleter(server, thread); 
     server->moveToThread(thread); 
     thread->start(); 
     return server; 
    } 
}; 

// A telnet server with following functionality: 
// 1. It echoes everything it receives, 
// 2. It shows a smiley face upon receiving CR, 
// 3. It quits the server upon ^C 
// 4. It disconnects upon receiving 'Q' 
class TelnetServer : public QObject { 
    Q_OBJECT 
    QTcpSocket * m_socket; 
    bool m_firstInput; 
    Q_SLOT void readyRead() { 
     const QByteArray data = m_socket->readAll(); 
     if (m_firstInput) { 
      QTextStream out(m_socket); 
      out << "Welcome from thread " << thread() << endl; 
      m_firstInput = false; 
     } 
     for (int i = 0; i < data.length(); ++ i) { 
      char c = data[i]; 
      if (c == '\004') /* ^D */ { m_socket->close(); break; } 
      if (c == 'Q') { QCoreApplication::exit(0); break; } 
      m_socket->putChar(c); 
      if (c == '\r') m_socket->write("\r\n:)", 4); 
     } 
     m_socket->flush(); 
    } 
public: 
    Q_INVOKABLE TelnetServer(QTcpSocket * socket, QObject * parent = 0) : 
     QObject(parent), m_socket(socket), m_firstInput(true) 
    { 
     connect(m_socket, SIGNAL(readyRead()), SLOT(readyRead())); 
     connect(m_socket, SIGNAL(disconnected()), SLOT(deleteLater())); 
    } 
    ~TelnetServer() { qDebug() << this; } 
}; 

int main(int argc, char *argv[]) 
{ 
    QCoreApplication a(argc, argv); 
    QTcpServer server; 
    ThreadedServerFactory factory(TelnetServer::staticMetaObject); 
    factory.connect(&server, SIGNAL(newConnection()), SLOT(newConnection())); 
    server.listen(QHostAddress::Any, 8023); 
    return a.exec(); 
} 

#include "main.moc" 
+0

@Michelle:當然有更簡單的方法來做到這一點,但由於更大的整體類,它們不會正確運行或難以理解。 –

+0

@Michelle:與任何問題一樣,攻擊方法有很多種。其他人肯定會想出一個不同的答案,希望它也會被測試,並在Qt 4和Qt 5上運行完整的代碼:) –

+0

我還在搞清楚......我混亂的一部分是語法太。使用Q_SLOT,我認爲它與類中的「slots:」部分相同,而obj.connect(source,sourcesignal,objslot)...我認爲它與connect相同(source,sourcesignal,obj,objslot )..? – TSG

相關問題