QT編寫TCP/UDP調試助手之多線程TCP服務器

效果圖:

TCP/UDP調試助手之TCP Server,支持一對一,一對多通信,主動斷開客戶端;多線程讀寫數據,線程數可設置,停止監聽後及時釋放資源。

一、前言

一般的多線程TCP服務器,是連接一個客戶端,創建一個子線程,把它放到這個子線程中運行,這樣能提高效率,但在大量客戶端的時候線程頻繁調度也會浪費性能,所以這裏提出一種新的多線程方式,可設置最大線程數,一個線程可運行多個Socket,考慮線程負載的方式(線程裏運行的Socket數),新的Socket連接總在負載最少的線程中創建和運行,根據硬件合理的設置線程數,可以避免線程頻繁調度。

 

二、關鍵代碼

主要講創建子線程,客戶端連接後如何分配給子線程運行。

1.在.pro文件中添加QT += network,要用到的頭文件包括:

#include <QTcpSocket>
#include <QTcpServer>
#include <QThread>

2.爲了更好的管理socket連接,自定義一個MyServer類,繼承自QTcpServer,並且重寫incomingConnection(qintptr socketDescriptor)方法。list_thread用來保存子線程指針。

class MyServer : public QTcpServer
{
    Q_OBJECT
public:
    explicit MyServer(QObject *parent = nullptr);
    ~MyServer();
    void SetThread(int num);//設置線程數
    int GetMinLoadThread();//獲取當前最少負載的線程ID

    SocketHelper* sockethelper;//socket創建輔助對象
    QList<MyThread*> list_thread;//線程列表
    QList<SocketInformation> list_information;//socket信息列表

    MainWindow* mainwindow;
private:
    void incomingConnection(qintptr socketDescriptor);
public slots:
 void AddInf(MySocket* mysocket,int index);//添加信息
 void RemoveInf(MySocket* mysocket);//移除信息

};

3.子線程類MyThread 繼承自QThread,定義一個輔助類SocketHelper用來幫助創建socket對象。

#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>

class MyServer;
class MySocket;

//Socket創建輔助類
class SocketHelper:public QObject
{
    Q_OBJECT
public:
    explicit SocketHelper(QObject *parent = nullptr);
    MyServer* myserver;
public slots:
    void CreateSocket(qintptr socketDescriptor,int index);//創建socket
signals:
    void Create(qintptr socketDescriptor,int index);//創建
    void AddList(MySocket* tcpsocket,int index);//添加信息
    void RemoveList(MySocket* tcpsocket);//移除信息
};

//子線程類
class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit  MyThread(QObject *parent);
    ~MyThread() override;
public:
    MyServer* myserver;
    SocketHelper* sockethelper;
    int ThreadLoad;//當前線程負載
    void run() override;
};

#endif // MYTHREAD_H

4.點擊界面監聽按鈕,設置線程,線程數可設置最小值爲1,也就沒有子線程,只有主線程,變成單線程的tcp server。調用listen(ip, port)開啓監聽。

        m_tcpServer=new MyServer(this);
        //設置線程
        m_tcpServer->SetThread(ui->spinBox_threadNum->value()-1);
        //監聽
        bool islisten = m_tcpServer->listen(ip, port);
        if(!islisten)
        {
            QMessageBox::warning(this,"錯誤",m_tcpServer->errorString());
            m_tcpServer->close();
            m_tcpServer->deleteLater();//釋放
            m_tcpServer=nullptr;
            return;
        }

4.1根據輸入的線程數創建子線程,並調用start()運行。

//創建子線程數
void MyServer::SetThread(int num)
{
    for(int i=0;i<num;i++)
    {
        list_thread.append(new MyThread(this));//新建線程
        list_thread[i]->ThreadLoad = 0;//線程負載初始0
        list_thread[i]->start();
    }
}

4.2線程run()裏創建sockethelper對象,sockethelper的槽函數會在該線程裏執行,重點來了,因爲後面sockethelper的槽函數CreateSocket裏會創建socket對象,socket對象的槽函數也會在該線程執行,這樣就把socket讀寫放到子線程裏運行了。

void MyThread::run()
{
    //在線程內創建對象,槽函數在這個線程中執行
    this->sockethelper=new SocketHelper(this->myserver);
    //連接信號槽
    connect(sockethelper,&SocketHelper::Create,sockethelper,&SocketHelper::CreateSocket);
    connect(sockethelper,&SocketHelper::AddList,myserver,&MyServer::AddInf);
    connect(sockethelper,&SocketHelper::RemoveList,myserver,&MyServer::RemoveInf);
    //事件循環
    exec();
}

5.如果沒有重寫incomingConnection,每當有客戶端連接,可以從nextPendingConnection()返回一個QTcpSocket對象,但現在重寫incomingConnection就不需要nextPendingConnection了,有客戶連接會觸發函數incomingConnection(qintptr socketDescriptor),參數socketDuescriptor是socket描述符,可以根據它初始化socket,把它傳給對應線程裏的sockethelper對象

//在對應線程裏創建新socket連接
void MyServer::incomingConnection(qintptr socketDescriptor)
{
    //獲取負載最少的子線程索引
    int index = GetMinLoadThread();
    if( index!= -1)//非UI線程時
    {
        //交給子線程運行
        emit list_thread[index]->sockethelper->Create(socketDescriptor,index);
    }
    else
    {
        //交給UI線程運行
        emit sockethelper->Create(socketDescriptor,index);
    }
}

5.1 遍歷list_thread[N]->ThreadLoad負載值,找到最少負載(Socket數)的線程,-1表示UI線程,其他表示在list_thread裏的索引。

//獲取負載最少的線程索引
//-1:UI線程
int MyServer::GetMinLoadThread()
{
    //只有1個子線程
    if(list_thread.count()==1)
    {
        return 0;
    }
    //多個子線程
    else if(list_thread.count()>1)
    {
        int minload=list_thread[0]->ThreadLoad;
        int index=0;
        for(int i=1;i<list_thread.count();i++)
        {
            if(list_thread[i]->ThreadLoad<minload)
            {
                index = i;
                minload=list_thread[i]->ThreadLoad;
            }
        }
        return index;
    }
    //沒有子線程
    return -1;
}

6. sockethelper的CreateSocket槽函數裏會創建並用socketDescriptor初始化tcpsocket 對象,並連接一系列信號槽,接下來就可以通信了。

void SocketHelper::CreateSocket(qintptr socketDescriptor,int index)
{
    qDebug()<<"subThread:"<<QThread::currentThreadId();

    MySocket* tcpsocket = new MySocket(this->myserver);
    tcpsocket->sockethelper = this;
    //初始化socket
    tcpsocket->setSocketDescriptor(socketDescriptor);
    //發送到UI線程記錄信息
    emit AddList(tcpsocket,index);//

    if( index!= -1)//非UI線程時
    {
        //負載+1
        myserver->list_thread[index]->ThreadLoad+=1;//負載+1
        //關聯釋放socket,非UI線程需要阻塞
        connect(tcpsocket , &MySocket::DeleteSocket , tcpsocket, &MySocket::deal_disconnect,Qt::ConnectionType::BlockingQueuedConnection);
    }
    else
    {
       connect(tcpsocket , &MySocket::DeleteSocket , tcpsocket, &MySocket::deal_disconnect,Qt::ConnectionType::AutoConnection);
    }

    //關聯顯示消息
    connect(tcpsocket,&MySocket::AddMessage,myserver->mainwindow,&MainWindow::on_add_serverMessage);
    //發送消息
    connect(tcpsocket,&MySocket::WriteMessage,tcpsocket,&MySocket::deal_write);
    //關聯接收數據
    connect(tcpsocket , &MySocket::readyRead , tcpsocket , &MySocket::deal_readyRead);
    //關聯斷開連接時的處理槽
    connect(tcpsocket , &MySocket::disconnected , tcpsocket, &MySocket::deal_disconnect);

    QString ip = tcpsocket->peerAddress().toString();
    quint16 port = tcpsocket->peerPort();
    QString message = QString("[%1:%2] 已連接").arg(ip).arg(port);
    //發送到UI線程顯示
    emit tcpsocket->AddMessage(message);

}

7. socket的讀寫就不貼了,數據顯示用信號和槽的方式和UI線程通信。

8. 每次結束監聽時及時釋放所有資源,避免內存溢出

MyServer::~MyServer()
{
    //釋放所有socket
    while(list_information.count()>0)
    {
        emit list_information[0].mysocket->DeleteSocket();
        list_information.removeAt(0);
    }
    //清空combox
    while (this->mainwindow->ui->comboBox->count()>1) {
        this->mainwindow->ui->comboBox->removeItem(1);
    }

    //釋放所有線程
    while(list_thread.count()>0)
    {
        list_thread[0]->quit();
        list_thread[0]->wait();//等待退出
        list_thread[0]->deleteLater();//釋放
        list_thread.removeAt(0);
    }
    //UI線程裏的sockethelper
    sockethelper->disconnect();
    delete  this->sockethelper;//
}

三、下載

tcp/udp調試助手​​​​​​​

發佈了25 篇原創文章 · 獲贊 48 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章