效果圖:
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調試助手