本帖最后由 cary_云飞扬 于 2022-9-19 18:24 编辑
6月份学完C基础,写了一个控制台贪吃蛇。9月份了,学习Qt框架用来写界面程序,主要是目前工作需要一个项目管理系统,所有就着手边学习边练手。
tcp协议通信,我就研究了小一周,网上找的教程都是单线程通信单客户端通信,这个没问题,但我需要的是多客户端并发访问服务器,且服务器需要多线程处理数据。光服务器端多线程处理客户端通信就研究好久,今天终于搞通了,分享一下。
1、服务器初始化界面
[img][/img]
2、开启监听服务
[img][/img]
3、多个客户端与服务器建立连接
[img][/img]
4、服务器向特定客户端发信息
[img][/img]
以下是代码部分:
1、子线程头文件
[C++] 纯文本查看 复制代码 #ifndef COMTHREAD_H
#define COMTHREAD_H
#include <QThread>
#include <QTcpSocket>
class ComThread : public QThread
{
Q_OBJECT
public:
explicit ComThread(qintptr socketDescriptor,QObject *parent = nullptr);
void onReadClientInfo();//读取客户端信息函数
void onReadData();//读取客户端发送过来的数据函数
void onWriteData(qintptr sock,QString data);//发送数据给客户端函数
void onClientDisconnect();//客户端断开连接函数
protected:
void run();//线程处理函数,需重写
private:
qintptr m_sock;//通信套接字描述符
QTcpSocket *socket;//通信套接字
signals:
void readClientInfo();//发射读取客户端信息信号
void clientInfo(QString info);//将客户端信息发送给调用者(主线程)
void readData(QString data);//将客户端发过来的信息发送给调用者(主线程)
void writeData(qintptr sock,QString data);//调用者(主线程)发送数据给客户端
void clientDisconnect();//客户端断开连接信号,发送给调用者(主线程)
};
#endif // COMTHREAD_H
2、子线程源文件
[C++] 纯文本查看 复制代码 #include "comthread.h"
#include <QDebug>
/**
* [url=home.php?mod=space&uid=190858]@brief[/url] ComThread::ComThread
* [url=home.php?mod=space&uid=952169]@Param[/url] socketDescriptor
* @param parent
* 程序名称:多线程服务器测试小程序
* 模块名称:线程处理通信服务
* 作者:cary-云飞扬
* 时间:2022-9-19
*/
ComThread::ComThread(qintptr socketDescriptor,QObject *parent)
: QThread{parent}
{
socket = new QTcpSocket(this);//创建通信套接字
this->m_sock = socketDescriptor;//接收服务器检测到的通信描述符
this->socket->setSocketDescriptor(m_sock);//将接收的描述符设置给当前通信套接字
}
//读取客户端信息函数
void ComThread::onReadClientInfo()
{
QString ip = socket->peerAddress().toString();
int port = socket->peerPort();
ip = QString("[%1]:[%2]已成功连接!").arg(ip).arg(port);
emit clientInfo(ip);
}
//读取客户端发送过来的数据信息。可以是文件,也可以是文字,本次为文字信息
void ComThread::onReadData()
{
QByteArray array = socket->readAll();//全部读取,发过来多少读多少
emit readData(array);//将读取到的信息发送给调用者(主线程)
}
//发送数据给客户端
void ComThread::onWriteData(qintptr sock, QString data)
{
if(sock == this->m_sock)//判断传入的描述符和当前线程的描述符是否一致,一致才写数据
{
socket->write(data.toUtf8());
}else{
//当传入的描述符和当前线程描述符不一致,可以发送信号给调用者处理,本次空实现
qDebug() << "发送消息失败,35行"<< "传入的描述符:"<< sock << "当前线程描述符:" << m_sock;
}
}
//客户端断开连接函数
void ComThread::onClientDisconnect()
{
//客户端断开连接后,发送信号给调用者(主线程),具体善后工作由调用者(主线程)处理
emit clientDisconnect();
}
//线程处理函数
void ComThread::run()
{
qDebug() <<"子线程号:"<< QThread::currentThread();
//读取客户端信息,并将该信息发送给调用者(主线程)
this->onReadClientInfo();
//读取客户端发来的数据,并将该数据发送给调用者(主线程)
connect(socket,&QTcpSocket::readyRead,this,&ComThread::onReadData);
//主线程发送数据给客户端
connect(this,&ComThread::writeData,this,&ComThread::onWriteData);
//客户端断开连接,发送信号给调用者(主线程)
connect(socket,&QTcpSocket::disconnected,this,&ComThread::clientDisconnect);
}
3、自定义监听套接字头文件
[C++] 纯文本查看 复制代码 #ifndef MYTCPSERVER_H
#define MYTCPSERVER_H
#include <QTcpServer>
class MyTcpServer : public QTcpServer
{
Q_OBJECT
public:
explicit MyTcpServer(QObject *parent = nullptr);
protected:
virtual void incomingConnection(qintptr socketDescriptor);//多线程通信时需重写此虚函数
private:
signals:
void newSocketDescriptor(qintptr);
};
#endif // MYTCPSERVER_H
4、自定义监听套接字源文件
[C++] 纯文本查看 复制代码 #include "mytcpserver.h"
/**
* @brief MyTcpServer::MyTcpServer
* @param parent
* 程序名称:多线程服务器测试小程序
* 模块名称:自定义监听套接字
* 作者:cary-云飞扬
* 时间:2022-9-19
*/
MyTcpServer::MyTcpServer(QObject *parent)
: QTcpServer{parent}
{
}
//多线程通信时需重写此虚函数
void MyTcpServer::incomingConnection(qintptr socketDescriptor)
{
//有新客户端连接时,Qt自动调用此虚函数。
//本次将获得的描述符以信号形式发送给调用者使用
emit newSocketDescriptor(socketDescriptor);
}
5、主界面头文件
[C++] 纯文本查看 复制代码 #ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "mytcpserver.h"
#include "comthread.h"
#include <QMap>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_startButton_clicked();
void on_sendButton_clicked();
void on_closeButton_clicked();
private:
Ui::MainWindow *ui;
MyTcpServer *server;//服务器
int total;//连接的客户端总数
QMap<qintptr,ComThread*> map;//存放子线程的容器
signals:
void writeData(qintptr sock,QString data);
};
#endif // MAINWINDOW_H
6、主界面源文件
[C++] 纯文本查看 复制代码 #include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QDebug>
#include <QThread>
#include "comthread.h"
#include <QTcpSocket>
/**
* @brief MainWindow::MainWindow
* @param parent
* 程序名称:多线程服务器测试小程序
* 模块名称:服务器主界面
* 作者:cary-云飞扬
* 时间:2022-9-19
*/
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
qDebug() <<"主线程号:"<< QThread::currentThread();
ui->setupUi(this);
this->setWindowTitle("TCP通信服务器");
//设定服务器监听端口
ui->port->setText("1986");
//初始化客户端连接总数
total = 0;
ui->total->setText(QString("%1").arg(total));
ui->sendButton->setEnabled(false);
ui->closeButton->setEnabled(false);
//创建服务器
server = new MyTcpServer(this);
/*当有新的客户端连接时,服务器发送newSocketDescriptor信号,此时创建
子窗口,并创建独立的线程与之通信*/
connect(server,&MyTcpServer::newSocketDescriptor,[=](qintptr sock)
{
//当由客户端连接时,创建子线程处理客户端对话
ComThread *thread = new ComThread(sock);
thread->start();//启动子线程
total ++;//更新客户端连接数
ui->total->setText(QString("%1").arg(total));//显示连接客户端总数
ui->sendButton->setEnabled(true);//设置发送消息按钮可发送状态
//显示与服务器通话的客户端,默认显示首次连接的客户端
ui->comboBox->addItem(QString("%1").arg(sock));
//将套接字描述符和对应的线程存放到容器中,便于后续操作
map.insert(sock,thread);
//读取已连接的客户端信息,并更新主界面
connect(thread,&ComThread::clientInfo,[=](QString info)
{
ui->receiveEdit->append(info);
});
//读取客户端发来的数据,并显示在主界面
connect(thread,&ComThread::readData,[=](QString data)
{
ui->receiveEdit->append(QString("客户端%1:%2").arg(sock).arg(data));
});
//客户端断开连接时更新主界面
connect(thread,&ComThread::clientDisconnect,[=]()
{
total --;//连接总数自减
ui->total->setText(QString("%1").arg(total));//更新客户端连接数
if(0 == total)
{
ui->sendButton->setEnabled(false);
}
//删除已经断开连接的客户端
ui->comboBox->removeItem(ui->comboBox->findText(QString("%1").arg(map.key(thread))));
map.remove(map.key(thread));
thread->quit();
thread->wait();
thread->deleteLater();
});
});
//给指定的客户端发送数据
connect(this,&MainWindow::writeData,[=](qintptr sock,QString data)
{
//发送消息给客户端
emit map.value(sock)->writeData(sock,QString("服务器:%1").arg(data));
//将发送的消息显示到接收消息界面
ui->receiveEdit->append(QString("服务器:%1").arg(data));
//发送消息界面清屏
ui->sendEdit->clear();
});
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_startButton_clicked()
{
int port = ui->port->text().toInt();
if(server->listen(QHostAddress::Any,port))
{
QMessageBox::about(this,"启动监听","开启监听服务成功!!!");
}
ui->startButton->setEnabled(false);
ui->closeButton->setEnabled(true);
}
//发送数据到指定的客户端
void MainWindow::on_sendButton_clicked()
{
QString data = ui->sendEdit->toPlainText();
qintptr sock = ui->comboBox->currentText().toInt();
emit writeData(sock,data);
}
void MainWindow::on_closeButton_clicked()
{
server->close();
QMessageBox::about(this,"关闭服务器","服务器已关闭,所有通信终止!!!");
ui->closeButton->setEnabled(false);
ui->startButton->setEnabled(true);
}
7、main函数源文件
[C++] 纯文本查看 复制代码 #include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
由于客户端代码比较简单,就不贴了,把服务端exe文件和客户端exe文件打包上传,但两个exe文件一定得放到Qt安装目录的bin目录下才可以运行。 |