QT开发框架以其跨平台的优势,在全世界IT界如雷贯耳。其封装了功能齐全的各种类,大大的提高了开发者的效率。本篇内容将介绍如何使用QT 6.4.1框架开发服务器和客户端程序,让两端能够首发消息,服务端往客户端发送文件(客户端往服务器发送类似,没有实现)。
说明:首先运行同时运行客户端和服务端程序,服务绑定端口开启服务,客户端连接服务器。然后服务器和客户端互相打招呼,然后服务器给客户端发送一首唐诗。
- #include "mainwindow.h"
-
- #include
-
- int main(int argc, char *argv[])
- {
- QApplication a(argc, argv);
- MainWindow w;
- w.show();
- return a.exec();
- }
头文件和实现:
- #ifndef MYTCPSERVER_H
- #define MYTCPSERVER_H
-
- #include
- #include
-
- class MyTcpServer : public QTcpServer
- {
- Q_OBJECT
- public:
- explicit MyTcpServer(QObject *parent = nullptr);
- //该函数是由框架调用
- void incomingConnection(qintptr socketDescriptor);
-
- signals:
- void newClient(qintptr socket);
-
- };
-
- #endif // MYTCPSERVER_H
- #include "mytcpserver.h"
-
- MyTcpServer::MyTcpServer(QObject *parent)
- : QTcpServer{parent}
- {
-
- }
-
- //该函数是由框架调用
- void MyTcpServer::incomingConnection(qintptr socketDescriptor)
- {
- emit newClient(socketDescriptor);
- }
- #ifndef MAINWINDOW_H
- #define MAINWINDOW_H
-
- #include
- #include "MyTcpServer.h"
- #include
-
- QT_BEGIN_NAMESPACE
- namespace Ui { class MainWindow; }
- QT_END_NAMESPACE
-
- class MainWindow : public QMainWindow
- {
- Q_OBJECT
-
- public:
- MainWindow(QWidget *parent = nullptr);
- ~MainWindow();
-
- signals:
- void start(QString file);
- void sendMsg(QByteArray msg);
-
- private slots:
- void on_start_clicked();
-
- void on_selectFile_clicked();
-
- void on_sendMsg_clicked();
-
- void on_transferFile_clicked();
-
- private:
- Ui::MainWindow *ui;
- MyTcpServer *m_server;
- QLabel *m_status;
- };
- #endif // MAINWINDOW_H
- #include "mainwindow.h"
- #include "ui_mainwindow.h"
- #include
- #include
- #include
- #include
- #include
- #include
- #include "sendfile.h"
-
- //alt +回车添加自动添加头文件
- MainWindow::MainWindow(QWidget *parent)
- : QMainWindow(parent)
- , ui(new Ui::MainWindow)
- {
- ui->setupUi(this);
- ui->lineEditPort->setText("9521");
- ui->lineFile->setText("C:\\Users\\Administrator\\Desktop\\OKR设定.txt");
- ui->progressBar->setRange(0, 100);
- ui->progressBar->setValue(0);
-
- //创建服务器socket
- m_server = new MyTcpServer(this);
- connect(m_server, &MyTcpServer::newClient, this, [=](qintptr socket) {
- ui->textEditContent->append("检测到有新的客户端连接");
- //有新连接
- m_status->setPixmap(QPixmap(":/ok.png").scaled(20, 20));
- QThread *subThread = new QThread;
- SendFile* worker = new SendFile(socket);
- worker->moveToThread(subThread);
-
- //子线程工作是需要主线程给其发信号通知工作,此处用来通知子线程给客户端发送文件
- connect(this, &MainWindow::start, worker, &SendFile::working);
-
- connect(worker, &SendFile::done, this, [=](){
- ui->textEditContent->append("检测到客户端退出");
- //客户端断开
- m_status->setPixmap(QPixmap(":/error.png").scaled(20, 20));
- qDebug() << "销毁子线程资源";
- subThread->quit(); //通知退出
- subThread->wait(); //等任务做完
- subThread->deleteLater(); //相当于对new的对象进行delete操作
- worker->deleteLater();
- });
- //不能在子线程操作ui对象,读和写都不行,需要子线程通过信号通知ui线程间接操作ui对象
- connect(worker, &SendFile::text, this, [=](QByteArray msg){
- QVector
colors = { - Qt::red, Qt::green, Qt::black, Qt::blue,
- Qt::darkRed, Qt::cyan, Qt::magenta
- };
- int index = QRandomGenerator::global()->bounded(colors.size());
- ui->textEditContent->setTextColor(colors.at(index));
- ui->textEditContent->append(msg);
- });
- //显示客户端发来的消息
- connect(worker, &SendFile::showRecvMsg, this, [=](QByteArray msg){
- ui->textEditContent->append("收到消息:" + msg);
- });
-
- //通知子线程发送一行文本
- connect(this, &MainWindow::sendMsg, worker, &SendFile::sendMsg);
-
- connect(worker, &SendFile::updateProgressBar, this, [=](int value){
- ui->progressBar->setValue(value);
- });
-
- subThread->start();
- });
-
- //处理状态栏,new一个QLabel,可以制定父对象,也可以不指定,因为需要把
- //它设置到状态栏中,设置后其父对象默认就是状态栏,状态栏析构后就会回收QLabel
- //不用delete
- m_status = new QLabel;
- m_status->setPixmap(QPixmap(":/error.png").scaled(20, 20));
- ui->statusbar->addWidget(new QLabel("状态:"));
- ui->statusbar->addWidget(m_status);
- }
-
- MainWindow::~MainWindow()
- {
- delete ui;
- }
-
-
- void MainWindow::on_start_clicked()
- {
- unsigned short port = ui->lineEditPort->text().toUShort();
- //绑定端口等待客户端连接,有客户端连接触发信号:[signal] void QTcpServer::newConnection()
- //QT中用于通信的QTcpSocket套接字对象不能跨线程访问
- m_server->listen(QHostAddress::Any, port);
- ui->start->setDisabled(true);
- }
-
-
- void MainWindow::on_selectFile_clicked()
- {
- QString file = QFileDialog::getOpenFileName(this);
- if (!file.isEmpty()) {
- ui->lineFile->setText(file);
- }
- }
-
-
- void MainWindow::on_sendMsg_clicked()
- {
- QString msg = ui->lineEditMsg->text();//.toUShort();
- emit sendMsg(msg.toUtf8());
- ui->textEditContent->append("发出消息:" + msg);
- }
-
- void MainWindow::on_transferFile_clicked()
- {
- if (ui->lineFile->text().isEmpty()) {
- QMessageBox::information(this, "提示", "请选择要发送的文件");
- return;
- }
- emit start(ui->lineFile->text());
- }
-
- #ifndef SENDFILE_H
- #define SENDFILE_H
-
- #include
- #include
- #include
- #include
-
- class SendFile : public QObject
- {
- Q_OBJECT
- public:
- explicit SendFile(qintptr socket, QObject *parent = nullptr);
- void working(QString file);
- void sendMsg(QByteArray msg);
-
- signals:
- void done();
- void text(QByteArray msg);
- void showRecvMsg(QByteArray msg);
- void updateProgressBar(int value);
-
- private:
- qintptr m_socket;
- QTcpSocket* m_tcpConn;
- };
-
- #endif // SENDFILE_H
- #include "sendfile.h"
- #include
- #include
- #include
- #include
-
- SendFile::SendFile(qintptr socket, QObject *parent)
- : QObject{parent}
- {
- //传递过来了用于通信的套接字,就可以和对端通信了
- m_socket = socket;
- }
-
- void SendFile::working(QString file) {
- qDebug() << "子线程ID: " << QThread::currentThread();
- //注意:m_tcpConn的创建要在子线程中
- m_tcpConn = new QTcpSocket;
- //用文件描述符初始化m_tcpConn
- m_tcpConn->setSocketDescriptor(m_socket);
-
- //检测客户端断开
- connect(m_tcpConn, &QTcpSocket::disconnected, this, [=]() {
- m_tcpConn->close();
- m_tcpConn->deleteLater();
- emit done();
- qDebug() << "检测到客户端退出,我的资源也进行了销毁,再见!";
- });
-
- //检测客户端发来数据
- connect(m_tcpConn, &QTcpSocket::readyRead, this, [=]() {
- //接收数据并把数据发送给主线程进行显示
- QByteArray data = m_tcpConn->readAll();
- emit showRecvMsg(data);
- });
-
- qDebug() << "传输的文件: " << file;
- QFile opFile(file);
- bool ret = opFile.open(QFile::ReadOnly);
- if (ret) {
- emit updateProgressBar(0);
- //此处打开的是windows下的文件,windows默认是GBK编码,所以此处指定编码GBK
- QTextCodec *codec = QTextCodec::codecForName("GBK");
- qint64 fileSize = opFile.size();
- qint64 sendSize = 0;
- while (!opFile.atEnd()) {
- //自定格式发包,将发送长度转换成大端存储发送到对端
- QByteArray line = opFile.readLine();
- sendSize += line.size();
- QString strUnicode = codec->toUnicode(line);
- QByteArray utf8line = strUnicode.toUtf8();
- //以下代码写成两行比较稳定,如果写成
- //uint32_t len = qToBigEndian(utf8line.size());在某些环境转换长度就为0
- uint32_t orgLen = utf8line.size();
- uint32_t len = qToBigEndian(orgLen);
- QByteArray data((char*)&len, 4);
- data.append(utf8line);
- //发送一行数据给对端
- m_tcpConn->write(data);
- emit text(utf8line);
- emit updateProgressBar(100 * sendSize / fileSize);
- //为了减轻对端压力,休眠一会
- QThread::sleep(1);
- }
- }
- opFile.close();
- }
-
- void SendFile::sendMsg(QByteArray msg) {
- //用文件描述符初始化m_tcpConn
- m_tcpConn = new QTcpSocket;
- m_tcpConn->setSocketDescriptor(m_socket);
-
- //检测客户端断开
- connect(m_tcpConn, &QTcpSocket::disconnected, this, [=]() {
- m_tcpConn->close();
- m_tcpConn->deleteLater();
- emit done();
- });
-
- //检测客户端发来数据
- connect(m_tcpConn, &QTcpSocket::readyRead, this, [=]() {
- //接收数据并把数据发送给主线程进行显示
- QByteArray data = m_tcpConn->readAll();
- emit showRecvMsg(data);
- });
-
- //QT默认是UTF8编码,所以输入框是UTF8编码,此处指定编码方式为UTF8
- QTextCodec *codec = QTextCodec::codecForName("UTF8");
- QString strUnicode = codec->toUnicode(msg);
- QByteArray utf8line = strUnicode.toUtf8();
- uint32_t orgLen = utf8line.size();
- uint32_t len = qToBigEndian(orgLen);
- //拼接头:表示长度
- QByteArray data((char*)&len, 4);
- //拼接发送内容
- data.append(utf8line);
- m_tcpConn->write(data);
- }
- QT += core gui network
-
- greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
- greaterThan(QT_MAJOR_VERSION, 5): QT += core5compat
- CONFIG += c++17 console
-
- # You can make your code fail to compile if it uses deprecated APIs.
- # In order to do so, uncomment the following line.
- #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
-
- SOURCES += \
- main.cpp \
- mainwindow.cpp \
- mytcpserver.cpp \
- sendfile.cpp
-
- HEADERS += \
- mainwindow.h \
- mytcpserver.h \
- sendfile.h
-
- FORMS += \
- mainwindow.ui
-
- # Default rules for deployment.
- qnx: target.path = /tmp/$${TARGET}/bin
- else: unix:!android: target.path = /opt/$${TARGET}/bin
- !isEmpty(target.path): INSTALLS += target
-
- RESOURCES += \
- res.qrc
- #include "mainwindow.h"
-
- #include
-
- int main(int argc, char *argv[])
- {
- QApplication a(argc, argv);
- MainWindow w;
- w.show();
- return a.exec();
- }
- #ifndef MAINWINDOW_H
- #define MAINWINDOW_H
-
- #include
-
- QT_BEGIN_NAMESPACE
- namespace Ui { class MainWindow; }
- QT_END_NAMESPACE
-
- class MainWindow : public QMainWindow
- {
- Q_OBJECT
-
- public:
- MainWindow(QWidget *parent = nullptr);
- ~MainWindow();
-
- signals:
- void startConnect(QString, unsigned short);
- void disConnect();
- void sendMsg(QByteArray msg);
-
- private slots:
- void on_connServer_clicked();
-
- void on_sendMsg_clicked();
-
- void on_disconnServer_clicked();
-
- private:
- Ui::MainWindow *ui;
- };
- #endif // MAINWINDOW_H
- #include "mainwindow.h"
- #include "ui_mainwindow.h"
- #include
- #include
- #include
- #include "recvfile.h"
-
- //多线程中,主线程负责相应ui事件,创建子线程,子线程处理服务端发过来的数据
- //QT中通信的套接字是不能跨线程访问的,所以客户端中用于通信的套接字是不能在
- //主线程创建的,需要在子线程创建通讯套接字,主线程把ip和端口传输给子线程
- //子线程new一个QTcpSocket的对象,然后子线程用这个通讯套接字接受服务端
- //发过来的数据
- MainWindow::MainWindow(QWidget *parent)
- : QMainWindow(parent)
- , ui(new Ui::MainWindow)
- {
- ui->setupUi(this);
- ui->IP->setText("127.0.0.1");
- ui->port->setText("9521");
- ui->disconnServer->setDisabled(true);
- //创建子线程
- QThread *subThread = new QThread;
- //重点说明:此处new RecvFile时候不能给其指定父对象
- //否则worker移动到子线程就会不生效,谨记谨记!!!
- RecvFile* worker = new RecvFile;
- worker->moveToThread(subThread);
- connect(this, &MainWindow::startConnect, worker, &RecvFile::connectServer);
- //连接服务端成功
- connect(worker, &RecvFile::connectOk, this, [=]() {
- //QMessageBox::information(this, "提示", "成功连接了服务器");
- ui->content->append("成功连接了服务器");
- ui->disconnServer->setDisabled(false);
- ui->connServer->setDisabled(true);
- });
- connect(worker, &RecvFile::message, this, [=](QByteArray msg) {
- QVector
colors = { - Qt::red, Qt::green, Qt::black, Qt::blue,
- Qt::darkRed, Qt::cyan, Qt::magenta
- };
- int index = QRandomGenerator::global()->bounded(colors.size());
- ui->content->setTextColor(colors.at(index));
- ui->content->append("收到消息:" + msg);
- });
- connect(worker, &RecvFile::gameOver, this, [=]() {
- qDebug() << "主线程销毁子线程";
- subThread->quit();
- subThread->wait();
- subThread->deleteLater();
- worker->deleteLater();
- });
- connect(worker, &RecvFile::disconnected, this, [=] {
- ui->content->append("与服务器断开了连接");
- ui->disconnServer->setDisabled(true);
- ui->connServer->setDisabled(false);
- });
- //断开服务器连接
- connect(this, &MainWindow::disConnect, worker, &RecvFile::disConnect);
- //给服务器发消息
- connect(this, &MainWindow::sendMsg, worker, &RecvFile::sendMsg);
- //start之后,线程仍然不能正常工作,仍然需要信号触发
- subThread->start();
- qDebug() << "主线程id:" << QThread::currentThread();
- }
-
- MainWindow::~MainWindow()
- {
- delete ui;
- }
-
-
- void MainWindow::on_connServer_clicked()
- {
- QString ip = ui->IP->text();
- unsigned short port = ui->port->text().toUShort();
- emit startConnect(ip, port);
- }
-
- void MainWindow::on_sendMsg_clicked()
- {
- QString msg = ui->msg->text();
- emit sendMsg(msg.toUtf8());
- ui->content->append("发出消息:" + msg);
- }
-
- void MainWindow::on_disconnServer_clicked()
- {
- emit disConnect();
- ui->disconnServer->setDisabled(true);
- ui->connServer->setDisabled(false);
- }
-
- #ifndef RECVFILE_H
- #define RECVFILE_H
-
- #include
- #include
- #include
-
- class RecvFile : public QObject
- {
- Q_OBJECT
- public:
- explicit RecvFile(QObject *parent = nullptr);
- void connectServer(QString ip, unsigned short port);
- void dealData();
- void disConnect();
- void sendMsg(QByteArray msg);
-
- signals:
- void connectOk();
- void message(QByteArray msg);
- void gameOver();
- void disconnected();
-
- private:
- QTcpSocket *m_tcpScoket;
- };
-
- #endif // RECVFILE_H
- #include "recvfile.h"
- #include
- #include
-
- RecvFile::RecvFile(QObject *parent)
- : QObject{parent}
- {
-
- }
-
- void RecvFile::connectServer(QString ip, unsigned short port) {
- qDebug() << "子线程ID:" << QThread::currentThread();
- m_tcpScoket = new QTcpSocket;
- //该函数是非阻塞函数,所有需要connect来注册回调,告诉我连接ok
- m_tcpScoket->connectToHost(QHostAddress(ip), port);
- connect(m_tcpScoket, &QTcpSocket::connected, this, &RecvFile::connectOk);
- //连接之后怎么怎么知道有数据到来呢,继续connect连接来通知我,槽使用匿名槽函数
- connect(m_tcpScoket, &QTcpSocket::readyRead, this, [=](){
- //QByteArray all = m_tcpScoket->readAll();
- //emit message(all);
- dealData();
- //emit gameOver();
- });
- connect(m_tcpScoket, &QTcpSocket::disconnected, this, &RecvFile::disconnected);
- }
-
- void RecvFile::dealData() {
- unsigned int totalBytes = 0;
- unsigned int recvBytes = 0;
- QByteArray block;
-
- if (0 == m_tcpScoket->bytesAvailable()) {
- qDebug() << "没有数据";
- return;
- }
- if (m_tcpScoket->bytesAvailable() >= sizeof(int)) {
- QByteArray head = m_tcpScoket->read(sizeof(int));
- //网络字节序转小端
- totalBytes = qFromBigEndian(*(int*)head.data());
- qDebug() << "接收数据长度::" << totalBytes;
- }
- else {
- return;
- }
- while (totalBytes -recvBytes > 0 && m_tcpScoket->bytesAvailable()) {
- block.append(m_tcpScoket->read(totalBytes - recvBytes));
- recvBytes = block.size();
- }
- if (totalBytes == recvBytes) {
- emit message(block);
- }
- //如果还有数据继续读取
- if (m_tcpScoket->bytesAvailable() > 0) {
- dealData();
- qDebug() << "递归调用数据接收";
- }
- }
-
- void RecvFile::disConnect() {
- //关闭连接,销毁自己
- m_tcpScoket->close();
- //m_tcpScoket->deleteLater();
- }
-
- //给服务器发消息
- void RecvFile::sendMsg(QByteArray msg) {
- m_tcpScoket->write(msg);
- }
(1)不能在子线程直接操作ui对象,而是通过发送信号给主线程间接操作ui对象
(2)QT处理大端和小端字节序的问题,提供了如下两组四个函数:
组一(工作于小端机器): qToBigEndian (小端转大端) qFromBigEndian (大端转小端)
组二(工作于大端机器): qToLittleEndian (大端转小端) qFromLittleEndian (小端转大端)
其中组一用在小端机器上,组二用在大端机,简单记忆后缀是BigEndian工作在小段机器,后缀是LittleEndian工作在大端机器,工作机器刚好和后缀相反。
(3)QT有父子对象树机制来回收内存,父对象析构时会先析构子对象。
(4)多线程中,主线程负责相应ui事件,创建子线程,子线程处理服务端发过来的数据QT中通信的套接字是不能跨线程访问的,所以客户端中用于通信的套接字是不能在主线程创建的,需要在子线程创建通讯套接字,主线程把ip和端口传输给子线程子线程new一个QTcpSocket的对象,然后子线程用这个通讯套接字接受服务端发过来的数据
最后附上源码下载地址: https://download.csdn.net/download/hsy12342611/87178417