• QT下TCP协议实现数据网络传输


    QT开发框架以其跨平台的优势,在全世界IT界如雷贯耳。其封装了功能齐全的各种类,大大的提高了开发者的效率。本篇内容将介绍如何使用QT 6.4.1框架开发服务器和客户端程序,让两端能够首发消息,服务端往客户端发送文件(客户端往服务器发送类似,没有实现)。

    1.运行效果

     说明:首先运行同时运行客户端和服务端程序,服务绑定端口开启服务,客户端连接服务器。然后服务器和客户端互相打招呼,然后服务器给客户端发送一首唐诗。

    2.关键代码:

    2.1服务端代码

    2.1.1 服务端主函数

    1. #include "mainwindow.h"
    2. #include
    3. int main(int argc, char *argv[])
    4. {
    5. QApplication a(argc, argv);
    6. MainWindow w;
    7. w.show();
    8. return a.exec();
    9. }

    2.1.2 QTcpServer实现类

    头文件和实现:

    1. #ifndef MYTCPSERVER_H
    2. #define MYTCPSERVER_H
    3. #include
    4. #include
    5. class MyTcpServer : public QTcpServer
    6. {
    7. Q_OBJECT
    8. public:
    9. explicit MyTcpServer(QObject *parent = nullptr);
    10. //该函数是由框架调用
    11. void incomingConnection(qintptr socketDescriptor);
    12. signals:
    13. void newClient(qintptr socket);
    14. };
    15. #endif // MYTCPSERVER_H
    1. #include "mytcpserver.h"
    2. MyTcpServer::MyTcpServer(QObject *parent)
    3. : QTcpServer{parent}
    4. {
    5. }
    6. //该函数是由框架调用
    7. void MyTcpServer::incomingConnection(qintptr socketDescriptor)
    8. {
    9. emit newClient(socketDescriptor);
    10. }

    2.1.3 主窗口类

    1. #ifndef MAINWINDOW_H
    2. #define MAINWINDOW_H
    3. #include
    4. #include "MyTcpServer.h"
    5. #include
    6. QT_BEGIN_NAMESPACE
    7. namespace Ui { class MainWindow; }
    8. QT_END_NAMESPACE
    9. class MainWindow : public QMainWindow
    10. {
    11. Q_OBJECT
    12. public:
    13. MainWindow(QWidget *parent = nullptr);
    14. ~MainWindow();
    15. signals:
    16. void start(QString file);
    17. void sendMsg(QByteArray msg);
    18. private slots:
    19. void on_start_clicked();
    20. void on_selectFile_clicked();
    21. void on_sendMsg_clicked();
    22. void on_transferFile_clicked();
    23. private:
    24. Ui::MainWindow *ui;
    25. MyTcpServer *m_server;
    26. QLabel *m_status;
    27. };
    28. #endif // MAINWINDOW_H
    1. #include "mainwindow.h"
    2. #include "ui_mainwindow.h"
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include "sendfile.h"
    10. //alt +回车添加自动添加头文件
    11. MainWindow::MainWindow(QWidget *parent)
    12. : QMainWindow(parent)
    13. , ui(new Ui::MainWindow)
    14. {
    15. ui->setupUi(this);
    16. ui->lineEditPort->setText("9521");
    17. ui->lineFile->setText("C:\\Users\\Administrator\\Desktop\\OKR设定.txt");
    18. ui->progressBar->setRange(0, 100);
    19. ui->progressBar->setValue(0);
    20. //创建服务器socket
    21. m_server = new MyTcpServer(this);
    22. connect(m_server, &MyTcpServer::newClient, this, [=](qintptr socket) {
    23. ui->textEditContent->append("检测到有新的客户端连接");
    24. //有新连接
    25. m_status->setPixmap(QPixmap(":/ok.png").scaled(20, 20));
    26. QThread *subThread = new QThread;
    27. SendFile* worker = new SendFile(socket);
    28. worker->moveToThread(subThread);
    29. //子线程工作是需要主线程给其发信号通知工作,此处用来通知子线程给客户端发送文件
    30. connect(this, &MainWindow::start, worker, &SendFile::working);
    31. connect(worker, &SendFile::done, this, [=](){
    32. ui->textEditContent->append("检测到客户端退出");
    33. //客户端断开
    34. m_status->setPixmap(QPixmap(":/error.png").scaled(20, 20));
    35. qDebug() << "销毁子线程资源";
    36. subThread->quit(); //通知退出
    37. subThread->wait(); //等任务做完
    38. subThread->deleteLater(); //相当于对new的对象进行delete操作
    39. worker->deleteLater();
    40. });
    41. //不能在子线程操作ui对象,读和写都不行,需要子线程通过信号通知ui线程间接操作ui对象
    42. connect(worker, &SendFile::text, this, [=](QByteArray msg){
    43. QVector colors = {
    44. Qt::red, Qt::green, Qt::black, Qt::blue,
    45. Qt::darkRed, Qt::cyan, Qt::magenta
    46. };
    47. int index = QRandomGenerator::global()->bounded(colors.size());
    48. ui->textEditContent->setTextColor(colors.at(index));
    49. ui->textEditContent->append(msg);
    50. });
    51. //显示客户端发来的消息
    52. connect(worker, &SendFile::showRecvMsg, this, [=](QByteArray msg){
    53. ui->textEditContent->append("收到消息:" + msg);
    54. });
    55. //通知子线程发送一行文本
    56. connect(this, &MainWindow::sendMsg, worker, &SendFile::sendMsg);
    57. connect(worker, &SendFile::updateProgressBar, this, [=](int value){
    58. ui->progressBar->setValue(value);
    59. });
    60. subThread->start();
    61. });
    62. //处理状态栏,new一个QLabel,可以制定父对象,也可以不指定,因为需要把
    63. //它设置到状态栏中,设置后其父对象默认就是状态栏,状态栏析构后就会回收QLabel
    64. //不用delete
    65. m_status = new QLabel;
    66. m_status->setPixmap(QPixmap(":/error.png").scaled(20, 20));
    67. ui->statusbar->addWidget(new QLabel("状态:"));
    68. ui->statusbar->addWidget(m_status);
    69. }
    70. MainWindow::~MainWindow()
    71. {
    72. delete ui;
    73. }
    74. void MainWindow::on_start_clicked()
    75. {
    76. unsigned short port = ui->lineEditPort->text().toUShort();
    77. //绑定端口等待客户端连接,有客户端连接触发信号:[signal] void QTcpServer::newConnection()
    78. //QT中用于通信的QTcpSocket套接字对象不能跨线程访问
    79. m_server->listen(QHostAddress::Any, port);
    80. ui->start->setDisabled(true);
    81. }
    82. void MainWindow::on_selectFile_clicked()
    83. {
    84. QString file = QFileDialog::getOpenFileName(this);
    85. if (!file.isEmpty()) {
    86. ui->lineFile->setText(file);
    87. }
    88. }
    89. void MainWindow::on_sendMsg_clicked()
    90. {
    91. QString msg = ui->lineEditMsg->text();//.toUShort();
    92. emit sendMsg(msg.toUtf8());
    93. ui->textEditContent->append("发出消息:" + msg);
    94. }
    95. void MainWindow::on_transferFile_clicked()
    96. {
    97. if (ui->lineFile->text().isEmpty()) {
    98. QMessageBox::information(this, "提示", "请选择要发送的文件");
    99. return;
    100. }
    101. emit start(ui->lineFile->text());
    102. }

    2.1.4 网络处理线程类

    1. #ifndef SENDFILE_H
    2. #define SENDFILE_H
    3. #include
    4. #include
    5. #include
    6. #include
    7. class SendFile : public QObject
    8. {
    9. Q_OBJECT
    10. public:
    11. explicit SendFile(qintptr socket, QObject *parent = nullptr);
    12. void working(QString file);
    13. void sendMsg(QByteArray msg);
    14. signals:
    15. void done();
    16. void text(QByteArray msg);
    17. void showRecvMsg(QByteArray msg);
    18. void updateProgressBar(int value);
    19. private:
    20. qintptr m_socket;
    21. QTcpSocket* m_tcpConn;
    22. };
    23. #endif // SENDFILE_H
    1. #include "sendfile.h"
    2. #include
    3. #include
    4. #include
    5. #include
    6. SendFile::SendFile(qintptr socket, QObject *parent)
    7. : QObject{parent}
    8. {
    9. //传递过来了用于通信的套接字,就可以和对端通信了
    10. m_socket = socket;
    11. }
    12. void SendFile::working(QString file) {
    13. qDebug() << "子线程ID: " << QThread::currentThread();
    14. //注意:m_tcpConn的创建要在子线程中
    15. m_tcpConn = new QTcpSocket;
    16. //用文件描述符初始化m_tcpConn
    17. m_tcpConn->setSocketDescriptor(m_socket);
    18. //检测客户端断开
    19. connect(m_tcpConn, &QTcpSocket::disconnected, this, [=]() {
    20. m_tcpConn->close();
    21. m_tcpConn->deleteLater();
    22. emit done();
    23. qDebug() << "检测到客户端退出,我的资源也进行了销毁,再见!";
    24. });
    25. //检测客户端发来数据
    26. connect(m_tcpConn, &QTcpSocket::readyRead, this, [=]() {
    27. //接收数据并把数据发送给主线程进行显示
    28. QByteArray data = m_tcpConn->readAll();
    29. emit showRecvMsg(data);
    30. });
    31. qDebug() << "传输的文件: " << file;
    32. QFile opFile(file);
    33. bool ret = opFile.open(QFile::ReadOnly);
    34. if (ret) {
    35. emit updateProgressBar(0);
    36. //此处打开的是windows下的文件,windows默认是GBK编码,所以此处指定编码GBK
    37. QTextCodec *codec = QTextCodec::codecForName("GBK");
    38. qint64 fileSize = opFile.size();
    39. qint64 sendSize = 0;
    40. while (!opFile.atEnd()) {
    41. //自定格式发包,将发送长度转换成大端存储发送到对端
    42. QByteArray line = opFile.readLine();
    43. sendSize += line.size();
    44. QString strUnicode = codec->toUnicode(line);
    45. QByteArray utf8line = strUnicode.toUtf8();
    46. //以下代码写成两行比较稳定,如果写成
    47. //uint32_t len = qToBigEndian(utf8line.size());在某些环境转换长度就为0
    48. uint32_t orgLen = utf8line.size();
    49. uint32_t len = qToBigEndian(orgLen);
    50. QByteArray data((char*)&len, 4);
    51. data.append(utf8line);
    52. //发送一行数据给对端
    53. m_tcpConn->write(data);
    54. emit text(utf8line);
    55. emit updateProgressBar(100 * sendSize / fileSize);
    56. //为了减轻对端压力,休眠一会
    57. QThread::sleep(1);
    58. }
    59. }
    60. opFile.close();
    61. }
    62. void SendFile::sendMsg(QByteArray msg) {
    63. //用文件描述符初始化m_tcpConn
    64. m_tcpConn = new QTcpSocket;
    65. m_tcpConn->setSocketDescriptor(m_socket);
    66. //检测客户端断开
    67. connect(m_tcpConn, &QTcpSocket::disconnected, this, [=]() {
    68. m_tcpConn->close();
    69. m_tcpConn->deleteLater();
    70. emit done();
    71. });
    72. //检测客户端发来数据
    73. connect(m_tcpConn, &QTcpSocket::readyRead, this, [=]() {
    74. //接收数据并把数据发送给主线程进行显示
    75. QByteArray data = m_tcpConn->readAll();
    76. emit showRecvMsg(data);
    77. });
    78. //QT默认是UTF8编码,所以输入框是UTF8编码,此处指定编码方式为UTF8
    79. QTextCodec *codec = QTextCodec::codecForName("UTF8");
    80. QString strUnicode = codec->toUnicode(msg);
    81. QByteArray utf8line = strUnicode.toUtf8();
    82. uint32_t orgLen = utf8line.size();
    83. uint32_t len = qToBigEndian(orgLen);
    84. //拼接头:表示长度
    85. QByteArray data((char*)&len, 4);
    86. //拼接发送内容
    87. data.append(utf8line);
    88. m_tcpConn->write(data);
    89. }

    2.1.5 pro工程文件

    1. QT += core gui network
    2. greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
    3. greaterThan(QT_MAJOR_VERSION, 5): QT += core5compat
    4. CONFIG += c++17 console
    5. # You can make your code fail to compile if it uses deprecated APIs.
    6. # In order to do so, uncomment the following line.
    7. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
    8. SOURCES += \
    9. main.cpp \
    10. mainwindow.cpp \
    11. mytcpserver.cpp \
    12. sendfile.cpp
    13. HEADERS += \
    14. mainwindow.h \
    15. mytcpserver.h \
    16. sendfile.h
    17. FORMS += \
    18. mainwindow.ui
    19. # Default rules for deployment.
    20. qnx: target.path = /tmp/$${TARGET}/bin
    21. else: unix:!android: target.path = /opt/$${TARGET}/bin
    22. !isEmpty(target.path): INSTALLS += target
    23. RESOURCES += \
    24. res.qrc

    2.2客户端代码

    2.2.1 主函数

    1. #include "mainwindow.h"
    2. #include
    3. int main(int argc, char *argv[])
    4. {
    5. QApplication a(argc, argv);
    6. MainWindow w;
    7. w.show();
    8. return a.exec();
    9. }

    2.2.2 主窗口类

    1. #ifndef MAINWINDOW_H
    2. #define MAINWINDOW_H
    3. #include
    4. QT_BEGIN_NAMESPACE
    5. namespace Ui { class MainWindow; }
    6. QT_END_NAMESPACE
    7. class MainWindow : public QMainWindow
    8. {
    9. Q_OBJECT
    10. public:
    11. MainWindow(QWidget *parent = nullptr);
    12. ~MainWindow();
    13. signals:
    14. void startConnect(QString, unsigned short);
    15. void disConnect();
    16. void sendMsg(QByteArray msg);
    17. private slots:
    18. void on_connServer_clicked();
    19. void on_sendMsg_clicked();
    20. void on_disconnServer_clicked();
    21. private:
    22. Ui::MainWindow *ui;
    23. };
    24. #endif // MAINWINDOW_H
    1. #include "mainwindow.h"
    2. #include "ui_mainwindow.h"
    3. #include
    4. #include
    5. #include
    6. #include "recvfile.h"
    7. //多线程中,主线程负责相应ui事件,创建子线程,子线程处理服务端发过来的数据
    8. //QT中通信的套接字是不能跨线程访问的,所以客户端中用于通信的套接字是不能在
    9. //主线程创建的,需要在子线程创建通讯套接字,主线程把ip和端口传输给子线程
    10. //子线程new一个QTcpSocket的对象,然后子线程用这个通讯套接字接受服务端
    11. //发过来的数据
    12. MainWindow::MainWindow(QWidget *parent)
    13. : QMainWindow(parent)
    14. , ui(new Ui::MainWindow)
    15. {
    16. ui->setupUi(this);
    17. ui->IP->setText("127.0.0.1");
    18. ui->port->setText("9521");
    19. ui->disconnServer->setDisabled(true);
    20. //创建子线程
    21. QThread *subThread = new QThread;
    22. //重点说明:此处new RecvFile时候不能给其指定父对象
    23. //否则worker移动到子线程就会不生效,谨记谨记!!!
    24. RecvFile* worker = new RecvFile;
    25. worker->moveToThread(subThread);
    26. connect(this, &MainWindow::startConnect, worker, &RecvFile::connectServer);
    27. //连接服务端成功
    28. connect(worker, &RecvFile::connectOk, this, [=]() {
    29. //QMessageBox::information(this, "提示", "成功连接了服务器");
    30. ui->content->append("成功连接了服务器");
    31. ui->disconnServer->setDisabled(false);
    32. ui->connServer->setDisabled(true);
    33. });
    34. connect(worker, &RecvFile::message, this, [=](QByteArray msg) {
    35. QVector colors = {
    36. Qt::red, Qt::green, Qt::black, Qt::blue,
    37. Qt::darkRed, Qt::cyan, Qt::magenta
    38. };
    39. int index = QRandomGenerator::global()->bounded(colors.size());
    40. ui->content->setTextColor(colors.at(index));
    41. ui->content->append("收到消息:" + msg);
    42. });
    43. connect(worker, &RecvFile::gameOver, this, [=]() {
    44. qDebug() << "主线程销毁子线程";
    45. subThread->quit();
    46. subThread->wait();
    47. subThread->deleteLater();
    48. worker->deleteLater();
    49. });
    50. connect(worker, &RecvFile::disconnected, this, [=] {
    51. ui->content->append("与服务器断开了连接");
    52. ui->disconnServer->setDisabled(true);
    53. ui->connServer->setDisabled(false);
    54. });
    55. //断开服务器连接
    56. connect(this, &MainWindow::disConnect, worker, &RecvFile::disConnect);
    57. //给服务器发消息
    58. connect(this, &MainWindow::sendMsg, worker, &RecvFile::sendMsg);
    59. //start之后,线程仍然不能正常工作,仍然需要信号触发
    60. subThread->start();
    61. qDebug() << "主线程id:" << QThread::currentThread();
    62. }
    63. MainWindow::~MainWindow()
    64. {
    65. delete ui;
    66. }
    67. void MainWindow::on_connServer_clicked()
    68. {
    69. QString ip = ui->IP->text();
    70. unsigned short port = ui->port->text().toUShort();
    71. emit startConnect(ip, port);
    72. }
    73. void MainWindow::on_sendMsg_clicked()
    74. {
    75. QString msg = ui->msg->text();
    76. emit sendMsg(msg.toUtf8());
    77. ui->content->append("发出消息:" + msg);
    78. }
    79. void MainWindow::on_disconnServer_clicked()
    80. {
    81. emit disConnect();
    82. ui->disconnServer->setDisabled(true);
    83. ui->connServer->setDisabled(false);
    84. }

    2.2.3 处理网络数据的子线程类

    1. #ifndef RECVFILE_H
    2. #define RECVFILE_H
    3. #include
    4. #include
    5. #include
    6. class RecvFile : public QObject
    7. {
    8. Q_OBJECT
    9. public:
    10. explicit RecvFile(QObject *parent = nullptr);
    11. void connectServer(QString ip, unsigned short port);
    12. void dealData();
    13. void disConnect();
    14. void sendMsg(QByteArray msg);
    15. signals:
    16. void connectOk();
    17. void message(QByteArray msg);
    18. void gameOver();
    19. void disconnected();
    20. private:
    21. QTcpSocket *m_tcpScoket;
    22. };
    23. #endif // RECVFILE_H
    1. #include "recvfile.h"
    2. #include
    3. #include
    4. RecvFile::RecvFile(QObject *parent)
    5. : QObject{parent}
    6. {
    7. }
    8. void RecvFile::connectServer(QString ip, unsigned short port) {
    9. qDebug() << "子线程ID:" << QThread::currentThread();
    10. m_tcpScoket = new QTcpSocket;
    11. //该函数是非阻塞函数,所有需要connect来注册回调,告诉我连接ok
    12. m_tcpScoket->connectToHost(QHostAddress(ip), port);
    13. connect(m_tcpScoket, &QTcpSocket::connected, this, &RecvFile::connectOk);
    14. //连接之后怎么怎么知道有数据到来呢,继续connect连接来通知我,槽使用匿名槽函数
    15. connect(m_tcpScoket, &QTcpSocket::readyRead, this, [=](){
    16. //QByteArray all = m_tcpScoket->readAll();
    17. //emit message(all);
    18. dealData();
    19. //emit gameOver();
    20. });
    21. connect(m_tcpScoket, &QTcpSocket::disconnected, this, &RecvFile::disconnected);
    22. }
    23. void RecvFile::dealData() {
    24. unsigned int totalBytes = 0;
    25. unsigned int recvBytes = 0;
    26. QByteArray block;
    27. if (0 == m_tcpScoket->bytesAvailable()) {
    28. qDebug() << "没有数据";
    29. return;
    30. }
    31. if (m_tcpScoket->bytesAvailable() >= sizeof(int)) {
    32. QByteArray head = m_tcpScoket->read(sizeof(int));
    33. //网络字节序转小端
    34. totalBytes = qFromBigEndian(*(int*)head.data());
    35. qDebug() << "接收数据长度::" << totalBytes;
    36. }
    37. else {
    38. return;
    39. }
    40. while (totalBytes -recvBytes > 0 && m_tcpScoket->bytesAvailable()) {
    41. block.append(m_tcpScoket->read(totalBytes - recvBytes));
    42. recvBytes = block.size();
    43. }
    44. if (totalBytes == recvBytes) {
    45. emit message(block);
    46. }
    47. //如果还有数据继续读取
    48. if (m_tcpScoket->bytesAvailable() > 0) {
    49. dealData();
    50. qDebug() << "递归调用数据接收";
    51. }
    52. }
    53. void RecvFile::disConnect() {
    54. //关闭连接,销毁自己
    55. m_tcpScoket->close();
    56. //m_tcpScoket->deleteLater();
    57. }
    58. //给服务器发消息
    59. void RecvFile::sendMsg(QByteArray msg) {
    60. m_tcpScoket->write(msg);
    61. }

    3.开发注意事项

    (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

  • 相关阅读:
    【Linux成长史】Linux基本指令大全
    4 轮拿下字节 Offer,面试题复盘
    基于Android studio+SSH的单词记忆(背单词)APP设计
    The DAO事件始末
    【外汇天眼】解析外汇交易平台:深度了解DD与NDD两大模式
    系统韧性研究(1)| 何谓「系统韧性」?
    C++标准模板(STL)- 输入/输出操纵符-(std::setprecision,std::setw)
    如何在ASO中使用App Store的促销文本
    Linux检查端口nmap
    AUTOPOI导入Excel文件(可获取表头数据)
  • 原文地址:https://blog.csdn.net/hsy12342611/article/details/128068651