无论是TCP还是UDP通信, 其通信流程都是一样的, 只是使用的语言可能不一样, 而后面要用的QT, 也只是在QT中对通信流程进行封装, 让操作更加简单, 其实底层的通信原理和通信的过程是完全相同的;
如果要进行网络通信, 它和语言是没有任何关系的, 要进行网络通信, 只需要关注到底是基于TCP还是UDP(两个传输层的协议)通信;
在传输层协议定下来后, 往上还有应用层(FTP协议,HTTP协议)
负责监听, 并且接受客户端的连接;
Public Functions
—— 公共成员函数构造一个用于监听的服务器端对象
QTcpServer::QTcpServer(QObject *parent = Q_NULLPTR)
构造一个用于监听的服务器端对象
, 这个构造函数接收一个参数, QObject *parent = Q_NULLPTR
, 即指定一个父对象;// 设置监听 -》 对应前面TCP通信流程中的服务器的bind和listen两步(先绑定再设置监听)
bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)
QHostAddress
类封装IPV4, IPV6
bool QTcpServer::isListening() const
// 若当前的服务器对象已经开始监听就返回true, 否则false;
// 只有服务器端开始监听, 才知道有客户端连接, 并且和客户端建立连接
QHostAddress QTcpServer::serverAddress() const
: 如果当前服务器对象正在监听, 则返回监听的服务器地址信息, 否则返回QHostAddress::Null
;quint16 QTcpServer::serverPort() const
:如果当前服务器对象正在监听连接, 则返回服务器的端口, 否则返回0;nextPendingConnection()
: 返回下一个挂起的连接作为已连接的QTcpSocket对象。[virtual] QTcpSocket *QTcpServer::nextPendingConnection()
:当服务器启动监听之后, 若有客户端连接上来, 那么服务器就会与客户端建立连接, 建立连接之后就会有一个用于通信的套接字对象;注意
一下, 虽然返回的是一个指针, 但是地址在函数体内部分配出来, 所以这里指针指向一块有效的堆内存;所以, 用完后要释放, 你可以自己释放掉,也可以让服务器对象自己释放:前面讲过了对象树的概念, 这里的QTcpSocket对象是由QTcpServer(父对象)对象返回的, 二者是父子对象关系, 这就组成了一个对象树, 当QTcpServer对象析构的时候会先析构QTcpSocket对象(结构上的父子关系 ),保证了内存不泄漏,但是这两者是没有继承关系的;waitForNewConnection(int msec = 0, bool *timedOut = Q_NULLPTR)
bool QTcpServer::waitForNewConnection(int msec = 0, bool *timedOut = Q_NULLPTR)
:这个函数是一个阻塞函数, 当服务器端启动监听之后, 调用这个函数会阻塞当前的服务器线程, 阻塞线程等待客户端的连接, 若没有客户端来连接, 就会一直阻塞住, 所以可以指定阻塞时长msec(毫秒), 当时间到了还没有客户端连接, 函数就会解除阻塞;msec
:指定阻塞的最大时长, 单位为毫秒(ms);timedOut
:传出参数, 如果操作超时timedOut为true, 没有超时为false;(这里是要传入地址的, 因为是bool *)[signal] void QTcpServer::newConnection()
newConnection()
信号, 然后connect绑定槽函数, 在槽函数中(处理对应的新连接)调用nextPendingConnection()
, 得到一个用于通信的套接字对象, 基于这个对象去接收和发送数据;[signal] void QTcpServer::acceptError(QAbstractSocket::SocketError socketError)
QTcpSocket
, 关于这个类的使用有两种情况:
nextpendingConnection()
, 得到直接用于通信的QTcpSocket;QTcpSocket
对象, 创建出来的这个对象不能直接用来通信, 需要先连接服务器, 连接成功之后才能和服务器进行通信;
QTcpSocket
如何连接服务器;
connectToHost
:连接服务器[virtual] void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol)
:
[virtual] void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite)
:
如何基于
QTcpSocket
进行通信, 通信即是指数据的读或写;
QTcpSocket::QTcpSocket(QObject *parent = Q_NULLPTR);
: 创建一个状态为UnconnectedState的QTcpSocket对象。connectToHost
需要指定服务器绑定的IP和端口信息
继承自QAbstractSocket
[virtual] void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol)
:
[virtual] void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite)
:
在 Qt 中不管调用读操作函数接收数据,还是调用写函数发送数据,操作的对象都是本地的由 Qt 框架维护的一块内存。所以,调用了发送函数,数据不一定会马上被发送到网络中,调用了接收函数也不是直接从网络中接收数据。
qint64 QIODevice::read(char *data, qint64 maxSize);
: 指定可接收的最大字节数 maxSize 的数据到指针 data 指向的内存中;QByteArray QIODevice::read(qint64 maxSize);
:指定可接收的最大字节数 maxSize,返回接收的字符串;QByteArray QIODevice::readAll();
:将当前可用操作数据全部读出,通过返回值返回读出的字符串;qint64 QIODevice::write(const char *data, qint64 maxSize);
:发送指针 data 指向的内存中的 maxSize 个字节的数据
qint64 QIODevice::write(const char *data);
: 发送指针 data 指向的内存中的数据,字符串以 ‘\0’ 作为结束标记
qint64 QIODevice::write(const QByteArray &byteArray);
:发送参数指定的字符串
调用read和write操作的都是QT帮我们维护的这块内存, QT框架检测到有数据后, 会把数据发送到网络中,所以read和write不是直接操作的网络中的数据;
查阅帮助文档, 可以发现QTcpSocket所用到的信号都是从父类QAbstractSocket继承来的;
[signal] void QAbstractSocket::connected()
[signal] void QAbstractSocket::disconnected()
套接字对象(QAbstractSocket | QTcpSocket)
就会发射出disconnected信号;(检测对端有没有断开连接, 不是当前这端, 而是通信的对端)[signal] void QIODevice::readyRead()
readyRead()
信号, 说明对端发送的数据达到了, 之后就可以调用read函数
接收数据了;QTcpServer
对象(用于监听的套接字对象)QTcpServer
对象设置监听, 即:QTcpServer::listen()
: IP + 端口QTcpServer::newConnection()
信号检测是否有新的客户端连接;newConnection()
的信号, 说明有新的客户端连接, 就在newConnection
对应的槽函数中调用 [virtual] QTcpSocket *QTcpServer::nextPendingConnection()
得到通信的套接字对象;QTcpSocket
和客户端进行通信:QTcpSocket
中提供了接收和发送数据的函数read和write;并且在QTcpSocket
中提供了3个信号:
QTcpSocket
对象会发出readyRead
信号,在这个信号对应的槽函数中做对应的接收数据的操作read
即可;QTcpSocket
对象去检测当前的连接是否成功了:[signal] void QAbstractSocket::connected()
,这个连接的检测是在客户端进行检测的, 服务器端是不可以使用这个信号的;[signal] void QAbstractSocket::disconnected()
:无论在客户端或者服务器端都是可以使用的, 只要通信的两端(A, B), 其中的某一端(B)断开了连接, 那么在这一端(A)就可以通过TcpSocket
对象发射出这个信号, 通知A端, 对端B已经断开了连接;开始开发之前, 先在.pro工程文件中将network模块加入进去;记得保存刷新, 才能将模块重新导入;
另外, 在Qt Creator中, 提供了可以在UI设计界面, 直接右键控件转到槽(如下图所示:), 由系统自动帮我们去进行信号和槽的连接, 这里为了加深练习, 全部都统一使用手动连接信号和槽:
QTcpServer
;mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
#include
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QTcpServer * m_Server;
QTcpSocket * m_tcp;
QLabel * m_status; // 将状态图片放到QLabel中
bool isCon;
};
#endif // MAINWINDOW_H
mainwindow.c
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
// QT 中的connect执行完之后, 不代表对应的槽函数就执行了, QT中的connect是先进行了注册, 告诉QT框架, 假设有对应的事件发生了, 某个信号就会被发射出来
// 信号被发射出来, 某个对象就会接收到发射出来的信号, 且调用连接的槽函数来处理这个信号;
// 其实这个connect操作就是注册了一个回调, 当实际传输之后, 回调函数即槽函数才能被调用;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
isCon = false;
setWindowTitle("服务器端");
// 1. 创建监听的服务器对象, 将其实例化
// 这里可以指定父对象, 这样在当前MainWindow对象析构后, 会自动析构QTcpServer类型的对象(对象树)
m_Server = new QTcpServer(this);
// 先设置一个端口号, 方便测试
ui->port->setText("8899");
ui->btn_Close->setDisabled(true); // 当还没有启动监听的时候, 关闭监听的按钮就是不可用的
// 2. 点击按钮启动监听 | 槽函数用的是匿名函数的写法
connect(ui->btn_setListen, &QPushButton::clicked, this, [=](){
// 得到端口号, 转成无符号短整形
unsigned short port = ui->port->text().toUShort();
// 设置监听, 绑定本机任意IP地址, 还有port端口值
m_Server->listen(QHostAddress::Any, port);
// 当启动监听之后, 就让按钮变成不可用状态
ui->btn_setListen->setDisabled(true);
ui->btn_Close->setDisabled(false); // 关闭监听的按钮设置为可用的
});
// 3. 等待客户端的连接
// 当有客户端连接到来之后, QTcpServer对象就会发出一个newConnection信号(当有新连接建立, 就要修改一下连接状态为成功), 在槽函数中得到用于通信的QTcpSocket对象
connect(m_Server, &QTcpServer::newConnection, this, [=](){
// 通过得到的这个实例对象进行通信 | 定位到类名上, alt + 回车键可以帮我们自动补全头文件
m_tcp = m_Server->nextPendingConnection();
ui->record->append("已经成功和客户端进行连接......");
// 修改连接状态
m_status->setPixmap(QPixmap(":/success.png").scaled(20, 20));
isCon = true;
// 实例对象, 什么时候可以接收数据?
// 检测是否可以接收数据 | 当tcp对象发射出readyRead信号, 就可以接收数据了
// 在槽函数中进行数据的接收
connect(m_tcp, &QTcpSocket::readyRead, this, [=](){
// readAll 接收所有数据 | 读到客户端发来的所有数据, 放到历史记录框中
QByteArray data = m_tcp->readAll();
ui->record->append("客户端 say: " + data);
});
// 一切由QT框架维护
// 当QTcpSocket对象发出disconnected信号, 说明对端已经断开了连接
connect(m_tcp, &QTcpSocket::disconnected, this, [=](){
// 如果客户端和当前的服务器断开了连接 , 那么服务器也需要断开连接, 双向断开
// 关闭套接字 | 接着要去释放QTcpServer(m_tcp)指针指向的内存
m_tcp->close();
m_tcp->deleteLater(); // 里面封装了delete, 或者直接 delete m_tcp
// 修改状态栏
m_status->setPixmap(QPixmap(":/failed.png").scaled(20, 20));
isCon = false;
ui->record->append("服务器端已经和客户端断开了连接");
});
});
connect(ui->btn_Close, &QPushButton::clicked, this, [=](){
m_tcp->close();
ui->btn_Close->setDisabled(true); // 设置关闭监听的按钮为不可用;
ui->btn_setListen->setEnabled(true);
// ui->record->append("服务器端已经和客户端断开了连接");
});
// 4. 点击发送信息按钮, 发送信息给客户端
connect(ui->sendMsg, &QPushButton::clicked, this, [=](){
if (isCon)
{
QString msg = ui->msg->toPlainText(); // 把数据以文本形式都取出来
// 取出数据后要发送给客户端 | msg.toUtf8()将QString类型的转换成QByteArray类型
m_tcp->write(msg.toUtf8());
// 将发送出去的信息保存到历史记录文本框中
ui->record->append("服务器端 say:" + msg);
}
else
{
// 如果没有连接, 发送消息要提示
QMessageBox::critical(this, "错误提示", "没有连接上客户端, 发送消息失败");
}
});
// 5. 状态栏处理连接状态
// 实例化QLabel对象
m_status = new QLabel;
// 默认情况下未连接状态 | 因为存入的图片可能很大, 所以还要指定缩放
m_status->setPixmap(QPixmap(":/failed.png").scaled(20, 20));
m_status->setScaledContents(true); // 控件自适应图片大小
ui->statusbar->addWidget(new QLabel("连接状态:"));
// 将这个控件m_status添加到状态栏中
ui->statusbar->addWidget(m_status);
}
MainWindow::~MainWindow()
{
delete ui;
}
// 存在一个BUG 为什么有客户端先断开的连接, 服务器端再关闭监听, 就会闪退
QTcpSocket
对象:实例化后要去连接服务器, 否则无法进行套接字通信;QAbstractSocket::connectToHost()
;QTcpSocket
对象和服务器进行通信;按照通信流程走;
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QTcpSocket * m_tcp;
QLabel * m_status;
bool isConnect; // 标志位判断有没有断开连接
};
#endif // MAINWINDOW_H
mainwindow.c
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
#include
// 连接按钮和断开连接按钮互斥使用;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
isConnect = false;
// 设置默认的IP(本地回环地址)和端口地址
ui->port->setText("8899");
ui->IP->setText("127.0.0.1");
setWindowTitle("客户端");
// 1. 创建用于通信的套接字对象
m_tcp = new QTcpSocket(this);
ui->btn_Disconnect->setDisabled(true);
// 2. 连接服务器按钮
connect(ui->btn_Connect, &QPushButton::clicked, this, [=](){
QString ip = ui->IP->text();
unsigned short port = ui->port->text().toUShort();
// 连接服务器
m_tcp->connectToHost(QHostAddress(ip), port);
});
// 断开连接按钮
connect(ui->btn_Disconnect, &QPushButton::clicked, this, [=](){
m_tcp->close(); // 可以这里主动断开连接
isConnect = false;
ui->btn_Connect->setDisabled(false);
ui->btn_Disconnect->setDisabled(true);
});
// 3. 发送信息按钮
connect(ui->btn_SendMsg, &QPushButton::clicked, this, [=](){
if (isConnect)
{
QString msg = ui->sendMsg->toPlainText(); // 拿到发送信息框的内容
// 发送
m_tcp->write(msg.toUtf8());
// 将发送出去的信息放到历史记录框中
ui->histroyEdit->append("客户端 say:" + msg);
}
else
{
QMessageBox::critical(this, "错误提示", "两端断开连接, 消息发送不成功");
}
});
// 4. 当用于通信的Tcpsocket套接字对象发送出了一个readyRead信号就说明有数据到来了(所以这是被动的)
connect(m_tcp, &QTcpSocket::readyRead, this, [=](){
QByteArray data = m_tcp->readAll();
ui->histroyEdit->append("服务器端 say:" + data);
});
// 5. 当通信的时候, 怎么知道服务器已经断开连接了 | 当用于通信的套接字对象发射出一个disconnected信号
connect(m_tcp, &QTcpSocket::disconnected, this, [=](){
m_tcp->close(); // 双向断开连接
// 因为上面添加了父对象, 所以这里可以不用自己手动释放 | 而且在这里就释放了资源, 客户端断开连接,再次重连就会有问题
// m_tcp->deleteLater();
// 修改状态栏的状态
m_status->setPixmap(QPixmap(":/failed.png").scaled(20, 20));
ui->histroyEdit->append("服务器已经和客户端断开了连接 ");
ui->btn_Connect->setDisabled(false);
ui->btn_Disconnect->setDisabled(true);
isConnect = false;
});
// 6. 当通信的套接字对象发射出connected信号, 就说明连接成功
connect(m_tcp, &QTcpSocket::connected, this, [=](){
m_status->setPixmap(QPixmap(":/success.png").scaled(20, 20));
ui->histroyEdit->append("已经成功连接到了服务器......");
ui->btn_Connect->setDisabled(true);
ui->btn_Disconnect->setDisabled(false);
isConnect = true;
});
// 状态栏初始化
ui->statusbar->addWidget(new QLabel("连接状态:"));
m_status = new QLabel(this);
m_status->setPixmap(QPixmap(":/failed.png").scaled(20, 20));
ui->statusbar->addWidget(m_status);
}
MainWindow::~MainWindow()
{
delete ui;
}
- 根据IP和端口连接服务器, 当点击发送文件按钮后, 会将选择的磁盘文件传输给服务器端;
- 当服务器端接收完毕后, 服务器会断开连接, 客户端这边会发射出一个disconnected信号, 通知服务器已经接收完毕, 客户端断开连接;
- 以上通信流程都是在子线程中完成;
Qt中提供了两种线程的创建方式;步骤如下,我们使用其中的一种, 更为灵活的创建方式
moveToThread()
方法:-若前面创建的时候给work指定了父对象, 这里的移动就会失败
work->moveToThread(subThread); // 移动到子线程中工作;
start()
, 这时候线程启动了, 但是移动到线程中的对象是没有工作的;mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "sendfile.h"
#include
#include
#include
#include
#include
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->IP->setText("127.0.0.1");
ui->port->setText("8899");
// 对进度条初始化
ui->ProgressBar_trans->setRange(0, 100);
ui->ProgressBar_trans->setValue(0);
// 都没有指定父对象, 后面要记得自己手动析构
// 创建线程对象
QThread * sub = new QThread;
// 创建任务对象
SendFile * worker = new SendFile;
worker->moveToThread(sub);
// 1. 连接服务器
connect(ui->btn_Con, &QPushButton::clicked, this, [=](){
QString ip = ui->IP->text(); // 获取IP值
unsigned short port = ui->port->text().toUShort(); // 获取端口值, 并转换成无符号整型
// 发送信号, 让worker里面的任务函数开始执行;
// 在mainwindow中添加自定义的信号
// 发射出信号后, 让某个对应的任务函数开始执行
emit startConnect(port, ip);
});
// 若当前的窗口对象, 发射出一个startConnect信号,worker对象去接收这个信号, 调用类中的任务函数 | 信号和槽, 对应的参数要一致
connect(this, &MainWindow::startConnect, worker, &SendFile::connectServer);
// 工作对象接收信号
connect(this, &MainWindow::sendFileSignal, worker, &SendFile::sendFileToServer);
// 处理主线程发送的信号
connect(worker, &SendFile::connectOK, this, [=](){
QMessageBox::information(this, "连接服务器", "服务器和客户端连接成功");
});
// 处理主线程发送的信号 | 断开连接
connect(worker, &SendFile::gameOver, this, [=](){
QMessageBox::information(this, "连接服务器", "服务器和客户端断开连接");
// 进行资源的释放
sub->quit(); // 让线程退出
// 调用quit()之后,将线程停止了,但是如果用户再次触发这个线程的启动,那么会导致你delete 了一个正在运行的线程,因此需要wait()来等待QThread子线程的结束
sub->wait();
// 析构worker对象
worker->deleteLater();
sub->deleteLater(); // 线程对象析构
});
// 2. 选择文件
connect(ui->btn_selFile, &QPushButton::clicked, this, [=](){
// 弹出选择文件对话框
// 获得某个磁盘文件对应的绝对路径
QString path = QFileDialog::getOpenFileName();
// 容错处理, 若选择的路径是空的, 则停止后续操作
if (path.isEmpty())
{
QMessageBox::warning(this, "打开文件", "选择的文件路径不能为空");
return ;
}
// 将得到的路径设置到框中
ui->filePath->setText(path);
});
// 发送文件在子线程中执行, 所以主线程这里只需要发送信号给子线程中的工作对象让其完成发送文件的操作
connect(ui->btn_sendFile, &QPushButton::clicked, this, [=](){
emit sendFileSignal(ui->filePath->text());
});
// 更新进度条的显示数据
connect(worker, &SendFile::curPercent, ui->ProgressBar_trans, &QProgressBar::setValue);
sub->start(); // 启动子线程
}
MainWindow::~MainWindow()
{
delete ui;
}
mainwindow.h
#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();
private:
Ui::MainWindow *ui;
signals:
// 端口和IP
void startConnect(unsigned short, QString);
// 发送文件的信号
void sendFileSignal(QString);
};
#endif // MAINWINDOW_H
sendfile.cpp
:准备要在子线程中完成的工作, 连接服务器和发送文件给服务器#include "sendfile.h"
#include
#include
#include
SendFile::SendFile(QObject *parent) : QObject(parent)
{
}
// 当前是在子线程
void SendFile::connectServer(unsigned short port, QString IP)
{
m_tcp = new QTcpSocket;
m_tcp->connectToHost(QHostAddress(IP),port);
// 若连接成功, 要通知主线程, 主线程弹出提示框 | 这里需要添加自定义的信号进行通知
connect(m_tcp, &QTcpSocket::connected, this, &SendFile::connectOK);
// 什么时候断开连接
connect(m_tcp, &QTcpSocket::disconnected, this, [=](){
// 关闭套接字
m_tcp->close();
// 释放资源
m_tcp->deleteLater();
// 发送信号给主线程, 告诉主线程, 服务器已经和客户端断开了连接
emit gameOver();
});
}
void SendFile::sendFileToServer(QString path)
{
QFile file(path);
QFileInfo info(path);
// 如果打开文件成功, 再进行下一步
if (!file.open(QFile::ReadOnly))
{
return ;
}
int fileSize = info.size();
// 要把当前读取文件的进度, 发送给主窗口, 主线程根据当前的文件进度去更新进度条
// 服务器那边可以根据文件大小的总和已经达到去判断文件是否读取结束
// 若没有读完就一直读
while (!file.atEnd())
{
static int num = 0;
if (num == 0)
{
m_tcp->write((char *)&fileSize, 4);
}
// 读取一行就发送一行
QByteArray line = file.readLine();
num += line.size();
// 计算出百分比, 发送给主线程
int percent = (num * 100 / fileSize);
emit curPercent(percent);
m_tcp->write(line);
}
}
sendfile.h
#ifndef SENDFILE_H
#define SENDFILE_H
#include
#include
class SendFile : public QObject
{
Q_OBJECT
public:
explicit SendFile(QObject *parent = nullptr);
// 连接服务器
void connectServer(unsigned short port, QString IP);
// 发送文件 | Alt + 回车键, 在源文件中自动生成函数的定义, 进行填写逻辑代码即可
void sendFileToServer(QString path);
signals:
void connectOK(); // 发送通知
void gameOver();
void curPercent(int num);
private:
QTcpSocket * m_tcp;
};
#endif // SENDFILE_H
上面采用了Qt中提供的多线程使用方式的其中一种, 接下来在服务器端使用另外一种较为简单的使用方法, 因为服务器端程序没有太多的功能, 仅仅是在子线程中接收文件;
class MyThread:public QThread
{
......
}
fun()
方法, 在该函数内部编写子线程要处理的具体的业务流程class MyThread:public QThread
{
......
protected:
void run()
{
......
}
}
MyThread * sub = new MyThread;
start()
方法
start()
方法, 子线程中run()
方法就可以被执行;sub->start();
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
#include
#include
#include
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << "服务器主线程:" << QThread::currentThread();
ui->port->setText("8899");
m_Server = new QTcpServer(this);
// 启动监听
connect(ui->btn_setListen, &QPushButton::clicked, this, [=]()
{
unsigned short port = ui->port->text().toUShort();
m_Server->listen(QHostAddress::Any, port);
});
// 当有客户端连接到来时, QTcpServer对象会发出newConnection信号
connect(m_Server, &QTcpServer::newConnection, this, [=]()
{
// 得到用于通信的套接字对象
QTcpSocket * m_tcp = m_Server->nextPendingConnection();
// 创建子线程, 并且将用于通信的套接字对象传参给这个类的构造函数
recvFile * sub = new recvFile(m_tcp);
sub->start();
// 捕捉over信号
connect(sub, &recvFile::over, this, [=]()
{
sub->exit();
sub->wait();
sub->deleteLater();
QMessageBox::information(this, "文件接收", "文件接收完毕");
});
});
}
MainWindow::~MainWindow()
{
delete ui;
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QTcpServer * m_Server;
};
#endif // MAINWINDOW_H
recvfile.cpp
#include "recvfile.h"
#include
recvFile::recvFile(QTcpSocket * tcp, QObject *parent) : QThread(parent)
{
m_tcp = tcp;
}
// (这里只是一个注册, 不是执行)connect 中的槽函数不知道是什么时候执行的, 而run方法一旦执行, 直接就会走完, 当run方法执行完毕, 子线程的处理流程也就结束了
// 所以要保证当前的子线程不退出, 要一直检测事件
void recvFile::run()
{
qDebug() << "服务器子线程:" << QThread::currentThread();
// 接收文件
QFile * file = new QFile("recv.txt");
file->open(QFile::WriteOnly);
// 接收数据
connect(m_tcp, &QTcpSocket::readyRead, this, [=]()
{
static int count = 0;
static int total = 0;
if (count == 0)
{
// 如果是第一次就要接收头四个字节的数据(这里面是全部文件的大小数据信息)
m_tcp->read((char *)&total, 4);
}
// 读出剩余的数据
QByteArray all = m_tcp->readAll();
count += all.size();
file->write(all);
// 判断数据是否都接收完毕
if (count == total)
{
m_tcp->close();
m_tcp->deleteLater();
file->close();
file->deleteLater();
emit over();
}
});
// 进入事件循环, 不代表子线程退出, 只是到了后台, 当子线程中有对应的事件触发了, 那事件的对应的处理功能还是在子线程中处理
exec(); // 卡住
}
#ifndef RECVFILE_H
#define RECVFILE_H
#include
#include
#include
class recvFile : public QThread
{
Q_OBJECT
public:
explicit recvFile(QTcpSocket * tcp, QObject *parent = nullptr);
protected:
// 添加从父类继承来的虚函数 run()
void run() override;
signals:
void over(); // 发射接收结束信号
private:
QTcpSocket * m_tcp;
};
#endif // RECVFILE_H
incomingConnection()
:可以得到一个用于通信的文件描述符, 并且这个函数自动被QT框架所调用, 不需要调用nextPendingConnection()
;
incomingConnection()
mytcpserver.cpp
#include "mytcpserver.h"
MyTcpServer::MyTcpServer(QObject *parent) : QTcpServer(parent)
{
}
// 在子线程中通过通信的文件描述符自己创建用于通信的套接字对象
// 将用于通信的文件描述符, 传递到子线程中 | 用信号发射
void MyTcpServer::incomingConnection(qintptr handle)
{
emit newDescriptor(handle);
}
mytcpserver.h
#ifndef MYTCPSERVER_H
#define MYTCPSERVER_H
#include
class MyTcpServer : public QTcpServer
{
Q_OBJECT
public:
explicit MyTcpServer(QObject *parent = nullptr);
void incomingConnection(qintptr handle) override;
signals:
void newDescriptor(qintptr SocketDes);
};
#endif // MYTCPSERVER_H
recvfile.cpp
#include "recvfile.h"
#include
recvFile::recvFile(qintptr sock, QObject *parent) : QThread(parent)
{
// 将文件描述符封装成QTcpSocket
m_tcp = new QTcpSocket(this);
m_tcp->setSocketDescriptor(sock);
}
// (这里只是一个注册, 不是执行)connect 中的槽函数不知道是什么时候执行的, 而run方法一旦执行, 直接就会走完, 当run方法执行完毕, 子线程的处理流程也就结束了
// 所以要保证当前的子线程不退出, 要一直检测事件
void recvFile::run()
{
qDebug() << "服务器子线程:" << QThread::currentThread();
// 接收文件
QFile * file = new QFile("recv.txt");
file->open(QFile::WriteOnly);
// 接收数据
connect(m_tcp, &QTcpSocket::readyRead, this, [=]()
{
static int count = 0;
static int total = 0;
if (count == 0)
{
// 如果是第一次就要接收头四个字节的数据(这里面是全部文件的大小数据信息)
m_tcp->read((char *)&total, 4);
}
// 读出剩余的数据
QByteArray all = m_tcp->readAll();
count += all.size();
file->write(all);
// 判断数据是否都接收完毕
if (count == total)
{
m_tcp->close();
m_tcp->deleteLater();
file->close();
file->deleteLater();
emit over();
}
});
// 进入事件循环, 不代表子线程退出, 只是到了后台, 当子线程中有对应的事件触发了, 那事件的对应的处理功能还是在子线程中处理
exec(); // 卡住
}
recvfile.h
#ifndef RECVFILE_H
#define RECVFILE_H
#include
#include
#include
class recvFile : public QThread
{
Q_OBJECT
public:
// explicit recvFile(QTcpSocket * tcp, QObject *parent = nullptr);
explicit recvFile(qintptr sock, QObject *parent = nullptr);
protected:
// 添加从父类继承来的虚函数 run()
void run() override;
signals:
void over(); // 发射接收结束信号
private:
QTcpSocket * m_tcp;
};
#endif // RECVFILE_H
mainwindow.cpp / .h
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
#include
#include
#include
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << "服务器主线程:" << QThread::currentThread();
ui->port->setText("8899");
m_Server = new MyTcpServer(this);
// 启动监听
connect(ui->btn_setListen, &QPushButton::clicked, this, [=]()
{
unsigned short port = ui->port->text().toUShort();
m_Server->listen(QHostAddress::Any, port);
});
// 当有客户端连接到来时, QTcpServer对象会发出newConnection信号
// connect(m_Server, &QTcpServer::newConnection, this, [=]()
// {
// // 得到用于通信的套接字对象
// QTcpSocket * m_tcp = m_Server->nextPendingConnection();
// // 创建子线程, 并且将用于通信的套接字对象传参给这个类的构造函数
// recvFile * sub = new recvFile(m_tcp);
// sub->start();
// // 捕捉over信号
// connect(sub, &recvFile::over, this, [=]()
// {
// sub->exit();
// sub->wait();
// sub->deleteLater();
// QMessageBox::information(this, "文件接收", "文件接收完毕");
// });
// });
// newDescriptor qintptr
connect(m_Server, &MyTcpServer::newDescriptor, this, [=](qintptr sock)
{
// 得到用于通信的套接字对象
// QTcpSocket * m_tcp = m_Server->nextPendingConnection();
// 将qintptr类型的文件描述符传递到子线程中, 修改子线程的构造函数;
recvFile * sub = new recvFile(sock);
sub->start();
// 捕捉over信号
connect(sub, &recvFile::over, this, [=]()
{
sub->exit();
sub->wait();
sub->deleteLater();
QMessageBox::information(this, "文件接收", "文件接收完毕");
});
});
}
MainWindow::~MainWindow()
{
delete ui;
}
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
MyTcpServer * m_Server;
};
#endif // MAINWINDOW_H