效果图:
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调试助手