QT跨平台应用程序开发框架(11)—— Qt网络编程

发布于:2025-07-26 ⋅ 阅读:(10) ⋅ 点赞:(0)

目录

一,概述

二,UDP

2.1 API 介绍

2.2 回显服务器

2.3 回显客户端

2.4 演示

三,TCP

3.1 API 介绍

3.2 回显服务器

3.3 回显客户端

3.4 演示 

四,HTTP

4.1 API 介绍

4.2 http客户端


计算机网络可以参考:https://blog.csdn.net/aaqq800520/category_12705783.html

一,概述

支持网络的操作系统也都会一共一组 API(socket API)来供用户使用,但是 C++ 至今还没有提供一套封装了网络编程的 API,被业界很多人吐槽,所以后面我们将使用 Qt 自己封装的网络 API

  • 我们编写的网络程序,需要传输层支持,所以Qt 也就提供了两套 API,分别针对 UDP 和 TCP
  • 使用 Qt 的网络 API,需要先在 .pro 文件添加 network 模块,我们前面介绍的各种控件,都是包含在 QtCore 模块中的,因为这个模块默认添加

关于模块化,下面来简单概括一下:

  • Qt 是一个体量非常大的框架,如果直接把其所有的功能放到一起,那么搞一个程序就会包含大量没有用到的东西,造成浪费
  • 所以Qt 就把这些功能分成一个一个的模块,要用哪个就添加哪个即可,能极大节约成本 

下面直接开始实操 

二,UDP

2.1 API 介绍

主要涉及的类是两个,QUdpSocketQNetworkDatagram

QUdpSocket 表示一个 UDP 的 socket 文件,核心方法如下:

API 类型 说明 对标原生 API
bind(const QHostAddress&, quint16) 方法 绑定指定的端口号 bind
receiveDatagram() 方法 返回 QNetworkDatagram,是一个 UDP 数据包对象 recvfrom
writeDatagram(const QNetworkDatagram&) 方法

发送一个数据包

sendto
readyRead 信号

收到数据并准备就绪后触发该信号

  • 我们在 Linux 上是直接通过阻塞式地等待客户端发小
  • Qt 的这个类似 IO 的多路复用机制

QNetworkDatagram 表示一个 UDP 数据报,核心方法如下:

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

2.2 回显服务器

关于回显服务器:

回显服务器是指用于测试和验证网络连接的服务器。它的主要功能是返回客户端发送的数据,以便客户端确认网络连接是否正常。回显服务器一般是一个简单的程序,它接收来自客户端的数据,并将该数据原样返回给客户端。回显服务器的工作原理如下:

  • 当客户端向回显服务器发送数据时,服务器将接收到的数据存储起来,然后将它原样返回给客户端
  • 客户端收到服务器返回的数据后,可以比对原始数据和返回数据是否一致,从而验证网络连接的准确性和稳定性

回显服务器常用于网络测试和诊断,可以帮助用户判断网络是否通畅。例如:

  • 在网络故障排查时,可以使用回显服务器来测试网络连接是否正常
  • 此外,回显服务器还可以用于测试服务器的性能和负载能力,帮助管理员监控网络性能

在实际应用中,回显服务器有多种实现方式:

  • 一种常见的方式是使用简单的套接字编程来实现回显服务器,通过编写程序来进行数据的接收和返回
  • 另一种方式是使用专门的回显服务器软件,这些软件提供了更多的功能和配置选项,能够更好地满足用户的需求

总之,回显服务器是一种用于测试和验证网络连接的服务器,它通过返回客户端发送的数据来确认网络连接的正常性,常用于网络测试、诊断以及性能监控。

来源:回显服务器是什么 • Worktile社区

我们先新建一个基于 QWidget 的项目,名称改为 UdpServer,并创建一个 List Widget用来显示消息:

然后就是添加 Udp 头文件,但是前面说过,需要在 .pro 文件添加 network 才行,如下:

然后就是在 widget.h 文件里添加定义了:

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); //创建实例,并且也可以挂对象树上,所以用 this 构造
    this->setWindowTitle("服务器"); //设置窗口标题
    connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
    
    //我们需要先连接信号槽再绑定端口号,这就好比要开店,得先装修好,如果没装修好就开业,那就完了
    bool ret = socket->bind(QHostAddress::Any, 8080); //端口有效范围是1 ~ 65535,是十六位二进制最小和最大值
    if(!ret)
    {
        QMessageBox::critical(this, "服务器启动出错", socket->errorString());
        return;
    }
    
}

Widget::~Widget()
{
    delete ui;
}
//该函数负责服务器核心逻辑,包括:
//1,读取请求并解析
//2,根据请求计算响应
//3,将响应发回客户端
//其实就是那一套,基本没变,应该说绝大部分服务器都是这样的
void Widget::processRequest() 
{
    //1,读取解析请求
    const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
    QString request = requestDatagram.data(); //返回一个 QByteArray,可以用来赋值和构造 QString(优势)
    
    //2,根据请求计算响应(由于是回显服务器,所以不需要计算响应,就是请求本身)
    const QString& response = process(request);
    
    //3,把响应写回客户端
    QNetworkDatagram responseDatagram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort());
    //上面有三个参数,第一个表示取出 QS听 内部的字节数组;第二个表示目的IP,第三个表示目的端口
    socket->writeDatagram(responseDatagram); //和文件描述符一样,所以是 write
    QString log = "[" + requestDatagram.senderAddress().toString() + ":" + QString::number(requestDatagram.senderPort())
             + "] req: " + request + ", resp: " + response;
    ui->listWidget->addItem(log); //显示到界面上
}

QString Widget::process(const QString &request)
{
    //回显服务器,响应和请求一样
    return request;
}

2.3 回显客户端

我们另外搞一个项目,和上面一样,命名为 UdpClient,然后创建下列控件:

UdpClient 项目的 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,上一个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);
    //将QString类型的 ip 转为合适的类型

    //3,发送请求数据
    socket->writeDatagram(requestDatagram);

    //4,把发送的请求也添加到列表框中
    ui->listWidget->addItem("客户端说:" + text);

    //5,把输入框的内容也清空一下
    ui->lineEdit->setText("");
}

void Widget::processResponse() //这个函数来处理收到的响应
{
    //1,读取到响应数据
    const QNetworkDatagram& responseDatagram = socket->receiveDatagram();
    QString response = responseDatagram.data(); //能用引用尽量用引用,但是涉及到不同类型转换时,还是要用值

    //2,把响应数据显示到界面上
    ui->listWidget->addItem("服务器说:" + response);
}

2.4 演示

三,TCP

3.1 API 介绍

  • UDP:无连接,不可靠传输,面向数据包,全双工
  • TCP:有连接,可靠传输,面向字节流,全双工

所以,TCP的代码比UDP多出很多

主要涉及两个类,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 信号 有数据到达并准备就绪时触发 无,但是类似 IO 多路复用的通知机制
disconnected 信号 连接断开时触发 无,但是类似 IO 多路复用的通知机制

3.2 回显服务器

和 UDP 一样,兴建一个命名为 TcpServer 的项目,创建一个 ListWidget 控件用来显示 log

先在 por 文件里加上netowrk,然后在 .h 文件里声明函数等:

下面是 widget.cpp 的内容:

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

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); //表示愿意接收任何 ip,端口为8080
    if (!ret) 
    {
        QMessageBox::critical(this, "服务器启动失败!", tcpServer->errorString()); //弹出错误对话框
        exit(1);
    }
}

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

void Widget::processConnection()
{
    //1,通过 tcpServer 拿到一个 socket 对象, 用来和客户端进行通信.
    QTcpSocket* clientSocket = tcpServer->nextPendingConnection(); //每个客户端都有一个对象,所以可能有多个,所以当断开连接时必须要释放
    QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端成功连接";
    ui->listWidget->addItem(log);

    //2,处理客户端发来的请求
    connect(clientSocket, &QTcpSocket::readyRead, this, [=]() //使用 lamdba 表达式
    {
        QString request = clientSocket->readAll(); //读取请求内容,返回QByteArray,转成 QString
        const QString& response = process(request); //处理请求,构建响应,返回要发回的内容
        clientSocket->write(response.toUtf8()); //将响应发回客户端
        QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] "
                + " req: " + request + ", resp: " + response; 
        ui->listWidget->addItem(log); //打印日志
        //上面的处理办法比较简陋,因为一个完整的请求可能是分成多端字节组进行传输,简单来说就是可能每一次收到的内容都不完整(粘包问题)
        //但是作为回显服务器已经足够,更好的做法是将每次收到的数据包都放到一个大的缓冲区中,并提前约定好应用层协议的格式,再进行更细致的解析
        //该步骤在主页的 http服务器 项目里已经实现过,这里就从简了
    });

    //3,处理断开连接的情况.
    connect(clientSocket, &QTcpSocket::disconnected, this, [=]() {
        QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端断开连接";
        ui->listWidget->addItem(log); //打印断开连接日志
        //手动释放 clientSocket 
        // delete clientSocket; //直接使用 delete 不好,有很多限制,比如要保证 delete 是最后一步,并且不能被 return 或抛异常等操作跳过,
        clientSocket->deleteLater(); //Qt 提供的,不立马销毁,告诉 Qt 在下一轮事件循环再销毁
        //槽函数都是在事件循环执行的,进入到下一轮事件循环,表示上一轮循环中要做的事已经全部做完了,也表示槽函数已经结束了
    });
}

//回显服务器.
QString Widget::process(const QString request)
{
    return request;
}

3.3 回显客户端

我们再新建一个 QWidget 的项目,命名为 TcpClient,并创建和 UdpClient 一样的控件

先在 pro 文件里添加network,下面是 .h 的内容:

下面是 widget.cpp 的内容:

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

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

    //1,设置窗口标题
    this->setWindowTitle("客户端");
    //2,创建 socket 对象的实例
    socket = new QTcpSocket(this);
    //3,和服务器建立连接 (这个不是立马连接,因为有三次握手,此处只是发起连接请求,三次握手交给 tcp做的)
    socket->connectToHost("127.0.0.1", 8080);
    //4,连接信号槽, 处理响应
    connect(socket, &QTcpSocket::readyRead, this, [=]() {
        QString response = socket->readAll(); //读取响应内容
        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()
{
    const QString& text = ui->lineEdit->text(); //获取输入框内容
    socket->write(text.toUtf8()); //将内容发给服务器
    ui->listWidget->addItem("客户端说: " + text); //显示发送的内容
    ui->lineEdit->setText(""); //清空输入框
}

3.4 演示 

当服务器未启动直接启动客户端时,会弹出窗口:

四,HTTP

4.1 API 介绍

主要涉及的类有三个:QNetworkAccessManagerQNetworkRequestQNetworkReply

QNetworkAccessManager 提供了 Http 的两个核心操作,如下:

方法 说明
get(const QNetworkRequest&) 发起一个 HTTP GET 请求,返回 QNetworkReply 对象
post(const QNetworkRequest&, const QByteArray&) 发起一个 HTTP POST 请求,返回 QNetworkReply 对象

 QNetworkRequest 这个类表示一个 Http 请求报头,不含请求正文,方法如下:

方法 说明
QNetworkRequest(const QUrl&) 通过 URL 构造 HTTP 请求
setHeader(QNetworkRequest::KnownHeaders header, const QVariant& value) 设置请求头

QNetworkRequest::KnownHeaders 是一个描述请求正文的枚举类型,常用取值如下:

取值 说明
ContentTypeHeader 描述正文类型
ContentLengthHeader 描述正文长度
LocationHeader 用于重定向,响应报文中使用
CookieHeader 设置 cookie
UserAgentHeader 设置 User-Agent

QNetworkReply 表示一个 http 响应,常用方法如下:

方法 说明
error() 获取错误状态
errorString() 获取出错原因
readAll() 读取响应文本
header(QNetworkRequest::Known) 读取响应指定的header的值

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

4.2 http客户端

注意,Qt 只提供了 http客户端,而没有提供 http服务器的库,所以服务器直接用 www.baidu.com 或者其它网站即可

首先也是和上面的 Udp 和 Tcp 一样,新建一个项目,添加相同控件,头文件如下: 

下面是 widget.cpp 的内容

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

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

    this->setWindowTitle("客户端");
    manager = new QNetworkAccessManager(this);
}

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

void Widget::on_pushButton_clicked()
{
    //1,获取到输入框中的 url
    QUrl url(ui->lineEdit->text());

    //2,构造一个 HTTP 请求对象
    QNetworkRequest request(url);

    //3,发送请求
    QNetworkReply* response = manager->get(request);

    //4,通过信号槽, 来处理响应
    connect(response, &QNetworkReply::finished, this, [=]() {
        if (response->error() == QNetworkReply::NoError) // 正确获取响应
        {
            QString html = response->readAll();
            ui->plainTextEdit->setPlainText(html); //不用 QTextEdit,因为其会对 html 进行解析,就不知原始的 html 代码了
        }
        else ui->plainTextEdit->setPlainText(response->errorString());  //响应错误

        // 释放response
        response->deleteLater();
    });
}


网站公告

今日签到

点亮在社区的每一天
去签到