QT下TCP协议实现数据网络传输
创始人
2024-04-13 21:45:01
0

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

1.运行效果

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

2.关键代码:

2.1服务端代码

2.1.1 服务端主函数

#include "mainwindow.h"#include int main(int argc, char *argv[])
{QApplication a(argc, argv);MainWindow w;w.show();return a.exec();
}

2.1.2 QTcpServer实现类

头文件和实现:

#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);
}

2.1.3 主窗口类

#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include 
#include "MyTcpServer.h"
#include QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic: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);//创建服务器socketm_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//不用deletem_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());
}

2.1.4 网络处理线程类

#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_tcpConnm_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编码,所以此处指定编码GBKQTextCodec *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());在某些环境转换长度就为0uint32_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_tcpConnm_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编码,此处指定编码方式为UTF8QTextCodec *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);
}

2.1.5 pro工程文件

QT       += core gui networkgreaterThan(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.0SOURCES += \main.cpp \mainwindow.cpp \mytcpserver.cpp \sendfile.cppHEADERS += \mainwindow.h \mytcpserver.h \sendfile.hFORMS += \mainwindow.ui# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += targetRESOURCES += \res.qrc

2.2客户端代码

2.2.1 主函数

#include "mainwindow.h"#include int main(int argc, char *argv[])
{QApplication a(argc, argv);MainWindow w;w.show();return a.exec();
}

2.2.2 主窗口类

#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic: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);
}

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

#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来注册回调,告诉我连接okm_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);
}

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

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
AWSECS:哪种网络模式具有... 使用AWS ECS中的awsvpc网络模式来获得最佳性能。awsvpc网络模式允许ECS任务直接在V...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...