【Qt】网络编程
目录
一.UDP
1.1.QUdpSocket常用方法
1.2.QUdpSocket常用信号——readyRead
1.2.QNetworkDatagram常用方法
1.3.示例——UDP回显服务端的编写
1.4.示例——UDP回显客户端的编写
1.5.理解QString和QByteArray的相互转换
1.5.1.从 QString 转换为 QByteArray
1.5.2.从 QByteArray 转换为 QString
1.5.3.特殊情况与技巧
二.TCP
2.1.QTcpServer核心方法
2.2.QTcpServer核心信号
2.3.QTcpSocket核心方法
2.4.QTcpSocket核心信号
2.5.示例——TCP回显服务器
2.6.示例——TCP回显客户端
三.HTTP
3.1.QNetworkAccessManager
3.2.QNetworkRequest
3.3.QNetworkReply
3.4.核心信号——QNetworkReply::finished
3.4.简单示例——HTTP客户端
和多线程类似,Qt为了⽀持跨平台,对⽹络编程的API也进⾏了重新封装.
实际Qt开发中进⾏⽹络编程,也不⼀定使⽤Qt封装的⽹络API,也有⼀定可能使⽤的是系统原⽣API或者其他第三⽅框架的API.
注意:
在进⾏⽹络编程之前,需要在项⽬中的.pro ⽂件中添加 network 模块.
加之后要⼿动编译⼀下项⽬,使QtCreator能够加载对应模块的头⽂件。
一.UDP
其实主要的类是QUdpSocket和QNetWorkDatagram
1.1.QUdpSocket常用方法
1. bind(const QHostAddress &address, quint16 port) - “占地盘”
作用:
这个方法的作用是让 QUdpSocket
对象“绑定”到一个本地的网络地址和端口号上。你可以把它想象成给你的应用程序分配一个专属的“邮箱”或者“门牌号”。
-
地址(address):指定绑定到本机的哪个IP地址。比如
QHostAddress::Any
(IPv4 的0.0.0.0
) 表示监听所有本机的网络接口(网卡)收到的数据。如果你的机器有多个网卡(比如有线、WiFi),用这个就能都监听到。你也可以指定一个具体的IP,如QHostAddress(“192.168.1.100”)
,那么它就只监听发往这个IP的数据。 -
端口(port):这是最关键的部分。UDP数据包是通过端口号来区分不同应用程序的。比如,DNS服务通常使用53端口。你的程序想接收UDP数据,就必须绑定到一个特定的端口上,告诉操作系统:“所有发往我这个端口的数据,都交给我来处理!”
对标原生API:
对标伯克利套接字(BSD Socket)的 bind()
函数。作用一模一样。
使用场景与细节:
-
作为接收方:如果你想接收UDP数据报,必须先调用
bind
。否则,你的 socket 没有“地盘”,操作系统不知道把数据交给谁。 -
作为纯发送方:你也可以不调用
bind
。当你第一次调用writeDatagram
发送数据时,系统会自动为你分配一个随机的、可用的本地端口。但如果你希望对方固定地回包到某个端口,那么你发送前最好先bind
一个固定端口。 -
返回值:返回
bool
类型,成功为true
,失败为false
。失败的原因可能是端口已被其他程序占用,或者你没有权限绑定该端口(如小于1024的端口在Linux/Unix下需要root权限)。
2.errorString() - “它到底怎么了?问个明白!”
作用:
errorString()
是 QUdpSocket
(以及所有继承自 QAbstractSocket
的类)的一个方法。它的唯一作用就是:当 socket 发生错误时,返回一个可读的、描述性的字符串,告诉你到底出了什么问题。
网络编程充满了不确定性,很多事情都可能出错:
-
端口被占用
-
网络连接断开
-
目标主机不可达
-
没有权限操作某个端口
-
内存不足
当你的 bind()
返回 false
,或者 writeDatagram()
返回 -1
时,你只知道 “操作失败了”,但你不知道 “为什么会失败”。errorString()
就是用来解答“为什么”这个关键问题的。
3. receiveDatagram() - “从邮箱取信”
作用:
这个方法用于从之前 bind
的“邮箱”(socket缓冲区)中读取一个等待处理的UDP数据报。它不接收任何参数,并返回一个 QNetworkDatagram
对象。
对标原生API:
对标原生的 recvfrom()
函数。但 QUdpSocket
的封装更友好、更现代。
返回的 QNetworkDatagram
对象包含什么?
这是关键!调用这个方法后,你得到的不是一个简单的 QByteArray
,而是一个信息丰富的“数据报对象”,它包含:
-
数据本身(Data):通过
datagram.data()
可以拿到数据内容的QByteArray
。 -
发送方地址(Sender Address):通过
datagram.senderAddress()
和datagram.senderPort()
可以知道这个数据报是谁发过来的。这对于需要回复消息的场景至关重要。 -
接收方地址(Receiver Address):通过
datagram.destinationAddress()
和datagram.destinationPort()
可以知道这个数据报是发到本机的哪个IP和端口的。这在服务器有多个IP时有用。 -
生存时间(TTL):在某些类型的socket上可以获取。
重要特性:
-
阻塞与非阻塞:如果使用了
Qt::Blocking
模式,当没有数据时,调用此方法会一直等待,直到有数据到来。但在默认的Qt::NonBlocking
模式下,如果缓冲区没有数据,它会立即返回一个无效的QNetworkDatagram
对象(你可以通过if (datagram.isValid()) {...}
来判断)。 -
读取并移除:一旦你调用
receiveDatagram()
成功读取了一个数据报,这个数据报就会从socket的接收缓冲区中被移除。
4. writeDatagram(const QNetworkDatagram &datagram) - “寄信”
作用:
这个方法用于发送一个UDP数据报到网络中的另一台机器。
对标原生API:
对标原生的 sendto()
函数。
参数 QNetworkDatagram
:
你需要在调用前构造好这个“数据报对象”。
使用场景与细节:
-
无需连接:UDP是无连接的,所以你每次发送都需要明确指定目标的地址和端口。
-
发送到广播/组播:你可以将目标地址设置为广播地址(如
QHostAddress(“255.255.255.255”)
)或组播地址,来实现一对多的通信。 -
返回值:返回
qint64
类型,表示成功发送的字节数。如果发送失败(如网络不可达),会返回-1
。 -
注意:UDP不保证数据一定能送达,也不保证数据包的顺序。这是由UDP协议本身的特性决定的。
1.2.QUdpSocket常用信号——readyRead
readyRead 信号 - “邮箱有新信了,快来取!”
作用:
这是一个信号,而不是一个方法。它是 QUdpSocket
事件驱动编程的核心。
对标原生机制:
对标IO多路复用机制(如 select
, poll
, epoll
)的通知功能。当socket变得“可读”时,这些机制会通知你,readyRead
信号做了同样的事情,但更符合Qt的风格。
它是如何工作的?
在Qt的事件循环(Event Loop)中,QUdpSocket
会一直监听底层的socket。一旦操作系统通知它有新的UDP数据报到达,并且已经被存入接收缓冲区,QUdpSocket
对象就会立刻发射(emit) 这个 readyRead
信号。
你该怎么做?
你需要将你的一个槽函数(Slot) 连接到这个信号上。
为什么它如此重要?
因为它实现了异步通知。你不需要自己写一个循环不停地去调用 receiveDatagram()
(这叫“轮询”,非常浪费CPU)。你只需要“告诉”Qt:“当有数据时,请调用我的 handlePendingDatagrams
函数”。这样,你的程序在等待数据时可以去做别的事情,CPU利用率高,响应也及时。
总结与工作流程
一个典型的UDP接收端程序流程是这样的:
-
创建Socket:
QUdpSocket udpSocket;
-
绑定端口:
udpSocket.bind(QHostAddress::Any, 7755);
-
连接信号:
connect(&udpSocket, &QUdpSocket::readyRead, this, &MyClass::readData);
-
事件循环:进入Qt的事件循环(
app.exec()
)。 -
触发与处理:
-
当有数据到达端口7755时,操作系统通知Qt。
-
Qt使
udpSocket
发射readyRead
信号。 -
你的
readData
槽函数被自动调用。 -
在
readData
里,你调用receiveDatagram()
来取出并处理数据。
-
1.2.QNetworkDatagram常用方法
什么是 QNetworkDatagram?
您可以把它理解成一个 “UDP 数据包”的包装器。在网络编程中,UDP 是一种无连接的、不可靠的、但效率很高的传输协议。一个 UDP 数据报就是一个完整的数据包,它包含了您要发送的数据本身,以及目标地址信息(当您发送时)或来源地址信息(当您接收时)。
QNetworkDatagram对象就封装了这三样东西:
-
数据内容
-
IP 地址
-
端口号
下面我就讲讲这个类里面最重要的几个函数
1. 构造函数:QNetworkDatagram(const QByteArray& data, const QHostAddress& destinationAddress, quint16 port)
-
功能说明:
这个构造函数用于创建一个准备发送的 UDP 数据报。-
data
: 这是您想要通过 UDP 发送的实际内容,比如一段文字、一串序列化后的数字等等。它被包装在QByteArray
中。 -
destinationAddress
: 这是数据报要发送到的目标主机的 IP 地址,例如QHostAddress("192.168.1.100")
或QHostAddress::Broadcast
(用于广播)。 -
port
: 这是目标主机上正在监听的那个应用程序的端口号。例如,如果目标程序在 8888 端口等待数据,这里就填 8888。
-
-
使用场景:当您使用 QUdpSocket 发送数据时,您不会直接调用 QUdpSocket 的 write 方法并附带地址,而是先创建一个 QNetworkDatagram 对象,然后通过 QUdpSocket::writeDatagram 将这个数据报对象发送出去。
-
代码示例:
// 假设我们要发送字符串 "Hello UDP!" 到本机(127.0.0.1)的 12345 端口 QByteArray dataToSend = "Hello UDP!"; QHostAddress targetAddress = QHostAddress::LocalHost; // 即 127.0.0.1 quint16 targetPort = 12345;// 使用构造函数创建数据报 QNetworkDatagram datagramToSend(dataToSend, targetAddress, targetPort);// 然后通过一个已创建的 udpSocket 发送它 QUdpSocket udpSocket; udpSocket.writeDatagram(datagramToSend);
2. data() 方法
-
功能说明:
这个方法用于从数据报中提取出有效载荷,也就是实际传输的数据内容。它返回一个QByteArray
。 -
使用场景:这个方法在接收 UDP 数据报时极其常用。当您的 QUdpSocket 收到一个数据报后,您会得到一个 QNetworkDatagram 对象,然后必须调用它的 data() 方法来读取里面的内容。
-
这个返回的是一个QByteArray,可以赋值给QString。
3. senderAddress() 方法
-
功能说明:
这个方法返回一个QHostAddress
对象,它是发送方的IP地址,也就是这数据是谁发来的,它代表了发送这个数据报的远端机器的 IP 地址。 -
使用场景:这个方法仅在接收到的数据报上有意义。当您的程序从一个 UDP 端口收到数据时,您不仅关心数据内容,通常还关心“这数据是谁发来的?”,以便进行回复或记录日志。senderAddress() 就提供了这个“谁”的 IP 地址信息。
4. senderPort() 方法
-
功能说明:
这个方法返回一个quint16
(即 16 位无符号整数),它代表了发送这个数据报的远端应用程序所使用的端口号。 -
使用场景:与 senderAddress() 完全一样,它也是仅在接收数据报时使用。IP 地址定位到主机,而端口号定位到主机上的具体应用程序。知道了 IP 和端口,您就能完整地标识出发送方,并可以向它回复消息。
1.3.示例——UDP回显服务端的编写
我们创建一个项目(基于QWidget)
注意事项1
我们这里的bind里面第一个参数是需要绑定的IP地址,我们在上面使用了QHostAddress::Any,那其实是一个枚举值
我们来看一下这个枚举类型,它定义了QHostAddress的一些特殊地址。这些地址在网络编程中非常常见,它们各自有特定的含义。下面我们逐一解释:
-
Null:表示一个空地址。它不指向任何地址,类似于一个未初始化的地址对象。
-
Broadcast:广播地址。用于在本地网络中向所有设备发送数据。在IPv4中,通常指的是255.255.255.255。当向这个地址发送数据时,同一网络段内的所有主机都会收到。
-
LocalHost:本地回环地址(IPv4)。通常指的是127.0.0.1,用于本地机器上的进程间通信。发送到这个地址的数据不会进入网络,而是直接在本地处理。
-
LocalHostIPv6:IPv6的本地回环地址。指的是::1,功能与IPv4的127.0.0.1相同。
-
Any:任意地址。在IPv4中,通常表示为0.0.0.0。当服务器监听这个地址时,表示它愿意接受来自任何网络接口的连接。在Qt中,这个值在IPv4和IPv6中都有定义,但根据上下文可能指向IPv4或IPv6。但是注意,这里有一个AnyIPv6和AnyIPv4,所以Any可能是为了兼容性,具体取决于配置。
-
AnyIPv6:IPv6的任意地址。指的是::(全零地址)。当服务器监听这个地址时,它会接受所有IPv6接口上的连接。
-
AnyIPv4:IPv4的任意地址。指的是0.0.0.0,表示服务器会接受所有IPv4接口上的连接。
注意事项2
为什么需要调用这个函数?
因为这QNetWorkDatagram的构造函数第一个参数是QByteArray类型的,而我们这个response则是QString类型的。它们的类型不同,需要另外转换
QByteArray 用于表示一个字节数组,可以很方便的和 QString 进行相互转换
例如:
- 使用 QString 的构造函数即可把 QByteArray转成 QString.
- 使用 QString的 toUtf8 函数即可把 QString转成 QByteArray.
1.4.示例——UDP回显客户端的编写
当然,有了服务端还不行,我们还需要一个客户端。
我们创建一个新的项目
注意了啊:不要忘记了添加下面这个啊
还是很不错的啊。
好了,现在可以来编写代码了
好了我们现在就算是完成了。
我们运行一下,注意我们先运行服务端,再运行客户端
我们点击发送
完全没有一点问题啊!!
其实我们也可以启动多个客户端程序来连接这么一个服务端
我们直接双击运行这个.exe文件就能打开另外一个客户端。
问题1
在学习 Linux 网络编程时,我们曾经使用云服务器部署过服务器程序,当时其他同学的客户端也能够顺利连接上来。
那么现在的问题是:我们能否将目前用 Qt 编写的 UDP 服务器程序也部署到云服务器上呢?
大概率是不行的——这主要取决于你的云服务器是否安装了图形化界面。因为 Qt 程序通常需要依赖图形界面环境才能运行,而我们之前使用的 Linux 云服务器一般是纯命令行环境,并没有预装图形界面。如果确实需要运行 Qt 程序,就需要手动安装图形化界面及相关依赖。
问题2
另一个问题是:我们能否用现在用QT编写的 UDP 客户端,连接到之前 Linux 阶段写的 UDP 服务器呢?
这一点是完全可行的 ✅ 这也正体现了网络编程和协议标准化的重要意义——只要通信协议一致,不同平台、不同技术实现的客户端和服务器之间完全可以互相通信。
在实际商业项目中,服务器程序通常不会使用 Qt 编写,而更可能采用 C/C++、Go、Java 等技术栈,以保证在高并发场景下的性能和稳定性。而客户端方面,Qt 凭借其良好的跨平台特性和丰富的界面组件,常被选作桌面客户端开发框架。因此,使用 Qt 编写客户端连接非 Qt 实现的服务器,是非常常见且合理的架构选择。
1.5.理解QString和QByteArray的相互转换
理解它们的本质是成功转换的关键:
-
QString:Qt 中的字符串类。它存储的是 Unicode 字符(16位
QChar
)。用于显示给用户看的文本。 -
QByteArray:Qt 中的字节数组类。它存储的是原始的
char
数据(8位字节)。用于处理二进制数据,或者没有编码信息的纯文本数据。
因为它们一个面向“字符”,一个面向“字节”,所以转换的桥梁就是编码。
1.5.1.从 QString 转换为 QByteArray
当你有一个 QString
,想把它变成字节流(例如,要保存到文件、通过网络发送,或调用一个需要 const char*
的 C 函数),你需要编码它。
最常用的方法是使用 QString
的成员函数。
使用 toUtf8()(最常用、推荐)
UTF-8 是一种兼容 ASCII 的 Unicode 编码,它是网络传输和文件存储的事实标准。
QString str = "你好,世界!Hello World!";
QByteArray byteArray = str.toUtf8();// 此时 byteArray 里存储的就是字符串的 UTF-8 编码字节。
// 例如,"你" 字在 UTF-8 下是 3 个字节:0xE4 0xBD 0xA0
适用场景:几乎所有情况,尤其是在不同系统间交换数据、与 Web 服务通信、保存文件(如 JSON、XML)时。
1.5.2.从 QByteArray 转换为 QString
当你有一串字节(例如,从文件读取、从网络接收,或从 C 函数返回),并且你知道这串字节代表的是文本时,你需要解码它才能得到 QString
。
最常用的方法是使用 QString
的静态函数。
使用 fromUtf8()(最常用、推荐)
假设你的 QByteArray
中的数据是 UTF-8 编码的。
// 假设 byteArray 里存储的是 "你好" 的 UTF-8 字节
QByteArray byteArray = ...;
QString str = QString::fromUtf8(byteArray);
适用场景:读取 UTF-8 编码的文件、处理网络请求的 UTF-8 响应等。
1.5.3.特殊情况与技巧
1. 直接构造(隐式转换)
QString 的构造函数可以直接接受 QByteArray,但其行为等同于 fromUtf8()。为了代码清晰,建议显式使用 fromUtf8()。
QByteArray byteArray = "Hello";
QString str(byteArray); // 等价于 QString::fromUtf8(byteArray)
2. 与 const char* 的互操作
QByteArray 可以很方便地与 const char* 相互转换,这使其成为 QString 与 C 风格字符串之间的完美桥梁。
-
QString -> const char*:
QString str = "Hello"; QByteArray ba = str.toUtf8(); const char *c_str = ba.constData(); // 或者 ba.data()
-
QString -> const char*:
const char *c_str = "Hello"; QString str = QString::fromUtf8(c_str);
二.TCP
核⼼类是两个: QTcpServer 和 QTcpSocket
QTcpServer 主要用于实现TCP服务器端的功能,负责监听指定端口,接受客户端的连接请求,并在新连接建立时创建一个QTcpSocket对象用于后续通信。
QTcpSocket 则代表一个TCP连接,用于客户端与服务器之间的实时数据读写和交互。它封装了TCP协议的核心操作,包括建立连接、传输数据以及连接状态管理,支持异步通信机制,能够高效处理网络数据的发送与接收。
总的来说,QTcpServer 负责接收连接,QTcpSocket 负责连接建立后的数据通信,二者共同构成了Qt网络模块中TCP通信的基础。
2.1.QTcpServer核心方法
QTcpServer ⽤于监听端⼝,和获取客⼾端连接.
1. listen(const QHostAddress &address, quint16 port) 方法
-
文字说明:
这个方法是启动服务器的“总开关”。它的作用就是让服务器“坐下来,竖起耳朵”,在一个指定的网络地址和端口号上开始等待客户端的连接请求。-
绑定:首先,它会将服务器程序与您指定的
address
(IP地址,如QHostAddress::Any
表示所有本机IP) 和port
(端口号) 进行绑定。这就像是给服务器一个固定的“门牌号”,客户端才知道要连接哪里。 -
监听:绑定成功后,它立即开始监听这个“门牌号”。这意味着操作系统会被告知:凡是发送到这个IP和端口的数据包,都请交给这个服务器程序处理。此时,服务器就进入了等待状态。
-
-
返回值:
它是一个bool
类型。如果监听成功(例如,端口没有被其他程序占用),返回true
;如果失败(例如,端口已被占用或权限不足),返回false
。在调用后一定要检查返回值,这是编程的好习惯。 -
对标原生API:
它实际上封装了原生 Berkeley Sockets API 中的bind()
和listen()
两个步骤。在原生C/C++中,你需要先调用bind()
来绑定地址,再调用listen()
来开始监听。Qt 将其简化为一个方法。
2. nextPendingConnection() 方法
-
文字说明:
这个方法是你在收到“有新客户”的通知后,用来“接待”客户的具体动作。它从服务器的等待连接队列中,取出下一个已经建立好的连接,并将其包装成一个QTcpSocket
对象返回给你。-
返回值:返回一个指向
QTcpSocket
对象的指针。这个QTcpSocket
对象就代表了与那个特定客户端的通信通道。你之后所有与该客户端的读写操作,都通过这个QTcpSocket
对象来完成。 -
所有权:返回的
QTcpSocket
对象是没有父对象的,你需要负责管理它的生命周期。通常的做法是,在接收到这个对象后,立刻将其设置一个父对象,或者将其存储起来以便后续使用和销毁。
-
-
调用时机:
这个方法必须在newConnection
信号触发的槽函数内部(或由该槽函数引发的后续调用链中)调用。因为只有在这个时候,你才知道有新的连接在等待处理。 -
对标原生API:
它直接对标原生API中的accept()
函数。accept()
也是从监听套接字中接受一个新连接,并返回一个用于通信的新套接字描述符。
2.2.QTcpServer核心信号
newConnection 信号
-
文字说明:
这是一个非常关键的信号。它不是你需要主动调用的方法,而是由QTcpServer
在特定事件发生时自动发出的“广播”或“通知”。-
触发时机:当有一个全新的客户端成功地通过三次握手与服务器建立了TCP连接时,这个信号就会被
QTcpServer
触发。 -
作用:它告诉你:“喂!注意了,有个新客户来了,快去接待一下!” 在你的代码中,你需要将一个槽函数连接到这个信号。一旦信号发出,你的槽函数就会被自动调用,这样你就能在里面处理新的客户端连接了。
-
-
工作模式类比(IO多路复用):
你提到的“类似于IO多路复用中的通知机制”非常准确!-
在传统的阻塞式编程中,你可能会在一个循环里调用
accept()
,如果没有新连接,程序就会卡在那里(阻塞)。 -
而
QTcpServer
配合newConnection
信号,是 事件驱动 的。你不需要主动去轮询询问“有新连接吗?”。你只需要告诉Qt:“当有新连接时,请调用我这个函数”。这背后的机制正是由Qt的事件循环实现的,它底层会使用像select
,poll
, 或epoll
这样的IO多路复用技术来高效地监视所有网络连接。当有事件(如新连接)就绪时,事件循环会收到通知,并触发对应的信号。
所以,
newConnection
信号就是Qt封装好的、基于事件循环的“新连接通知”。 -
2.3.QTcpSocket核心方法
QTcpSocket ⽤⼾客⼾端和服务器之间的数据交互
1. QByteArray readAll()
-
说明:这是最重要的数据读取方法。它的作用是一次性读取当前接收缓冲区中积攒的所有数据,并将其作为一个整体返回。
-
工作方式:
-
Socket(套接字)在收到数据后,并不会立刻通知你,而是先将数据存入一个内部的“接收缓冲区”。
-
当这个缓冲区里有新的数据到达时,
readyRead
信号就会被触发。 -
此时,你调用
readAll()
方法,就会把到目前为止缓冲区里所有的数据“一扫而空”,全部取出来。
-
-
返回:一个
QByteArray
对象。这是Qt中用于存放原始字节(包括文本和二进制数据)的容器。如果你知道数据是文本,可以再用QString::fromUtf8()等方法将其转换为QString。 -
对标原生API:类似于 Berkeley Sockets (C语言)中的
read()
或recv()
函数。 -
重要注意事项:
-
清空缓冲区:调用
readAll()
后,内部缓冲区会被清空。再次调用会返回空数据,直到有新的数据到来。 -
数据量:如果对端一次性发送了大量数据,
readAll()
会返回所有数据。你需要确保你的程序有能力处理可能很大的数据块。
-
2. write(const QByteArray& data)
-
说明:这是主要的写入方法。用于将一段数据(
data
)发送到连接的对方。 -
工作方式:
-
你提供一个
QByteArray
作为参数,比如socket->write(“Hello World”)
。 -
这个方法会将这些数据放入一个内部的“发送缓冲区”,然后立即返回。数据的实际发送是异步的、在后台进行的。
-
这意味着
write()
方法本身不保证数据已经通过网络发送出去,它只负责将数据排队。这种非阻塞的特性是高性能事件驱动编程的核心。
-
-
返回:通常会返回一个
qint64
值,表示成功排队到发送缓冲区的字节数。如果返回-1,则表示发生了错误。 -
对标原生API:类似于 Berkeley Sockets 中的
write()
或send()
函数。 -
重要注意事项:
-
异步发送:你不能假设调用
write()
后对方就立刻收到了数据。 -
缓冲:如果网络拥堵或对方接收慢,数据可能会在发送缓冲区中积压。
-
3. deleteLater()
-
说明:这是一个用于安全销毁对象的方法。
-
工作方式:
-
在事件驱动编程中,直接使用
delete
操作符删除一个正在处理事件的对象(比如Socket)是极其危险的,可能会导致程序崩溃(例如,一个事件还在处理中,但对象已经被删除了)。 -
deleteLater()
不会立即删除对象,而是给对象打上一个标记,告诉事件循环系统:“在当前事件处理完成,并返回到事件循环之后,再安全地删除这个对象”。
-
-
它提供了一种安全、自动的延迟销毁机制。
-
使用场景:通常在连接断开(例如在
disconnected
信号的槽函数中)后,需要销毁Socket对象时调用。socket->deleteLater();
2.4.QTcpSocket核心信号
1. readyRead()
-
说明:这是最重要的数据到达通知信号。
-
触发时机:当有新的数据从网络或对方进程到达,并被加载到Socket的接收缓冲区,准备就绪让你读取时,这个信号就会发射。
-
工作方式:
-
你需要将一个槽函数(比如
onReadyRead()
)连接到这个信号。 -
一旦信号发射,你的槽函数就会被调用,在槽函数里你就可以放心地使用
readAll()
或其他读取方法来获取数据。
-
-
对标原生API:类似于 I/O 多路复用中的通知机制。在像
epoll
或select
这样的原生API中,你会被通知某个文件描述符“可读了”,然后你再去读。readyRead
信号就是这种通知机制的面向对象封装。 -
重要注意事项:
-
它只通知你“有数据可读”,但不告诉你具体有多少数据。你可能需要一次读取完,或者根据自定义的协议(如数据包头部声明长度)来分多次读取。
-
2. disconnected()
-
说明:这是连接断开通知信号。
-
触发时机:当对方主动关闭了连接,或者网络异常导致连接中断时,这个信号就会发射。
-
工作方式:
-
你将一个槽函数(比如
onDisconnected()
)连接到这个信号。 -
当连接断开时,槽函数被调用,你可以在里面进行清理工作,例如:释放资源、更新用户界面状态(显示“已断开”)、尝试重新连接等。
-
通常也会在这个槽函数中调用
deleteLater()
来安全销毁Socket对象。
-
-
对标原生API:在原生Socket编程中,你需要通过检查
recv()
的返回值(返回0)或select
/epoll
的错误事件来感知连接断开。disconnected
信号将这些底层细节封装了起来。
2.5.示例——TCP回显服务器
首先,大家千万不要忘了下面这个
接下来我们就来编写代码
2.6.示例——TCP回显客户端
我们创建一个新的项目
大家千万不要忘了
还是很不错的啊。
补充知识:QTcpSocket::connectToHost
这个函数就是客户端用来和服务端进行3次握手,然后建立连接的!!!
QTcpSocket::connectToHost 最核心的特性就是:它是一个非阻塞的、异步的函数。
这意味着:
- 调用立即返回:当你调用这个函数时,它不会等待到目标服务器的网络连接完全建立好(即三次握手完成)才返回。它会几乎立刻将控制权交还给你的程序,让你的代码可以继续执行下去。
- 后台操作:实际的连接建立过程(包括你提到的“三次握手”)是在系统后台由操作系统内核和Qt的事件循环来处理的。
既然函数不阻塞,我们如何知道连接是成功还是失败呢?答案就是Qt的信号与槽机制。
在调用 connectToHost 之后,在连接建立过程中的不同阶段,会触发不同的信号:
- connected(): 这是最重要的信号! 当内核完成三次握手,连接成功建立时,Qt会发出这个信号。这是你开始发送数据的“发令枪”。
- errorOccurred(QAbstractSocket::SocketError):如果连接失败(例如:服务器拒绝、网络超时、主机找不到等),会发出这个信号。
补充知识:QTcpSocket::waitForConnected
首先,waitForConnected是QTcpSocket的一个成员函数,它用于在调用connectToHost之后,阻塞等待直到连接建立(即三次握手完成)或者超时。
与connectToHost的非阻塞特性不同,waitForConnected是一个阻塞函数。这意味着调用waitForConnected后,当前线程会暂停执行,直到连接成功建立、发生错误或等待时间超过指定的超时时间。
函数原型如下:
bool QTcpSocket::waitForConnected(int msecs = 30000)
参数msecs是超时时间,单位是毫秒,默认值是30000毫秒(即30秒)。返回值是一个布尔值:
- 如果连接在超时时间内成功建立,返回true;
- 如果连接失败或超时,返回false。
下面详细描述其行为:
-
如果socket已经处于连接状态,那么waitForConnected会立即返回true。
-
如果socket还没有开始连接,即还没有调用connectToHost,那么waitForConnected将立即返回false。
-
在调用了connectToHost之后,waitForConnected会阻塞,直到以下情况之一发生:
a) 连接成功建立,返回true;
b) 连接失败(例如,被拒绝或超时),返回false;
c) 等待时间超过了msecs指定的毫秒数,返回false。
接下来我们运行一下
注意:需要先运行服务器,再运行客户端。
我们发现一运行,就出现了下面这个
点击发送
没有任何问题的!!!
我们让客户端下线的话
咋样!!!还是可以的吧。
三.HTTP
HTTP 协议在应用层上比底层的 TCP 或 UDP 更为抽象,也更贴近实际业务场景。
在 Qt 框架中,提供了用于实现 HTTP 客户端的相关类,方便开发者进行网络请求与资源访问。
需要注意的是,HTTP 协议本质上仍是基于 TCP 协议构建的,因此无论是实现一个 HTTP 客户端还是服务器,其核心都是对 TCP Socket 进行封装并在此基础上添加 HTTP 规范所需的逻辑处理。
目前,Qt 主要封装了 HTTP 客户端的实现,但并未提供官方的 HTTP 服务器库,若需实现服务端功能,开发者通常需要基于 Qt 的 TCP 服务器类(如 QTcpServer)自行构建符合 HTTP 标准的处理机制。
在 Qt 网络模块中,三个核心类构成了 HTTP 通信的基础,分别是:
-
QNetworkAccessManager
它是整个 HTTP 操作的中枢,负责协调所有的网络请求与响应。通过它,我们可以发送请求、处理回复,并管理诸如 Cookie、缓存等网络相关功能。 -
QNetworkRequest
该类用于封装一个 HTTP 请求的元信息,例如 URL、请求头、协议参数等。需要注意的是,它并不包含请求体(body)部分,仅用于描述请求的基本属性。 -
QNetworkReply
该类代表服务器对HTTP 请求的响应。除了包含响应的状态码、响应头等元信息之外,它还继承自QIODevice
,因此可以像一般的 I/O 设备一样进行数据读取,尤其适合处理异步接收的响应内容。
通过这三个类的配合,Qt 为开发者提供了一套完整且易于使用的 HTTP 客户端编程接口。
3.1.QNetworkAccessManager
QNetworkAccessManager 提供了HTTP的核⼼操作
1. get(const QNetworkRequest &request)
-
功能描述:此方法用于发起一个异步的 HTTP GET 请求。GET 方法通常用于向指定的服务器资源请求数据,请求的参数一般会附加在 URL 之后。
-
参数说明:它接收一个 QNetworkRequest 对象作为参数。这个对象包含了此次请求的所有必要信息,例如目标 URL、请求头以及各种属性设置。
-
返回值与机制:方法会立即返回一个 QNetworkReply 对象。这个对象代表着即将到来的网络响应。由于网络操作是异步的,调用此方法后,函数会立刻返回而不会阻塞当前线程。您需要通过连接 QNetworkReply 提供的信号(如
finished()
、readyRead()
、errorOccurred()
)来监听请求的完成状态、读取返回的数据或处理可能发生的错误。 -
核心用途:主要用于从服务器获取信息,例如加载网页内容、下载文件、调用获取数据的 API 接口等。
2. post(const QNetworkRequest &request, const QByteArray &data)
-
功能描述:此方法用于发起一个异步的 HTTP POST 请求。POST 方法通常用于向服务器提交数据,例如提交表单信息、上传文件或调用修改数据的 API 接口。
-
参数说明:
-
const QNetworkRequest &request:它接收一个 QNetworkRequest 对象作为参数。与 get 方法类似,此参数定义了请求的目标 URL、请求头等信息。
-
const QByteArray &data:此参数包含了要提交给服务器的请求体数据。您需要将数据转换为 QByteArray 格式。
-
-
返回值与机制:get 方法一样,它也会立即返回一个 QNetworkReply 对象。您同样需要通过连接该对象的信号来异步地处理服务器的响应。
-
核心用途:主要用于向服务器发送数据,例如用户登录、发表评论、上传文件等操作。
总结与
这两个方法是 QNetworkAccessManager
最核心的接口,它们共同的特点是:
-
异步非阻塞:调用后立即返回,不会阻塞程序执行。
-
返回 QNetworkReply:通过这个对象来关联请求、跟踪状态并最终获取响应数据。
-
信号与槽机制:整个请求和响应的生命周期都通过 Qt 的信号与槽机制进行管理和处理,这是 Qt 网络编程的典型风格。
3.2.QNetworkRequest
1. QNetworkRequest(const QUrl &url)
-
功能描述:这是
QNetworkRequest
类最常用的构造函数,用于快速创建一个网络请求对象。 -
参数说明:它接收一个
QUrl
类型的对象作为参数。这个QUrl
对象指定了网络请求的目标地址,例如 "https:/baidu.com"。 -
核心用途:它为整个 HTTP 请求奠定了基础,所有后续的请求头设置、属性配置都是围绕这个初始 URL 进行的。任何一个有效的网络请求都必须从一个明确的 URL 开始。
2. setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value)
-
功能描述:此方法用于为当前网络请求设置一个特定的、预定义的(已知的)HTTP 头部字段。它提供了一种类型安全且易于记忆的方式来设置常用请求头。
-
参数说明:
-
header
:这是一个QNetworkRequest::KnownHeaders枚举类型的值。它指定了你要设置的是哪一个标准的 HTTP 头部字段(例如内容类型、用户代理等)。 -
value
:这是一个QVariant类型的值,这个类型其实是一个类型可变的值,什么都能传,比如说字符串,数字啥的都能传进去,用于提供对应头部字段的具体内容。你需要根据不同的header
类型传入相应格式的值。
-
-
核心用途:精确地控制 HTTP 请求的元数据,与服务器进行正确的通信。例如,告诉服务器你发送的数据是什么格式,或者模拟特定浏览器的请求。
常用 QNetworkRequest::KnownHeaders 枚举值详解
QNetworkRequest::ContentTypeHeader
-
功能描述:用于描述请求体的媒体类型。
-
使用场景:主要在发送 POST、PUT 等带有请求体的请求时使用。它告知服务器应该如何解析请求体中包含的数据。
-
典型取值:
-
"application/x-www-form-urlencoded"
:用于提交普通的网页表单。 -
"application/json"
:用于告知服务器请求体是 JSON 格式的数据。 -
"multipart/form-data"
:用于上传文件。
-
QNetworkRequest::ContentLengthHeader
-
功能描述:用于明确指定请求体的字节长度。
-
使用场景:在发送 POST 等请求时,有时需要显式地设置内容长度。不过,在大多数情况下,当使用
QNetworkAccessManager::post
方法并传入QByteArray
作为数据体时,Qt 网络框架会自动计算并设置此头部,无需手动干预。
QNetworkRequest::UserAgentHeader
-
功能描述:用于标识发出请求的客户端软件(如浏览器、应用程序)的类型、版本和操作系统等信息。
-
使用场景:几乎所有的 HTTP 请求都会设置此头部。服务器常根据此信息来为不同类型的客户端返回优化后的内容(例如,为移动端和桌面端返回不同的页面)。设置一个恰当的 User-Agent 是确保与服务器正常交互的重要一环。
QNetworkRequest::LocationHeader
-
功能描述:用于响应报文中,指示重定向的目标 URL。
-
使用场景:请注意,这个头部通常在服务器返回的响应(
QNetworkReply
)中读取,而不是在请求(QNetworkRequest
)中设置。 当服务器返回 3xx 重定向状态码时,此头部会包含客户端应该自动跳转的新地址。
QNetworkRequest::CookieHeader
-
功能描述:用于在请求中向服务器发送之前存储的 Cookie 信息。
-
使用场景:当需要手动管理或设置特定的 Cookie 时使用。不过,更常见的做法是使用
QNetworkAccessManager
内建的QNetworkCookieJar
来自动管理 Cookie 的存储与发送,这样可以避免手动设置此头部。
3.3.QNetworkReply
1. 方法:error()
-
文字描述:这个方法用于获取网络请求过程中发生的错误类型。它返回一个 QNetworkReply::NetworkError 枚举值。
-
如果请求成功完成,没有发生任何错误,它的返回值将是
QNetworkReply::NoError
。 -
如果请求失败了(例如,服务器返回 404,连接超时,连接被拒绝等),这个方法就会返回一个特定的错误代码,帮助你精确地定位问题所在。
-
使用场景:通常在一个请求完成(例如在
finished()
信号的槽函数中)后,你首先会调用这个方法来检查请求是否成功。如果不是NoError
,你就可以根据具体的错误码来执行不同的错误处理逻辑。 -
示例:比如,你可以检查错误码是否是
QNetworkReply::ContentNotFoundError
(对应 HTTP 404),然后提示用户“您访问的页面不存在”。
2. 方法:errorString()
-
这个方法为
error()
返回的错误代码提供了一个人类可读的、描述性的文本信息。 -
当
error()
返回的不是NoError
时,调用这个方法可以获取到一段详细的、易于理解的错误描述,比如 “Connection refused” 或 “Host not found”。 -
使用场景:它通常与
error()
方法配合使用。在调试程序或者需要向用户显示错误信息时,errorString()
提供的信息比一个单纯的枚举值要友好和直观得多。 -
示例:如果
error()
返回了ConnectionRefusedError
,那么errorString()
可能会返回类似于 “The remote server refused the connection (the server is not accepting requests)” 的字符串,你可以直接把这个字符串显示在用户界面上。
3. 方法:readAll()
-
这个方法从回复对象中一次性读取所有剩余的可用数据,并以
QByteArray
的形式返回。 -
因为
QNetworkReply
是QIODevice
的子类,所以它继承了所有关于读写的功能。这个方法会一次性将整个响应体(Response Body)的内容读取出来。 -
使用场景:当服务器返回的数据量不大,或者你确定可以一次性处理完所有数据时,使用这个方法非常方便。例如,请求一个 JSON 接口或者一个小的文本文件,在收到
finished()
信号后,调用readAll()
就能拿到全部内容。 -
注意:对于非常大的数据(如文件下载),使用
read()
或readLine()
进行流式读取是更好的选择,以避免占用过多内存。
4. 方法:header(QNetworkRequest::KnownHeaders header)
-
文字描述:这个方法允许你从 HTTP 响应头中获取一个“已知的”、“标准的”头部字段的值。
-
你需要传入一个 QNetworkRequest::KnownHeaders 枚举值来指定你想要哪个HTTP头部字段。
-
该方法返回一个 QVariant,里面包含了该头部的解析值(如果是日期,会是 QDateTime;如果是内容类型,会是字符串等)。
-
使用场景:当你需要检查服务器返回的特定元信息时使用。最典型的用法是检查
ContentTypeHeader
来确定返回数据的格式(如 “application/json”),或者检查LocationHeader
来处理 HTTP 重定向。
3.4.核心信号——QNetworkReply::finished
QNetworkReply类(HTT响应类)有一个信号——finished,这个很重要啊。
首先,你需要理解 Qt 网络请求的 “异步” 本质。
-
同步请求:你发送一个请求后,程序就会“卡”在那里,一直等到服务器返回数据,才继续往下执行。这就像你打电话给朋友问一个问题,你拿着电话不说话,一直等到他回答你。
-
异步请求:你发送一个请求后,程序不会“卡住”,而是立刻继续执行后面的代码。当服务器的数据返回时,会通过一个 “信号” 来通知你。这就像你发短信问朋友问题,发完之后你就可以去干别的事了(喝茶、看书),等他回复你了,你的手机会 “响铃”(信号),你听到铃声再去看消息(处理数据)。
QNetworkReply::finished 就是这个 “闹钟铃声” 或 “短信提示音”。
触发时机
QNetworkReply::finished信号在以下情况下会被触发:
-
请求成功完成:当网络请求正常完成,并且接收到了服务器的完整响应(包括HTTP状态码、响应头和数据)时,也就是当所有HTTP响应数据(包括响应头和响应体)都已经被完整接收并准备好可供读取时,会触发finished信号。此时,你可以通过QNetworkReply的方法读取响应数据。
-
请求失败:当网络请求过程中发生错误(例如,连接超时、主机找不到、SSL错误等)时,也会触发finished信号。此时,你可以通过QNetworkReply的error()方法获取错误信息,并且可能无法读取到有效的响应数据。
-
请求被取消:如果你调用了QNetworkReply的abort()方法或close()方法取消请求,那么也会触发finished信号。这表示请求被中途取消,不会继续等待服务器的响应。
-
请求重定向:当遇到HTTP重定向(如301、302状态码)时,Qt的网络模块会自动处理重定向(除非你设置了不自动重定向)。在最终的重定向请求完成(无论成功还是失败)后,会触发finished信号。注意:在重定向过程中,你可能会收到多个QNetworkReply对象的finished信号(每个重定向步骤都有一个),但通常你只需要关注最初发出的请求(或最终重定向到的请求)的finished信号。
-
网络断开等异常:如果在请求过程中网络连接断开,或者服务器在传输过程中中断连接,也会触发finished信号,并伴随着相应的错误。
重要细节
-
信号触发意味着请求结束:一旦finished信号被触发,就意味着这个QNetworkReply对象所代表的网络请求已经结束,不会再有任何数据可读(除非已经读过了),也不会再有任何状态变化。
-
与readyRead信号的区别:readyRead信号是在有数据可读时触发,可能会多次触发(因为数据是分块到达的)。而finished信号只触发一次,即在请求完全结束时。
-
与errorOccurred信号的关系:Qt 5.15引入了errorOccurred信号(在Qt6中改为errorOccurred),它会在发生错误时触发。注意,errorOccurred信号触发后,finished信号仍然会触发。因此,通常我们只需要连接finished信号,在槽函数中检查错误并处理数据即可。
3.4.简单示例——HTTP客户端
我们创建一个新项目
大家千万别忘了下面这个
我们
此处展示的响应内容,通常可使用 QPlainTextEdit 组件进行呈现。
该组件能够保留文本的原始格式,便于用户直接查看未经渲染的 HTML 代码或其他文本结构。
而如果使用 QTextEdit 组件,由于它内置了对 HTML 的解析与渲染能力,最终呈现的将是经过格式处理后的视觉效果,而非原始代码形态。
QTextEdit 在背后执行了复杂的布局和样式计算,因此当需要处理较大或结构复杂的 HTML 内容时,可能会引起界面卡顿或响应延迟的问题。
我们现在就来编写代码
我们运行一下,我们去访问一下http://www.baidu.com
点击发送请求
这个程序就没有任何问题的。