📚 博主的专栏

🐧 Linux   |   🖥️ C++   |   📊 数据结构  | 💡C++ 算法 | 🅒 C 语言  | 🌐 计算机网络 |🗃️ mysql

本文摘要:本文介绍了Qt框架中的窗口部件、系统相关功能及网络编程实现。主要内容包括:1. 浮动窗口QDockWidget的创建与停靠位置设置;2. 对话框QDialog的使用及模态/非模态特性;3. 标准对话框(消息框、颜色选择、文件操作等)的实现方法;4. 事件处理机制和信号槽原理;5. 多线程编程中的线程安全与锁机制;6. 网络编程实现,包括UDP/TCP通信案例和HTTP客户端开发。通过具体代码示例,详细讲解了Qt在GUI开发中的核心功能实现方式,特别强调了信号槽机制在多线程和网络编程中的重要作用。

目录

一、窗口

二、系统相关

事件:

QFileInfo-文件和目录信息类

QThread-多线程

线程安全问题-加锁、条件变量

Qt 网络编程

UDP Demo

核心 API 概览

QUdpSocket

QNetworkDatagram

UDP回显服务器

TCP Demo

核心 API 概览

QTcpServer:

QTcpSocket:

TCP回显服务器

HTTP 客户端

核心 API

1. QNetworkAccessManager

2. QNetworkRequest

3. QNetworkReply


一、窗口

QDockWidget-浮动窗口

浮动窗口的创建 通过QDockWidget类的构造函数可以动态创建浮动窗口,示例如下:

// 创建浮动窗口
QDockWidget *dockwidget = new QDockWidget("浮动窗口", this);
// 将浮动窗口添加至当前窗口底部
addDockWidget(Qt::BottomDockWidgetArea, dockwidget);

设置停靠位置 浮动窗口可停靠在中心部件四周,通过setAllowedAreas()方法可设置允许停靠的区域,可选位置包括:

  • Qt::LeftDockWidgetArea:左侧停靠
  • Qt::RightDockWidgetArea:右侧停靠
  • Qt::TopDockWidgetArea:顶部停靠
  • Qt::BottomDockWidgetArea:底部停靠
  • Qt::AllDockWidgetAreas:所有位置均可停靠

示例:限制浮动窗口仅可上下停靠

// 设置浮动窗口仅允许在顶部和底部停靠
dockwidget->setAllowedAreas(Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea);

QDialog-对话框

对话框简介 对话框是GUI程序中的重要组件,主要用于实现不适合在主窗口展示的功能。作为顶层窗口,对话框通常显示在程序界面的最上层,用于处理短期任务或简单的用户交互。

Qt提供了多种内置对话框,包括:

  • QFileDialog(文件对话框)
  • QColorDialog(颜色对话框)
  • QFontDialog(字体对话框)
  • QInputDialog(输入对话框)
  • QMessageBox(消息框)

QDialog继承自QWidget,因此QWidget的各种属性方法、QDialog也能使用

在点击关闭按钮的时候,delete dialog

QDialog有一个属性:

//delete dialog;
// 正确的做法是将 delete 操作与关闭按钮的点击信号关联,
// 当用户点击关闭时触发 delete 操作。
// Qt 为方便开发,直接在 QDialog 中提供了相关属性,
// 只需设置该属性即可实现上述效果。

自定义对话框:

新的类,继承自QDialog:

模态/非模态

模态:dialog.exec();

非模态:dialog.show();

混合属性对话框

混合属性对话框兼具模态与非模态对话框的特性:其生成和销毁过程遵循非模态对话框的机制,而在功能实现上则具备模态对话框的特点。

通过调用QDialog::setModal()函数即可创建具备混合特性的对话框。需要注意的是,创建对话框时通常需要为其指定父组件。

示例代码:

Qt内置对话框

Qt提供了多种可复用的对话框类型,即Qt标准对话框。Qt标准对话框全部继承于QDialog类。常用
标准对话框如下:

消息对话框(QMessageBox)- 显示一个消息给用户,并且让用户进行一个简单的选择

消息对话框是应用程序中最常用的界面元素之一,主要用于向用户提示重要信息并要求其进行选择操作。

QMessageBox类提供了静态成员函数,可直接调用创建不同风格的消息对话框,包括:

Question-用于正常操作过程中的提问
Information-用于报告正常运行信息
Warning-用于报告非关键错误
Critical-用于报告严重错误

运行:

当没有设置setStandardButtons的时候,就只有ok按钮

单种类型对话框:

QColorDialog

颜色对话框(QColorDialog) 颜色对话框提供用户选择颜色的功能,继承自QDialog类。其界面显示如下:

常用方法介绍:

  1. QColorDialog(QWidget* parent = nullptr)
    创建对话框对象并设置父窗口

  2. QColorDialog(const QColor& initial, QWidget* parent = nullptr)
    创建对话框对象,同时设置默认颜色和父窗口

  3. void setCurrentColor(const QColor& color)
    设置当前对话框颜色

  4. QColor currentColor() const
    获取当前对话框颜色

  5. QColor getColor(const QColor& initial = Qt::white, QWidget* parent = nullptr, const QString& title = QString(), QColorDialog::ColorDialogOptions options = ColorDialogOptions())
    打开颜色选择对话框并返回选中的QColor对象
    参数说明:

    • initial: 默认颜色
    • parent: 父窗口
    • title: 对话框标题
    • options: 对话框选项
  6. void open(QObject* receiver, const char* member)
    打开颜色对话框

文件对话框 QFileDialog

文件对话框用于应用程序中需要打开外部文件或将当前内容保存到指定外部文件的场景。

常用方法:

打开单个文件

QString getOpenFileName(
    QWidget *parent = nullptr,
    const QString &caption = QString(),
    const QString &dir = QString(),
    const QString &filter = QString(),
    QString *selectedFilter = nullptr,
    QFileDialog::Options options = Options()
)

打开多个文件

QStringList getOpenFileNames(
    QWidget *parent = nullptr,
    const QString &caption = QString(),
    const QString &dir = QString(),
    const QString &filter = QString(),
    QString *selectedFilter = nullptr,
    QFileDialog::Options options = Options()
)

保存文件

QString getSaveFileName(
    QWidget *parent = nullptr,
    const QString &caption = QString(),
    const QString &dir = QString(),
    const QString &filter = QString(),
    QString *selectedFilter = nullptr,
    QFileDialog::Options options = Options()
)

参数说明:

  • parent:父窗口
  • caption:对话框标题
  • dir:默认打开路径
  • filter:文件过滤器

字体对话框 (QFontDialog)

Qt 框架提供了预定义的字体选择对话框类 QFontDialog,该组件专门用于实现字体选择功能。

输入对话框 (QInputDialog)

输入对话框......等

小总结:

二、系统相关

分为五大点来讲:事件、文件操作、多线程编程、网络编程、多媒体(音频、视频)

事件:

信号槽机制
当用户执行操作时,系统会生成信号。开发者可以为特定信号绑定相应的槽函数。当信号被触发时,对应的槽函数就会自动执行。

事件机制与之类似
用户操作同样会触发事件,开发者可以为事件关联处理函数(包含业务逻辑),当事件发生时,就会执行相应的处理代码。

事件机制本质上是操作系统提供的功能。Qt框架对操作系统的事件机制进行了封装。
不过由于直接使用事件机制编写代码较为繁琐,Qt在事件机制的基础上进一步封装,最终形成了信号槽机制。

信号槽是对事件机制的更高层封装
事件机制是信号槽实现的底层基础

事件介绍

事件是应用程序内部或外部发生的动作或状态的统称。在Qt中,事件通过QEvent类的子类对象来表示,所有Qt事件都继承自抽象基类QEvent。这些事件可能由系统或Qt平台在各种情况下触发,比如用户操作(如点击鼠标、敲击键盘)或系统状态变化(如窗口需要重绘)。

根据触发源不同,事件可分为两类:

  1. 用户操作触发的事件:如键盘事件、鼠标事件
  2. 系统自动触发的事件:如定时器事件

以下是常见的Qt事件类型:

事件名称 描述
鼠标事件 鼠标左键、鼠标右键、鼠标滚轮,鼠标的移动,鼠标按键的按下和松开
键盘事件 按键类型、按键按下、按键松开
定时器事件 定时时间到达
进入离开事件 鼠标的进入和离开
滚轮事件 鼠标滚轮滚动
绘屏事件 重绘屏幕的某些部分
显示隐藏事件 窗口的显示和隐藏
移动事件 窗口位置的变化
窗口事件 是否为当前窗口
大小改变事件 窗口大小改变
焦点事件 键盘焦点移动
拖拽事件 用鼠标进行拖拽

要熟悉C++多态和继承的语法:

示例:

注意:要想重写父类的函数:就要确保我们写的函数名和函数参数列表都完全一致(形参名无所谓、类型要相同):可以去帮助里面搜索然后复制

鼠标在进出的时候,就会有事件的发生

mousePressEvent(QMouseEvent *event)-鼠标按压事件

点击时,显示界面的坐标

mouseReleaseEvent(QMouseEvent *event)鼠标释放事件

QWheelEvent滚轮滚动事件

event->delta(),还可以设定值来+=,得到一个累加的数值

键盘输入事件:keyPressEvent(QKeyEvent*)

QShortCut:定义一个快捷键、搭配QCursors,得到是怎么一个按键序列

运行情况

和焦点是有关的,选中对应的控件、窗口被选中,激活时,才奏

QTimerEvent-定时器事件:

QTimer实现了定时器功能

在QTimer背后是QTimerEvent定时器事件进行支撑的

QObject提供了一个timerEvent这个函数

startTimer启动定时器

killTimer关闭定时器

定时器

moveEvent和resizeEvent - 窗口移动和大小改变事件

QFileInfo-文件和目录信息类

QFileInfo 是 Qt 提供的用于获取文件和目录信息的核心类,能够获取文件名、文件大小、文件修改日期等多种信息。该类提供了丰富的成员方法,常用功能包括:

  • isDir():检查是否为目录
  • isExecutable():检查文件是否可执行
  • fileName():获取完整文件名
  • completeBaseName():获取无后缀的文件名
  • suffix():获取文件后缀名
  • completeSuffix():获取完整文件后缀
  • size():获取文件大小
  • isFile():判断是否为常规文件
  • fileTime():获取文件时间信息(创建时间/修改时间/访问时间等)

QThread-多线程

Qt多线程和Linux中的线程本质是一个东西

有些场景对性能追求到极致(游戏引擎、AI、做高性能服务器)

但Qt做客户端开发、客户端性能不拉跨就行 

API 描述
run() 线程的入口函数。
start() 通过调用 run() 开始执行线程。操作系统将根据优先级参数调度线程。如果线程已经在运行,这个函数什么也不做。
currentThread() 返回一个指向管理当前执行线程的 QThread 的指针。
isRunning() 如果线程正在运行则返回 true;否则返回 false
sleep() / msleep() / usleep() 使线程休眠,单位为秒/毫秒/微秒。
wait() 阻塞线程,直到满足以下任何一个条件:与 QThread 对象关联的线程完成执行(从 run() 返回),如果线程已完成,将返回 true;如果线程尚未启动,也返回 true。也可以根据时间超时,如果超时则返回 false
terminate() 终止线程的执行。线程可以立即终止,但这也取决于操作系统的调度策略。在调用 terminate() 后,建议使用 QThread::wait() 来确保线程已终止。
finished() 当线程结束时会发出该信号,可以通过该信号来实现线程的清理工作。

运行步骤:        

线程安全问题-加锁、条件变量

从服务器开发的角度来看,多线程的主要目的是充分利用多核CPU的计算资源(比如双路CPU架构)。然而在客户端开发中,多线程的关注点则完全不同。

对于普通用户而言,"使用体验"至关重要。即使家用PC配备多核CPU,也很少有客户端程序会完全占用CPU资源。用户不会接受"系统卡顿"来换取"运行速度"。

客户端多线程的核心价值在于:通过多线程执行耗时操作(如网络通信或密集I/O),避免阻塞主线程。例如:

  • 大文件上传/下载(耗时可能达20分钟)
  • 持续的文件写入操作(如QFile.write)

当主线程被阻塞时,程序将无法响应用户操作。更合理的做法是:

  1. 创建独立线程处理耗时I/O
  2. 保持主线程负责事件循环和用户交互

典型案例: 游戏启动时需要加载大量资源(文件/网络)。如果将所有操作放在主线程,当用户频繁点击窗口时,系统可能会提示"程序未响应"。

把多个线程要访问的公共资源,通过锁保护起来、把并发执行变成串行执行

Qt下的QMutex 和 Linux下std::mutex的作用与使用相差不大

示例:两个线程针对同一个变量进行循环增加

#include "Thread.h"

int Thread::num = 0;

Thread::Thread()
{
    
}

void Thread::run()
{
    for(int i = 0; i < 50000; i++){
        num++;
    }
}
#ifndef THREAD_H
#define THREAD_H

#include <QWidget>
#include <QThread>
class Thread : public QThread
{
    Q_OBJECT
public:
    Thread();
    void run() override;    
    //添加一个static成员,作为公共修改的对象
    static int num;
};

#endif // THREAD_H
#include "widget.h"
#include "ui_widget.h"
#include "Thread.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    
    //创建两个线程对象
    Thread t1;
    Thread t2;
    t1.start();
    t2.start();
    
    //打印结果:
    qDebug() << Thread::num;
}

Widget::~Widget()
{
    delete ui;
}

由于三个线程(除 t1、t2 之外还有一个主线程)之间是并发执行的关系,当  t1 和 t2 运行起来之后,主线程,仍然会往后执行,执行到打印的时候,大概率 t1 、t2还没执行完

因此对t1、t2加上线程等待,让主线程等待这俩线程结束

此时打印出的结果:可能是小于100000的,这是因为出现了线程安全问题、因此可以使用加锁的方式来解决

#include "Thread.h"

int Thread::num = 0;
QMutex Thread::mutex;
Thread::Thread()
{

}

void Thread::run()
{
    for(int i = 0; i < 50000; i++){
        mutex.lock();
        num++;//两个线程访问的公共变量、如果是之前的并发执行、就可能第一个
        //线程执行了一半、第二个线程又进行修改、就容易出现问题(不是原子性操作
        mutex.unlock();
    }
}
    //创建一个锁对象,两个线程共用同一个锁对象
    static QMutex mutex;

但是容易忘记释放、因此在Qt中也有和Linux系统下std::lock_guard(mutex)作用一样的自动释放锁的QMutexLocker:

#include "Thread.h"
#include <QMutexLocker>
int Thread::num = 0;
QMutex Thread::mutex;
Thread::Thread()
{

}
void Thread::run()
{
    for(int i = 0; i < 50000; i++){
        QMutexLocker locker(&mutex);        
        num++;//两个线程访问的公共变量、如果是之前的并发执行、就可能第一个
    }
}

Qt的条件变量和信号量也与Linux中谈到的条件变量和信号量完全一致,条件变量、信号量

,可以通过我的文章去进一步了解

注意:这里使用的是while而不是if、本质上是为了、唤醒之后,还需要再确认一下、当前条件是否真的成立了,wait可能被提前唤醒、可能信号被打断了

信号量

在多线程编程中,经常需要控制多个线程对有限资源的并发访问。例如,当程序运行在内存受限的设备上时,我们希望内存密集型线程能够根据可用内存量调整自身行为。这类资源管理问题通常可以通过信号量来解决。

信号量是一种增强版的互斥锁,它不仅支持基本的加锁和解锁操作,还能动态跟踪可用资源的数量。

特点

  • QSemaphore是Qt框架提供的计数信号量实现

  • 用于精确控制可同时访问共享资源的线程数量

用途

  • 限制并发线程数

  • 解决资源受限场景下的线程同步问题

Qt 网络编程

网络编程(操作系统提供的Socket API)

C++新标准中引入了许多新特性 C++标准库本身并未提供网络编程API的封装 进行网络编程本质上是在编写应用层代码,需要传输层协议支持 传输层最核心的协议是UDP和TCP,这两种协议存在显著差异 Qt为此也提供了两套不同的API

为什么Qt要采用模块化设计?

Qt是一个功能全面的大型框架 如果将所有功能都集成到核心模块中 即使编写简单的"Hello World"程序 生成的可执行文件也会变得非常庞大 (在嵌入式系统中尤其明显) 这会导致包含大量未被使用的功能

Qt采用模块化设计:

  • 核心功能保留在主模块中
  • 其他功能分别封装为独立模块
  • 默认情况下这些附加模块不会参与编译
  • 需要在.pro文件中显式引入所需模块

Qt 其实提供了静态库的版本和动态库的版本

UDP Demo

核心 API 概览

主要的类有两个:QUdpSocketQNetworkDatagram

QUdpSocket

表示一个 UDP 的 socket 文件。

名称 类型 说明 对标原生 API
bind(const QHostAddress&,quint16) 方法 绑定指定的端口号。 bind
receiveDatagram() 方法 返回 QNetworkDatagram,读取一个 UDP 数据报。 recvfrom
writeDatagram(const QNetworkDatagram&) 方法 发送一个 UDP 数据报。 sendto
readyRead 信号 在收到数据并准备就绪后触发。 无(类似于 IO 多路复用的通知机制)
QNetworkDatagram

表示一个 UDP 数据报。

名称 类型 说明 对标原生 API
QNetworkDatagram(const QByteArray&, const QHostAddress&, quint16) 构造函数 通过 QByteArray,目标 IP 地址,目标端口号构造一个 UDP 数据报。通常用于发送数据时。
data() 方法 获取数据报内部持有的数据。返回 QByteArray。
senderAddress() 方法 获取数据报中包含的对端的 IP 地址。 无(recvfrom 包含了该功能)
senderPort() 方法 获取数据报中包含的对端的端口号。 无(recvfrom 包含了该功能)
UDP回显服务器

示例:写一个带有界面的UDP回显服务器、但是一个正经服务器、很少会有图形化界面(一般都是命令行)

 二、服务器端代码讲解(基于UDP协议)

(一)widget.h

使用QUdpSocket需要现在.pro文件中已进入netword,客户端同理

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QUdpSocket>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;
    
    QUdpSocket* socket;
    
    void processRequest();
    QString process(const QString& req);
};
#endif // WIDGET_H

头文件定义与包含

  • 与客户端类似,使用#ifndef#define#endif防止重复包含,包含了QWidgetQUdpSocket头文件。

  • QUdpSocket* socket:指向QUdpSocket对象的指针。

私有成员函数

  • void processRequest():用于处理客户端的请求。

  • QString process(const QString& req):用于根据请求计算响应,这里是一个简单的回显功能。

(二)widget.cpp

#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QNetworkDatagram>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    
    //创建出这个对象
    socket = new QUdpSocket(this);//也可以挂到Qt的对象树上
    
    //设置窗口标题
    this->setWindowTitle("服务器");
    //一定是先连接信号槽
    connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
    //再绑定端口号(这是因为、一旦绑定端口了、意味着,请求可以被收到了)
    bool ret = socket->bind(QHostAddress::Any, 8080);
    //用Any的意思是,无论我们有几个网卡都能绑定上
    //一个端口号、只能被一个socket绑定
    if(!ret){
        QMessageBox::critical(this, "服务器启动出错", socket->errorString());
        return ;
    }   
}

Widget::~Widget()
{
    delete ui;
}

//这个函数完成的逻辑就是服务器最核心的逻辑
void Widget::processRequest()
{
    //1、读取请求并解析
    //返回数据报
    const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
    QString request = requestDatagram.data(); //返回的是一个QByteArray(但QByteArray是可以赋值给QString的)
    //2、根据请求计算响应(由于是回显服务器、相应不需要计算、就是请求本身)
    const QString & response = process(request);
    //3、把响应写回给客户端
    //response.toUtf8() 是用于取出QString内部的字节数组
    //构造好了一个响应数据包
    QNetworkDatagram responseDatagram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort());
    socket->writeDatagram(responseDatagram);
    
    //把这次交互的信息,显示到界面上
    QString log = "[" + requestDatagram.senderAddress().toString() + ":" +QString::number(requestDatagram.senderPort())
            + "] req: " + request + ", resp: " + response;
    ui->listWidget->addItem(log);
}

QString Widget::process(const QString &req)
{
    //由于当前是回显服务器、响应就是和请求完全一致
    //但对于一个成熟的商业服务器、请求->相应计算过程相当复杂、(业务逻辑
    return req;
}

构造函数实现

  • 调用ui->setupUi(this)初始化界面。

  • 创建QUdpSocket对象socket,并将其父对象设置为当前Widget对象。

  • 设置窗口标题为“服务器”。

  • 使用connect()函数将socketreadyRead信号连接到processRequest槽函数,当有数据可读时,触发processRequest函数来处理客户端请求。

  • 调用socket->bind(QHostAddress::Any, 8080)socket绑定到本机的所有网络接口的8080端口。QHostAddress::Any表示可以接收来自任何网络接口的连接。如果绑定失败,会弹出一个错误消息框显示错误信息。

processRequest槽函数实现

  • 调用socket->receiveDatagram()接收一个客户端的请求数据报,并存储在requestDatagram中。

  • 获取数据报中的数据并转换为QString类型的request

  • 调用process(request)函数计算响应,这里是一个简单的回显,即响应等于请求。

  • 使用响应数据构造一个新的QNetworkDatagram对象responseDatagram,并发送回给客户端(使用客户端的IP地址和端口号)。

  • 构造一个日志字符串,记录客户端的IP地址、端口号、请求内容和响应内容,并将其添加到listWidget控件中显示。

process函数实现

这是一个简单的回显函数,直接返回传入的req参数。在实际的商业服务器中,这里会有复杂的业务逻辑来根据请求计算响应。

客户端实现

(一)widget.h

#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void on_pushButton_clicked();
    void processResponse();
private:
    Ui::Widget *ui;
    QUdpSocket* socket;
};
#endif // WIDGET_H

头文件定义与包含

使用#ifndef#define#endif防止头文件被重复包含。包含了QWidgetQUdpSocket头文件,这是因为Widget类继承自QWidget,并且要使用QUdpSocket类来实现UDP通信功能。

私有槽函数

on_pushButton_clicked():这是一个槽函数,当界面上的pushButton被点击时会被调用,用于处理发送数据的逻辑。

processResponse():也是一个槽函数,当QUdpSocketreadyRead信号触发时被调用,用于处理从服务器接收到的响应数据。

私有成员变量

Ui::Widget *ui:用于访问界面设计文件中的控件。

QUdpSocket* socket:指向一个QUdpSocket对象的指针,用于UDP通信。

(二)widget.cpp

#include "widget.h"
#include "ui_widget.h"
#include <QNetworkDatagram>

const QString& SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 8080; //本质上quint16就是一个unsigned short
//虽然 short 通常都是两个字节、但是C++标准中没有明确的规定的说明
//、只是说、short不应该少于2个字节
//端口号本质上是、一个2字节的无符号整数

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    socket = new QUdpSocket(this);

    //修改窗口标题、方便区分这是一个客户端程序
    this->setWindowTitle("客户端");

    //通过信号槽、来处理服务器返回的数据
    connect(socket, &QUdpSocket::readyRead, this, &Widget::processResponse);

}

Widget::~Widget()
{
    delete ui;
}


void Widget::on_pushButton_clicked()
{
    //1、获取到输入框的内容
    const QString& text = ui->lineEdit->text();
    //2、构造UDP的请求数据
    QNetworkDatagram requestDatagram(text.toUtf8(), QHostAddress(SERVER_IP), SERVER_PORT);
    //3、发送请求数据
    socket->writeDatagram(requestDatagram);
    //4、把发送的请求添加到列表框中
    ui->listWidget->addItem("客户端说:" + text);
    //5、把输入框的内容清空
    ui->lineEdit->setText("");
}

void Widget::processResponse()
{
    //通过这个函数来处理接收到的响应
    //    1、先读取到响应数据
    const QNetworkDatagram& responseDatagram = socket->receiveDatagram();
    //    2、把响应显示到界面上
    QString response = responseDatagram.data();
    ui->listWidget->addItem("服务器说:" +response);
}

  包含头文件与常量定义

 包含了"widget.h""ui_widget.h"头文件和QNetworkDatagram头文件。

定义了SERVER_IPSERVER_PORT两个常量,分别表示服务器的IP地址和端口号。 

构造函数实现

调用ui->setupUi(this)初始化界面。

创建一个QUdpSocket对象socket,并将其父对象设置为当前Widget对象。

设置窗口标题为“客户端”。

使用connect()函数将socketreadyRead信号连接到processResponse槽函数,当有数据可读时,触发processResponse函数来处理响应。

on_pushButton_clicked槽函数实现

  • 获取lineEdit控件中的文本内容。

  • 使用获取到的文本内容构造一个QNetworkDatagram对象requestDatagram,指定目标IP地址为SERVER_IP,目标端口号为SERVER_PORT

  • 调用socket->writeDatagram(requestDatagram)将构造好的数据报发送出去。

  • 将发送的请求内容添加到listWidget控件中显示。

  • 清空lineEdit控件中的内容。

processResponse槽函数实现

  • 调用socket->receiveDatagram()接收一个数据报,并存储在responseDatagram中。

  • 获取数据报中的数据,并将其转换为QString类型的response

  • 将接收到的服务器响应内容添加到listWidget控件中显示。 

 

运行结果:

启动多个客户端:

多点开几次可执行程序就行,找到你项目可执行程序的位置,多点开几个

TCP Demo

核心 API 概览

核心类是两个:QTcpServerQTcpSocket

QTcpServer

用于监听端口和获取客户端连接。

名称

类型

说明

对标原生 API

listen(const QHostAddress&, quint16 port)

方法

绑定指定的地址和端口号并开始监听

bind 和 listen

nextPendingConnection()

方法

从系统中获取一个已经建立好的 tcp 连接

返回一个 QTcpSocket,表示这个客户端的连接

通过这个 socket 对象完成和客户端之间的通信

accept

newConnection

信号

有新的客户端建立连接好之后触发

无(类似于IO多路复用中的通知机制)

QTcpSocket

用于客户端和服务器之间的数据交互。

名称

类型

说明

对标原生 API

readAll()

方法

读取当前接收缓冲区中的所有数据

返回 QByteArray 对象

read

write(const QByteArray&)

方法

把数据写入 socket 中

write

deleteLater

方法

暂时把 socket 对象标记为无效

Qt 会在下个事件循环中析构释放该对象

无(类似于“半自动化的垃圾回收”)

readyRead

信号

有数据到达并准备就绪时触发

无(类似于 I/O 多路复用中的通知机制)

disconnected

信号

连接断开时触发

无(类似于 I/O 多路复用中的通知机制)

QByteArray 说明:用于表示一个字节数组,可以很方便地和 QString 进行相互转换。例如:

  • 使用 QString 的构造函数即可把 QByteArray 转成 QString

  • 使用 QStringtoUtf8() 函数即可把 QString 转成 QByteArray

TCP回显服务器

Tcp Server 实现

widget.h(服务器端)

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTcpServer>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
    void processConnection();
    QString process(const QString& request);
private:
    Ui::Widget *ui;
    QTcpServer* tcpServer;
};
#endif // WIDGET_H
  • 定义了一个Widget类,继承自QWidget

  • 包含了QTcpServer头文件,用于TCP服务器功能。

  • 声明了构造函数、析构函数、processConnection槽函数和process成员函数。

  • 定义了私有成员变量ui(用于界面)和tcpServer(用于TCP服务器)。

widget.cpp(服务器端)

#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QTcpSocket>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    
    //1、修改窗口标题
    this->setWindowTitle("服务器");
    
    //2、创建QTcpServer实例
    tcpServer = new QTcpServer(this);
    
    //3、通过信号槽、指定如何处理连接
    connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);
    //4、绑定并监听端口号:一定要确保准备工作充分
    //该操作得是初始化的最后一步、都是需要把如何处理连接、如何处理请求、都准备好
    //才能真正的绑定端口并监听
    bool ret = tcpServer->listen(QHostAddress::Any, 8080);
    if(!ret){
        QMessageBox::critical(this, "服务器启动失败", tcpServer->errorString());
        exit(1);
    }
}

Widget::~Widget()
{
    delete ui;
}

void Widget::processConnection()
{
    // 1、通过tcpServer 拿到一个socket 对象,通过对象来和客户端进行通信
    QTcpSocket* listenSocket = tcpServer->nextPendingConnection(); 
    QString log = "[" + listenSocket->peerAddress().toString() + QString::number(listenSocket->peerPort())+
            "] 客户端上线!!";
    //peerAddress(对端地址)、peerPort(对端端口号)
    ui->listWidget->addItem(log);
    
    // 2、通过信号槽、来处理客户端发来请求的情况,以下代码、天然就会执行多次、根据信号槽触发、有新请求,就会触发
    connect(listenSocket, &QTcpSocket::readyRead, this, [=](){
        // a) 读取出请求data、此处readAll返回的是QByteArray,通过赋值转成QString
        QString request = listenSocket->readAll();
        // b) 根据请求处理响应
        const QString& response = process(request);
        // c) 把响应写回客户端
        listenSocket->write(response.toUtf8());
        // d) 把上述信息记录到日志中
        QString log = "[" + listenSocket->peerAddress().toString() + ":" +
                QString::number(listenSocket->peerPort()) + "]" + 
                "req: " + request + ", resp: " + response;
        ui->listWidget->addItem(log);
    });
    //3、通过信号槽,来处理客户端断开连接的情况
    connect(listenSocket, &QTcpSocket::disconnected, this, [=](){
        // a) 把断开连接的信息通过日志显示出来
        QString log = "[" + listenSocket->peerAddress().toString() + ":" +
                QString::number(listenSocket->peerPort()) + "]客户端下线!!";
        ui->listWidget->addItem(log);
        // b) 手动释放 、直接使用delete是下策,直接使用deleteLater()更加合适
//        delete listenSocket;        
        listenSocket->deleteLater();
        
    });
}

//此处是回显服务器、因此请求和响应相同
QString Widget::process(const QString& request)
{
    return request;
}
  • 包含了必要的头文件,如widget.hui_widget.hQMessageBoxQTcpSocket

  • 在构造函数中:

    • 调用ui->setupUi(this)初始化界面组件。

    • 设置窗口标题为“服务器”。

    • 创建一个QTcpServer实例tcpServer,用于监听传入的TCP连接。

    • QTcpServernewConnection信号连接到processConnection槽函数,以便在有新连接时进行处理。

    • 调用tcpServer->listen(QHostAddress::Any, 8080)绑定端口并开始监听。如果绑定失败,会弹出错误消息框并退出程序。

  • processConnection函数中:

    • 使用tcpServer->nextPendingConnection()获取一个新的QTcpSocket对象,用于与客户端通信。

    • 获取客户端的IP地址和端口号,并将其显示在列表控件中。

    • QTcpSocketreadyRead信号连接到一个匿名函数,当客户端发送数据时,读取数据并调用process函数处理请求,然后将响应写回客户端,并记录日志。

    • QTcpSocketdisconnected信号连接到另一个匿名函数,当客户端断开连接时,记录日志并释放QTcpSocket对象。

  • process函数是一个简单的回显函数,直接返回接收到的请求数据。

 

这段代码在实际应用中还不够严谨,虽然作为简单的回显服务器已经够用了。

在使用TCP协议时,需要注意它是面向字节流的。这意味着一个完整的请求可能会被拆分成多个数据包进行传输。虽然TCP协议本身已经帮我们解决了很多底层问题,但它并不负责区分应用层数据报的边界(也就是常见的粘包问题)。

更合理的做法是:将每次接收到的数据存入一个大型字节数组缓冲区,并预先定义好应用层协议格式(如分隔符、长度标识等方法),然后按照协议规范对缓冲区数据进行精确解析处理。

通过信号槽机制处理客户端断开连接事件

connect(clientSocket, &QTcpSocket::disconnected, this, [=]() {
    // a) 记录客户端下线日志
    QString log = QString("[%1:%2] 客户端下线")
                    .arg(clientSocket->peerAddress().toString())
                    .arg(clientSocket->peerPort());
    ui->listWidget->addItem(log);
    
    // b) 安全释放clientSocket资源
    clientSocket->deleteLater();
});

注意事项:

  1. 本槽函数主要针对clientSocket对象进行操作

  2. 调用deleteLater()而非直接delete,确保对象安全释放

  3. deleteLater()将销毁操作推迟到下一轮事件循环执行(槽函数都是在事件循环中执行的、进入到下一轮事件循环、意味着上一轮事件循环肯定结束了,也意味着当前槽函数执行结束了)

  4. 此方式可避免因提前释放导致的异常问题

  5. 确保释放操作始终能得到执行,不会被中途打断

    widget.cpp(客户端)

    #include "widget.h"
    #include "ui_widget.h"
    #include <QMessageBox>
    
    Widget::Widget(QWidget *parent)
        : QWidget(parent)
        , ui(new Ui::Widget)
    {
        ui->setupUi(this);
    
        this->setWindowTitle("客户端");
        // 2、创建socket实例
        socket = new QTcpSocket(this);
        // 3、和服务器建立连接
        //调用该函数后、系统kernel就会和对方的服务器之间进行三次挥手了(需要消耗时间
        //但此处的函数不会和原生Linux 的tcpsocket api一样是阻塞等待的
        //此处是非阻塞
        socket->connectToHost("127.0.0.1", 8080);
    
        // 4、连接信号槽处理响应
        connect(socket, &QTcpSocket::readyRead, this, [=](){
            // a)读取出响应内容
            QString response = socket->readAll();
            // b)把响应的内容显示到界面上
            ui->listWidget->addItem("服务器说: " +response);
        });
        // 5、等待连接建立的结果、确认是否连接成功
        bool ret = socket->waitForConnected();
        if(!ret){
            QMessageBox::critical(this, "连接服务器出错", socket->errorString());
            exit(1);
        }
    
    }
    
    Widget::~Widget()
    {
        delete ui;
    }
    
    
    void Widget::on_pushButton_clicked()
    {
    //    1、获取到输入框中的内容
        const QString& text = ui->lineEdit->text();
        //2、发送数据给服务器
        socket->write(text.toUtf8());
        //3、把发的消息显示到界面上
        ui->listWidget->addItem("客户端说: " + text);
        //4、清空输入框
        ui->lineEdit->setText("");
    }
    • 包含必要的头文件,如widget.hui_widget.hQMessageBox

    • 在构造函数中:

      • 调用ui->setupUi(this)初始化界面组件。

      • 设置窗口标题为“客户端”。

      • 创建一个QTcpSocket实例socket,用于与服务器通信。

      • 调用socket->connectToHost("127.0.0.1", 8080)尝试连接到服务器。

      • QTcpSocketreadyRead信号连接到一个匿名函数,当服务器发送数据时,读取数据并显示在列表控件中。

      • 调用socket->waitForConnected()等待连接建立,并检查连接是否成功。如果连接失败,会弹出错误消息框并退出程序。

    • on_pushButton_clicked槽函数中:

      • 获取输入框中的文本内容。

      • 将文本内容发送给服务器。

      • 将发送的消息显示在列表控件中。

      • 清空输入框。

    widget.h(客户端)

    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include <QWidget>
    #include <QTcpSocket>
    
    QT_BEGIN_NAMESPACE
    namespace Ui { class Widget; }
    QT_END_NAMESPACE
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    public:
        Widget(QWidget *parent = nullptr);
        ~Widget();
    
    private slots:
        void on_pushButton_clicked();
    
    private:
        Ui::Widget *ui;
        QTcpSocket* socket;
    };
    #endif // WIDGET_H
    • 定义了一个Widget类,继承自QWidget

    • 包含了QTcpSocket头文件,用于TCP通信。

    • 声明了构造函数、析构函数和on_pushButton_clicked槽函数。

    • 定义了私有成员变量ui(用于界面)和socket(用于TCP通信)。

    运行结果:

    关闭客户端窗口后:

    多客户端:

            

    之前在学习 Linux 时写的 TCP 的回显服务器时遇到的问题

    • 问题描述:多个客户端同时访问时,只有一个生效。

    • 解决方法:引入多线程,每个客户端安排一个单独的线程,问题得到改善。

    问题本质原因

    • 问题本质:之前写的程序中,使用了双重循环,里层循环没有及时结束,导致外层循环不能快速地第二次调用到 accept,从而导致第二个客户端无法进行处理。

    • 与 TCP 和多线程的关系:这个问题和 TCP、多线程本身没有关系。从来没有说法说 TCP 服务器必须使用多线程编写。

    引入多线程的本质作用

    • 作用:引入多线程,本质上是把双重循环化简成两个独立的循环。

    Qt 服务器程序的特点

    • 特点:在 Qt 的服务器程序中,其实一个循环都没写,而是通过 Qt 内置的信号槽来驱动的。

    • 信号槽机制的优势:信号槽机制很好地简化了程序的编写。

    在开发实践中,专业的TCP服务器通常不会选择Qt框架来实现,因为服务器程序往往不需要图形界面支持。

    HTTP 客户端

    Qt 只提供了HTTP客户端,而没有提供HTTP服务器的库

    核心 API
    1. QNetworkAccessManager
    • 作用:提供了 HTTP 的核心操作。

    方法

    说明

    get(const QNetworkRequest&)

    发起一个 HTTP GET 请求。返回 QNetworkReply 对象。

    post(const QNetworkRequest&, const QByteArray&)

    发起一个 HTTP POST 请求。返回 QNetworkReply 对象。

    2. QNetworkRequest
    • 作用:表示一个 HTTP 请求(不含 body)。

    • 说明:如果需要发送一个带有 body 的请求(比如 post),会在 QNetworkAccessManager 的 post 方法中通过单独的参数来传入 body。

    方法

    说明

    QNetworkRequest(const QUrl&)

    通过 URL 构造一个 HTTP 请求。

    setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value)

    设置请求头。

    QNetworkRequest::KnownHeaders 是一个枚举类型,常用取值:

    取值

    说明

    ContentTypeHeader

    描述 body 的类型。

    ContentLengthHeader

    描述 body 的长度。

    LocationHeader

    用于重定向报文中指定重定向地址。(响应中使用,请求用不到)

    CookieHeader

    设置 cookie

    UserAgentHeader

    设置 User-Agent

    3. QNetworkReply
    • 作用:表示一个 HTTP 响应。这个类同时也是 QIODevice 的子类。

    方法

    说明

    error()

    获取出错状态。

    errorString()

    获取出错原因的文本。

    readAll()

    读取响应 body。

    header(QNetworkRequest::KnownHeaders header)

    读取响应指定 header 的值。

    此外,QNetworkReply 还有一个重要的信号 finished,会在客户端收到完整的响应数据之后触发。

    结语:

           随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。    

             在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。

            你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容。

    Logo

    开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

    更多推荐