目录
[1. 模块添加](#1. 模块添加)
[3. QTcpServer](#3. QTcpServer)
[3.1 公共成员函数](#3.1 公共成员函数)
[3.1.1 构造函数](#3.1.1 构造函数)
[3.1.2 给监听的套接字设置监听](#3.1.2 给监听的套接字设置监听)
[3.1.3 返回监听成功的套接字对象](#3.1.3 返回监听成功的套接字对象)
[3.2 信号](#3.2 信号)
[4. QTcpSocket](#4. QTcpSocket)
[4.1 公共成员函数](#4.1 公共成员函数)
[4.1.1 构造函数](#4.1.1 构造函数)
[4.1.2 连接服务器,需要指定服务器端绑定的IP和端口信息](#4.1.2 连接服务器,需要指定服务器端绑定的IP和端口信息)
[4.1.3 接收数据](#4.1.3 接收数据)
[4.1.4 发送数据](#4.1.4 发送数据)
[4.2 信号](#4.2 信号)
[5. 通信流程](#5. 通信流程)
[5.1 服务器](#5.1 服务器)
[5.2 客户端](#5.2 客户端)
[5.3 运行效果](#5.3 运行效果)
[5.4 程序分析](#5.4 程序分析)
[6 多线程网络通信实现](#6 多线程网络通信实现)
[6.1 文件客户端创建](#6.1 文件客户端创建)
[6.1.1 界面实现](#6.1.1 界面实现)
[6.1.2 初始化ip和port](#6.1.2 初始化ip和port)
[6.1.3 任务类实现](#6.1.3 任务类实现)
[6.1.4 创建线程对象和任务对象](#6.1.4 创建线程对象和任务对象)
[6.1.5 将任务对象添加到线程中](#6.1.5 将任务对象添加到线程中)
[6.1.6 获取输入的IP和端口](#6.1.6 获取输入的IP和端口)
[6.1.7 设置自定义信号](#6.1.7 设置自定义信号)
[6.1.8 发送信号](#6.1.8 发送信号)
[6.1.9 绑定信号槽并启动线程](#6.1.9 绑定信号槽并启动线程)
[6.1.10 完善任务类](#6.1.10 完善任务类)
[6.2 实现客户端给服务器发送文件](#6.2 实现客户端给服务器发送文件)
[6.2.1 文件选择](#6.2.1 文件选择)
[6.2.2 文件发送](#6.2.2 文件发送)
[6.2.3 更新进度条](#6.2.3 更新进度条)
[6.3 服务器实现](#6.3 服务器实现)
[6.3.1 添加子类](#6.3.1 添加子类)
[6.3.2 在主线程中设置监听](#6.3.2 在主线程中设置监听)
[6.3.3 等待客户端连接](#6.3.3 等待客户端连接)
[6.3.4 实现文件接收](#6.3.4 实现文件接收)
[6.3.5 传输结束处理](#6.3.5 传输结束处理)
[6.4 调试信息输出](#6.4 调试信息输出)
[6.4.1 客户端](#6.4.1 客户端)
[6.4.2 服务器](#6.4.2 服务器)
[6.5 类型注册](#6.5 类型注册)
[6.6 运行结果](#6.6 运行结果)
前言
Qt网络通信原理:与文件操作类似。我们使用QTcpSocket这个类进行套接字通信的时候,不管我们是进行数据的读操作还是进行数据的写操作,操作的都不是网络中的数据而是本地的数据。假如通信的两端A和B,B给A发送数据,Qt框架会自动接收来自B的数据,Qt框架会给我们维护一块内存,A通过调用read方法读取的是这块内存里面的数据,也就是说这个内存数据是由Qt框架自动帮助我们接收过来的。
同样调用write方法,也会先将数据写入到这块内存里,Qt框架检测到之后会帮助我们把这个数据发送到网络中。因此我们在调用connectToHost时候设置OpenMode模式就是对这块内存进行读写权限的指定。
使用Qt提供的类进行基于TCP的套接字通信需要用到两个类:
- QTcpServer:服务器类,用于监听客户端连接以及和客户端建立连接。
- QTcpSocket:通信的套接字类,客户端、服务器端都需要使用。
这两个套接字通信类都属于网络模块network。
实现原理



1. 模块添加

2.常用接口函数API

3. QTcpServer
QTcpServer类用于监听客户端连接以及和客户端建立连接(一般只在服务器端使用),在使用之前先介绍一下这个类提供的一些常用API函数:
3.1 公共成员函数
3.1.1 构造函数
cpp
QTcpServer::QTcpServer(QObject *parent = Q_NULLPTR);
3.1.2 给监听的套接字设置监听
cpp
//绑定+监听
bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);
// 判断当前对象是否在监听, 是返回true,没有监听返回false
bool QTcpServer::isListening() const;
// 如果当前对象正在监听返回监听的服务器地址信息, 否则返回 QHostAddress::Null
QHostAddress QTcpServer::serverAddress() const;//返回listen绑定的IP
// 如果服务器正在侦听连接,则返回服务器的端口; 否则返回0
quint16 QTcpServer::serverPort() const//返回listen绑定的PORT
- 参数:
- address:通过类QHostAddress可以封装IPv4、IPv6格式的IP地址,QHostAddress::Any表示自动绑定
-
- port:如果指定为0表示随机绑定一个可用端口。
- 返回值:绑定成功返回true,失败返回false
qt的listen函数相对于bind+listen

通过指定某一具体类型地址进行自动选择IP


具体IP地址,点分十进制

3.1.3 返回监听成功的套接字对象
得到和客户端建立连接之后用于通信的QTcpSocket套接字对象,它是QTcpServer的一个子对象,当QTcpServer对象析构的时候会自动析构这个子对象,当然也可自己手动析构,建议用完之后自己手动析构这个通信的QTcpSocket对象。
cpp
bool QTcpServer::waitForNewConnection(int msec = 0, bool *timedOut = Q_NULLPTR);
- 参数:
- msec:指定阻塞的最大时长,单位为毫秒(ms)
- timeout:传出参数,如果操作超时timeout为true,没有超时timeout为false
3.2 信号
我们一般不使用上述阻塞的方式检测客户端的连接,而使用信号来进行判断。
当接受新连接导致错误时,将发射如下信号。socketError参数描述了发生的错误相关的信息。
cpp
[signal] void QTcpServer::acceptError(QAbstractSocket::SocketError socketError);
每次有新连接可用时都会发出 newConnection() 信号。
cpp
[signal] void QTcpServer::newConnection();
newConnection() 信号一般和QTcpSocket *QTcpServer::
nextPendingConnection();函数一起使用,来返回成功连接的客户端套接字对象
4. QTcpSocket
QTcpSocket是一个套接字通信类,不管是客户端还是服务器端都需要使用。在Qt中发送和接收数据也属于IO操作(网络IO),先来看一下这个类的继承关系:

4.1 公共成员函数
4.1.1 构造函数
cpp
QTcpSocket::QTcpSocket(QObject *parent = Q_NULLPTR);
4.1.2 连接服务器,需要指定服务器端绑定的IP和端口信息
由于qt中的connect函数已经用于信号槽绑定,因此qt使用connectToHost来命名
cpp
[virtual] void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol);
//参数1:要连接的服务器IP地址(string类型),参数2:服务器绑定的端口,参数3:打开方式,参数4:默认即可
[virtual] void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite);
//参数1:经过QAbstractSocket封装的服务器IP地址
在Qt中不管调用读操作函数接收数据,还是调用写函数发送数据,操作的对象都是本地的由Qt框架维护的一块内存。因此,调用了发送函数数据不一定会马上被发送到网络中,调用了接收函数也不是直接从网络中接收数据,关于底层的相关操作是不需要使用者来维护的。
4.1.3 接收数据
cpp
// 指定可接收的最大字节数 maxSize 的数据到指针 data 指向的内存中
qint64 QIODevice::read(char *data, qint64 maxSize);
// 指定可接收的最大字节数 maxSize,返回接收的字符串
QByteArray QIODevice::read(qint64 maxSize);
// 将当前可用操作数据全部读出,通过返回值返回读出的字符串
QByteArray QIODevice::readAll();
4.1.4 发送数据
cpp
// 发送指针 data 指向的内存中的 maxSize 个字节的数据
qint64 QIODevice::write(const char *data, qint64 maxSize);
// 发送指针 data 指向的内存中的数据,字符串以 \0 作为结束标记
qint64 QIODevice::write(const char *data);
// 发送参数指定的字符串
qint64 QIODevice::write(const QByteArray &byteArray);
4.2 信号
在使用QTcpSocket进行套接字通信的过程中,如果该类对象发射出readyRead()信号,说明对端发送的数据达到了,之后就可以调用 read 函数接收数据了。
cpp
[signal] void QIODevice::readyRead();
调用connectToHost()函数并成功与服务器建立连接之后发出connected()信号(客户端发出,只能在客户端使用)。
cpp
[signal] void QAbstractSocket::connected();
在套接字断开连接时发出disconnected()信号(某一端断开了连接,另一端就会收到)。
cpp
[signal] void QAbstractSocket::disconnected();
5. 通信流程
5.1 服务器

头文件
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
struct MyWidgetStruct;
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_startListen_clicked();
void on_sendButton_clicked();
private:
Ui::Widget *ui;
MyWidgetStruct *p;
};
#endif // WIDGET_H
源文件
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QTcpServer>
#include <QTcpSocket>
#include <QStatusBar>
#include <QLabel>
#include <QDebug>
struct MyWidgetStruct
{
MyWidgetStruct()
{
m_s = new QTcpServer;
m_tcp = new QTcpSocket;
}
~MyWidgetStruct()
{
delete m_s;
m_tcp->deleteLater();//delete m_tcp;
}
//创建监听套接字
QTcpServer* m_s;
QTcpSocket* m_tcp;
};
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget),p(new MyWidgetStruct)
{
ui->setupUi(this);
setFixedSize(QSize(408,542));
setWindowTitle("服务器");
ui->statusLabel->setPixmap(QPixmap(":/img/break.png"));
ui->portLine->setText("8899");
//等待客户端连接
connect(p->m_s,&QTcpServer::newConnection,this,[=](){
p->m_tcp = p->m_s->nextPendingConnection();
ui->statusLabel->setPixmap(QPixmap(":/img/connect.png"));
//检测是否可以接收数据
connect(p->m_tcp, &QTcpSocket::readyRead,this,[=](){
QByteArray data = p->m_tcp->readAll();
ui->recordMsg->append("客户端say:" + data);
});
connect(p->m_tcp,&QTcpSocket::disconnected,this,[=](){
p->m_tcp->close();
ui->statusLabel->setPixmap(QPixmap(":/img/break.png"));
});
});
}
Widget::~Widget()
{
delete ui;
delete p;
}
void Widget::on_startListen_clicked()
{
unsigned short port = ui->portLine->text().toUShort();
p->m_s->listen(QHostAddress::Any,port);
//设置按钮为不可用
ui->startListen->setDisabled(true);
}
void Widget::on_sendButton_clicked()
{
QString msg = ui->SendMsg->toPlainText();
p->m_tcp->write(msg.toUtf8());
ui->recordMsg->append("服务器say:" + msg);
}
5.2 客户端

头文件
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
struct MyWidgetStruct;
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_sendButton_clicked();
void on_connect_clicked();
void on_disconnect_clicked();
private:
Ui::Widget *ui;
MyWidgetStruct *p;
};
#endif // WIDGET_H
源文件
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QTcpSocket>
#include <QStatusBar>
#include <QHostAddress>
#include <QLabel>
struct MyWidgetStruct
{
MyWidgetStruct()
{
m_tcp = new QTcpSocket;
}
~MyWidgetStruct()
{
m_tcp->deleteLater();//delete m_tcp;
}
//创建监听套接字
QTcpSocket* m_tcp;
};
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget),p(new MyWidgetStruct)
{
ui->setupUi(this);
setFixedSize(QSize(408,542));
setWindowTitle("客户端");
ui->statusLabel->setPixmap(QPixmap(":/img/break.png"));
ui->portLine->setText("8899");
ui->ipLine->setText("127.0.0.1");
//检测是否可以接收数据
connect( p->m_tcp, &QTcpSocket::readyRead,this,[=](){
QByteArray data = p->m_tcp->readAll();
ui->recordMsg->append("服务器say:" + data);
});
//等待断开连接
connect(p->m_tcp,&QTcpSocket::disconnected,this,[=](){
p->m_tcp->close();
ui->statusLabel->setPixmap(QPixmap(":/img/break.png"));
ui->recordMsg->append("服务器已经和客户端断开了连接...");
ui->connect->setDisabled(false);
ui->disconnect->setEnabled(false);
});
//等待连接成功
connect(p->m_tcp,&QTcpSocket::connected,this,[=](){
ui->statusLabel->setPixmap(QPixmap(":/img/connect.png"));
ui->recordMsg->append("已经成功连接到了服务器...");
ui->connect->setDisabled(true);
ui->disconnect->setEnabled(true);
});
}
Widget::~Widget()
{
delete ui;
delete p;
}
void Widget::on_sendButton_clicked()
{
QString msg = ui->SendMsg->toPlainText();
p->m_tcp->write(msg.toUtf8());
ui->recordMsg->append("客户端say:" + msg);
}
void Widget::on_connect_clicked()
{
QString ip = ui->ipLine->text();
unsigned short port = ui->portLine->text().toShort();
p->m_tcp->connectToHost(QHostAddress(ip),port);
}
void Widget::on_disconnect_clicked()
{
p->m_tcp->close();
ui->connect->setDisabled(false);
ui->disconnect->setEnabled(false);
}
5.3 运行效果


5.4 程序分析
如下图所示,为什么要使用嵌套的connect

1.外部连接 (connect(p->m_s, &QTcpServer::newConnection, this, [=]() {...})): 这个连接是在服务器接受到一个新的客户端连接时触发的。它的作用是告诉服务器要处理新的客户端连接,因此在外部连接中,你执行了以下操作:
- 获取一个新的 QTcpSocket 对象(p->m_tcp)来处理与新客户端的通信。
- 更新UI状态标签,表示客户端已连接。
2.内部连接 (connect(p->m_tcp, &QTcpSocket::readyRead, this, [=]() {...}) 和 connect(p->m_tcp, &QTcpSocket::disconnected, this, [=]() {...})): 这些连接是在外部连接中创建的。它们监听与具体客户端套接字 (p->m_tcp) 相关的信号。它们的作用是处理与客户端的数据通信和客户端断开连接的事件。
- QTcpSocket::readyRead 信号表示客户端有数据可读。当这个信号触发时,你从客户端套接字 (p->m_tcp) 中读取数据并将其附加到 UI 上,以显示客户端发送的消息。
- QTcpSocket::disconnected 信号表示客户端已断开连接。当这个信号触发时,你关闭客户端套接字 (p->m_tcp) 并更新 UI 状态。
将这些内部连接放在外部连接内部的原因是,这些连接是特定于每个客户端的。当新客户端连接时,你需要为该客户端创建一个新的 QTcpSocket 并设置用于处理其通信和断开连接的连接。嵌套 connect 允许你在外部连接中设置这些与客户端相关的操作,以便在新客户端连接时能够建立适当的连接和处理逻辑。这种结构有助于维护多个客户端连接的服务器应用程序。
6 多线程网络通信实现
需求:客户端向服务器发送数据,服务器接受完数据之后断开连接,实现文件的传输,并且这些操作都由子线程来完成。
PS:客户端以第二种多线程方法创建子线程,服务器以第一种多线程方法创建子线程。
6.1 文件客户端创建
1、创建任务类
2、在类中编写一个任务函数
3、实例化一个QThread对象和一个任务类对象,任务对象不要绑定父对象
4、将任务对象添加到线程对象中
5、启动线程
6.1.1 界面实现

6.1.2 初始化ip和port

6.1.3 任务类实现

6.1.4 创建线程对象和任务对象

6.1.5 将任务对象添加到线程中

6.1.6 获取输入的IP和端口

6.1.7 设置自定义信号

6.1.8 发送信号

6.1.9 绑定信号槽并启动线程
触发信号时将ip和port传给任务对象的槽函数,槽函数由子线程执行

6.1.10 完善任务类
连接服务器和客户端

判断连接状态,并发送信号给主线程

主线程处理子线程发送的信号

6.2 实现客户端给服务器发送文件
6.2.1 文件选择

6.2.2 文件发送
主线程通过信号将文件路径发送子线程


绑定信号槽

任务类中读取文件内容

通过QFileInfo获取文件的大小,并在第一次发送时候将文件大小发送给主线程


6.2.3 更新进度条

6.3 服务器实现
1.创建一个线程类的子类,让其继承QT中的QThread
2.重写父类的run()方法,在该函数内部编写子线程要处理的具体业务流程
3.在主线程中创建一个子线程对象,new一个即可
4.启动子线程,调用start()方法
6.3.1 添加子类
继承QObject(为QThread基类)

将所有QObject换成QThread

6.3.2 在主线程中设置监听

6.3.3 等待客户端连接

有两种方式可以将传递到子线程中去,第一种是信号槽的方式,第二种是传参,这里我们选择第二种
我们在子线程的构造函数中添加一个QTcpSocket参数

然后在刚刚的槽函数中创建一个子线程并传参

启动子线程工作

6.3.4 实现文件接收
重写run()函数

写入数据。其中exec的作用是使线程进入事件循环,防止线程写入完一次数据就退出

6.3.5 传输结束处理

6.4 调试信息输出
添加console使qDebug()输出输出到控制台而不是QT的终端里,这样就解决了同时启动两个项目却只能在QT终端中输出一个项目的调试日志

注意子线程的线程id输出别放在线程类的构造函数中
6.4.1 客户端
主线程id:

客户端连接子线程id:

客户端发送子线程id:

6.4.2 服务器
服务器主线程id:

服务器子线程id:

6.5 类型注册
QObject::connect连接信号和槽时,有的时候Qt不知道如何处理的参数类型时会发生这种错误。
在本例中的客户端

中由于startConnect和connectServer的参数都具有unsigned short int类型
就出现了如下错误:
QObject::connect: Cannot queue arguments of type 'unsigned short int' (Make sure 'unsigned short int' is registered using qRegisterMetaType().)
要解决这个问题,您应该使用qRegisterMetaType函数在连接之前注册自定义数据类型
qRegisterMetaType("unsigned short int");
6.6 运行结果


