<Qt> 系统 - 网络编程 | 音视频

ops/2024/10/15 20:17:02/

目录

前言:

一、QUdpSocket

(一)核心 API 概览

 (二)设计一个UDP回显服务器

二、QTCPSocket

(一)核心 API 概览

(二)设计一个TCP回显服务器

三、HTTP Client

四、Qt音视频

(一)Qt 音频

(二)Qt 视频


前言:

Qt 为了支持跨平台,对网络编程的API也重新封装了。 网络编程其实编写的是应用层代码,但是需要传输层的支持,传输层的核心协议有UDP和TCP,Qt 也提供了两套API,分别是QUdpSocketQTcpSocket。实际 Qt 开发中进行网络编程,也不一定使用 Qt 封装的网络 API,也有一定可能使用的是系统原生 API 或者其他第三方框架的 API。 

 还有一点要注意的是,要想实现网络编程,还要在.pro文件中添加network模块。我们之前提到过的各种控件都包含在QtCore模块中,为了不让可执行程序变得过于庞大,导致一些性能不够好的机器承受太大的压力,所以就进行了模块化的处理,默认情况下额外的模块不会参与编译,有需要就在.pro文件中添加:

一、QUdpSocket

(一)核心 API 概览

主要的类有两个:QUdpSocket 和 QNetworkDatagram

QUdpSocket 表示一个 UDP 的 socket 文件

名称类型说明
bind(const QHostAddress&,quint16)方法绑定指定的端⼝号
receiveDatagram()方法返回 QNetworkDatagram,读取⼀个 UDP 数据报.
writeDatagram(const QNetworkDatagram&)方法发送⼀个 UDP 数据报
readyRead信号在收到数据并准备就绪后触发

QNetworkDatagram 表示一个 UDP 数据报:

名称类型说明
QNetworkDatagram(const QByteArray&, const QHostAddress& , quint16 )构造函数通过 QByteArray,⽬标 IP 地址,⽬标端⼝号 构造⼀个 UDP 数据报,通常⽤于发送数据时
data()方法获取数据报内部持有的数据,返回QByteArray
senderAddress()方法获取数据报中包含的对端的 IP 地址
senderPort()方法获取数据报中包含的对端的端⼝号

 (二)设计一个UDP回显服务器

代码示例:设计一个UDP回显服务器

在ui界面中设置一个 QListWidget 来显示客户端消息日志:

在写代码之前一定要在.pro文件中添加network模块。也记得要在头文件声明成员和成员函数(这里就不显示出来了)。

写一个服务器首先就要有一个Socket对象,之后就要连接信号和槽,捕捉readyRead信号,对应的槽函数就要完成服务器的核心逻辑,之后就是bind端口号,一个Udp服务器就做好了。 

#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->setWindowTitle("服务器");// 连接信号槽connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);// 绑定端口号bool ret = socket->bind(QHostAddress::Any, 8080);if(!ret){QMessageBox::critical(nullptr, "服务器启动出错", socket->errorString());return;}
}Widget::~Widget()
{delete ui;
}

完成处理请求的过程:

  • 读取请求并解析
  • 根据请求计算响应
  • 把响应写回到客户端
// 完成处理请求的过程
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());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 &request)
{// 由于当前是回显服务器,响应和请求完全一样return request;
}

Udp使用的是数据报的形式,所以接收要接收一个数据报对象,这个数据报中有对端发来的数据和其他属性字段。给客户端进行响应的时候,也要响应一个数据报,构建一个数据报对象,再填充数据,使用toUtf8就可以把QString转换成QByteArray。最后再显示到服务器的QListWidget中就可以了。


下面就是客户端的界面了,给客户端设计一个界面。有一个回显框、输入框和发送按钮,再使用布局管理器修饰一下,调整一下垂直布局管理器,让下面的发送栏宽一点:

没有变宽就是因为没有调整下面两个控件的sizePolicy,都设置成Expanding就可以了:

我们想要实现的功能是现在输入框输入内容,点击发送按钮发送给服务端,所以先写一个按钮的槽函数:

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

现在客户端就有了发送的能力,接下来就要写接收服务端数据的代码了:

#include "widget.h"
#include "ui_widget.h"
#include <QNetworkDatagram>const QString &SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 8080;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::processReponse);
}Widget::~Widget()
{delete ui;
}void Widget::processReponse()
{// 通过这个槽函数处理收到的响应// 读取响应数据const QNetworkDatagram& responseDatagram = socket->receiveDatagram();const QString& response = responseDatagram.data();// 把响应数据显示到界面中ui->listWidget->addItem(response);
}

端口到本质上是一个 2 字节的无符号整数。

quint16:本质上就是一个 unsigned short(虽然 short 通常都是 2 个字节,但是 C++ 标准中没有明确规定这一点,只是说 short 不应该少于 2 个字节)。

最终执行效果:

客户端服务器测试的基本原则:一定是先启动服务器,后启动客户端。

启动多个客户端都可以正常工作,但是不能在界面选择直接运行,否则会覆盖上一个客户端:

二、QTCPSocket

(一)核心 API 概览

核心类是两个:QTcpServer 和 QTcpSocket

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

名称类型说明对标原生API
listen(const QHostAddress&, quint16 port)方法绑定指定的地址和端口号并开始监听bind和listen
nextPendingConnection()方法

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

返回一个TcpSocket,表示这个连接

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

accept
newConnection信号有新的客户端建立好连接后触发无(类似与IO多路复用中的通知机制)

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

API类型说明对标原生API
readAll()方法读取当前接收缓冲区中的所有数据.返回 QByteArray 对象read
write(const QByteArray& )方法把数据写⼊ socket 中write
deleteLater方法暂时把 socket 对象标记为⽆效,Qt会在下个事件循环中析构释放该对象无(但是类似于"半自动化的垃圾回收)
readyRead信号有数据到达并准备就绪时触发无(但是类似与 IO 多路复用中的通知机制)
disconnected信号连接断开时触发无(但是类似与 IO 多路复用中的通知机制)

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

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

(二)设计一个TCP回显服务器

代码示例:设计一个UDP回显服务器

在ui界面中设置一个 QListWidget 来显示客户端消息日志:

在写代码之前一定要在.pro文件中添加network模块。也记得要在头文件声明成员和成员函数(这里就不显示出来了)。

客户端和服务端的界面都是不变的,变得是这是一个TCP服务器,除了bind还需要设置成监听状态,使用listen方法就可以完成,只要有新的连接就会触发newConnection信号。

#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(nullptr, "服务器启动失败", tcpServer->errorString());exit(1);}
}Widget::~Widget()
{delete ui;
}

接下来就是设置好listen状态后,触发了newConnection信号之后执行processConnection的操作:

void Widget::processConnection()
{// 1. 通过tcpServer拿到一个socket对象,通过这个对象来和客户端进行通信QTcpSocket *clientSocket = tcpServer->nextPendingConnection();// peerAddress 表示对端的IP地址QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线";ui->listWidget->addItem(log);// 2. 通过信号槽,来处理客户端发来的请求connect(clientSocket, &QTcpSocket::readyRead, this, [=](){// a.读取请求QString request = clientSocket->readAll();// b.根据请求处理响应const QString &response = process(request);// c. 把响应写回客户端clientSocket->write(response.toUtf8());// d. 把上述信息记录到日志中QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort())+ "] req: " + request + ", resp: " + response;ui->listWidget->addItem(log);});// 3. 通过信号槽,处理客户端断开连接的情况connect(clientSocket, &QTcpSocket::disconnected, this, [=](){// a.把断开连接的信息通过日志显示出来QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort())  + "] 客户端下线";ui->listWidget->addItem(log);// b.手动释放 clientSocketclientSocket->deleteLater();});
}QString Widget::process(const QString request)
{// 因为这里写的是回显服务器,所以请求和响应完全一样return request;
}

上述代码其实不够严谨,但在这里作为回显服务器已经够了。实际在使用 TCP 的过程中,TCP 是面向字节流的,一个完整的请求可能会分成多段字节数组进行传输。虽然 TCP 已经帮我们处理了很多棘手的问题,但是 TCP 本身并不负责区分从哪里到哪里是一个完整的应用层数据(粘包问题)。更严谨的做法:每次收到的数据都给它放到一个字节数组缓冲区中,并且提前约定好应用层协议的格式(分隔符 / 长度 / 其他办法),再按照协议格式对缓冲区数据进行更细致的解析处理。


下面就是客户端的界面了,同UDP界面一致:

下面就是客户端的代码了,除了要维护连接,编写上和Udp客户端没有太大的差别:

#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. 和服务器建立连接// 调用这个函数,此时系统内核就会和对方的服务器之间进行三次握手(需要销毁一定时间)// 此处这个函数不会阻塞等待三次握手完毕socket->connectToHost("127.0.0.1", 8080);// 4. 链接信号槽,处理响应connect(socket, &QTcpSocket::readyRead, this, [=](){// a. 读取相应内容QString response = socket->readAll();// b. 把相应内容显示到界面上ui->listWidget->addItem(QString("服务器说: ") + response);});// 5. 等待连接确立的结果,并确认是否连接成功bool ret = socket->waitForConnected();if(!ret){QMessageBox::critical(nullptr, "连接服务器出错!", 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(QString("客户端说: ") + text);// 4. 清空输入框的内容ui->lineEdit->setText("");
}

先启动服务器,再启动客户端(可以启动多个),最终执行效果:

由于我们使用信号槽处理同一个客户端的多个请求,不涉及到循环,也就不会使客户端之间相互影响了:

三、HTTP Client

进行 Qt 开发时,和服务器之间的通信很多时候也会用到 HTTP 协议。

  • 通过 HTTP 从服务器获取数据
  • 通过 HTTP 向服务器提交数据

TTP相比TCP/UDP还要使用的更多一点,而HTTP协议本质上是基于TCP协议实现的,也就是封装了TcpSocketQt 只是提供了 HTTP客户端,并没有提供服务端

下面是核心API,三个类,分别是QNetworkAccessManager,QNetworkRequest,QNetworkReply

 QNetworkAccessManager 提供了HTTP的核心操作:

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

QNetworkRequest 表示一个 HTTP 请求(不包含请求正文 body),想要发送一个带有body的请求需要再QNetworkAccessManager的post方法中的参数传入body:

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

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

取值说明
ContentTypeHeader描述 body 的类型
ContentLengthHeaderContentLengthHeader
ContentLengthHeader⽤于重定向报⽂中指定重定向地址(响应中使⽤,请求⽤不到)
CookieHeaderCookieHeader
UserAgentHeader设置 User-Agent

QNetworkReply 表示一个 HTTP响应,这个类同时也是 QIODevice 的子类。QNetworkReply 还有一个重要的信号 finishied,在客户端收到完整的响应数据后触发:

常用方法说明
error()获取出错状态
errorString()获取出错原因的⽂本
readAll()读取响应 body
header(QNetworkRequest::KnownHeaders header)读取响应指定 header 的值

下面就来写一个HTTP客户端,使用的界面与上面的差不多,通过指定一个Url发送请求,响应的结构大概率是一个 HTML,这里使用的是 QPlainTextEdit 来表示:

注意:此处建议使用 QPlainTextEdit,而不是 QTextEdit。主要是因为 QTextEdit 要进行富文本解析,最终显示的结果就不是原始的 HTML 了,如果得到的 HTTP 响应体积很大,会导致界面渲染缓慢甚至被卡住。


在写代码之前一定要在.pro文件中添加network模块。也记得要在头文件声明成员和成员函数(这里就不显示出来了)。

在构造函数中设置一下标题,并new一个 QNetworkAccessManager 对象,之后就可以写槽函数了: 

#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. 获取到输入框中的urlQUrl 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);}else{// 响应出错了ui->plainTextEdit->setPlainText(response->errorString());}// 还需要对response进行释放response->deleteLater();});
}

运行效果如下,输入一个Url就会返回一个html格式的文本: 

发送 POST 请求代码也是类似,使用 manager->post() 即可。

实际开发中,HTTP Client 获取到的的数据也不一定非得是 HTML,更大的可能性是客户端开发和服务器开发约定好交互的数据格式。按照约定的格式,客户端拿到之后进行解析,并显示到界面上。

四、Qt音视频

(一)Qt 音频

在 Qt 中,音频主要通过 QSound 类来实现。但是需要注意的是 QSound 类只支持播放 wav 格式的音频文件。在这之前也需要先引入 multimedia 模块,最核心的API就是play方法,用来播放音频。

在界面中添加一个按钮,命名为播放,当我们点击按钮,就会播放音乐。首先要有一个wav后缀的文件,像这种文件还是使用qrc来保存。

class MainWindow : public QMainWindow
{Q_OBJECTprivate slots:void on_pushButton_clicked();private:QSound* sound;
};MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);sound = new QSound(":/music/zjl_qingtian.wav", this);
}void MainWindow::on_pushButton_clicked()
{// 在这里进行音频播放sound->play();
}

(二)Qt 视频

在 Qt 中,视频播放的功能主要是通过 QMediaPlayer类 和 QVideoWidget类 来实现。在使用这两个类时要添加对应的模块:multimedia multimediawidgets。它也有核心的API:

方法说明
setMedia()设置当前媒体源。
setVideoOutput()

将 QVideoWidget 视频输出附加到媒体播放器。

如果媒体播放器已经附加了视频输出,将更换一个新的。

class Widget : public QWidget
{Q_OBJECT
public:// ...
private:Ui::Widget *ui;QMediaPlayer *mediaPlayer; // 播放声音QVideoWidget *videoWidget; // 显示视频//创建两个按钮:选择视频按钮和播放按钮QPushButton *chooseBtn, *playBtn;
};

 接下来就是设置视频播放窗口的代码:

#include "widget.h"
#include "ui_widget.h"#include <QMediaPlayer>
#include <QSlider>Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 对象实例化mediaPlayer = new QMediaPlayer(this);videoWidget = new QVideoWidget(this);// 设置播放窗口videoWidget->setMinimumSize(600, 600);// 垂直布局QVBoxLayout *vbox = new QVBoxLayout();this->setLayout(vbox);// 实例化按钮chooseBtn = new QPushButton("选择视频", this);playBtn = new QPushButton(this);// 设置图标playBtn->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));// 创建水平布局QHBoxLayout* hbox = new QHBoxLayout();hbox->addWidget(chooseBtn);hbox->addWidget(playBtn);// 添加到垂直布局管理器中vbox->addWidget(videoWidget);vbox->addLayout(hbox);connect(chooseBtn, &QPushButton::clicked, this, [=](){// 选择视频,返回视频的路径QString url = QFileDialog::getOpenFileName(this, "选择视频");// 设置声音mediaPlayer->setMedia(QUrl(url));// 输出画面mediaPlayer->setVideoOutput(videoWidget);// 播放mediaPlayer->play();});
}Widget::~Widget()
{delete ui;
}

http://www.ppmy.cn/ops/94987.html

相关文章

智能视界:一文掌握Transformer视频分类核心技术

视频分类&#xff1a;技术与实践 概述 视频分类是计算机视觉领域的一项基本任务&#xff0c;目标是从连续的图像序列中识别出视频的主题或内容类别。与图像分类不同&#xff0c;视频分类不仅要理解单帧画面&#xff0c;还需捕捉时间序列中的动态信息&#xff0c;这对于自动标…

关于SOA和微服务

面向服务的架构&#xff08;SOA&#xff09; 想象一下&#xff0c;你正在经营一家大型超市&#xff0c;超市里有各种各样的商品和服务。SOA 就像是超市的各个部门&#xff0c;比如生鲜区、家电区、收银台等等&#xff0c;每个部门提供特定的服务。这些服务&#xff08;部门&am…

在亚马逊云科技上利用生成式AI开发用户广告营销平台

项目简介&#xff1a; 小李哥将继续每天介绍一个基于亚马逊云科技AWS云计算平台的全球前沿AI技术解决方案&#xff0c;帮助大家快速了解国际上最热门的云计算平台亚马逊云科技AWS AI最佳实践&#xff0c;并应用到自己的日常工作里。 本次介绍的是如何利用亚马逊云科技大模型托…

Spring Boot集成sentinel快速入门Demo

1.什么是sentinel&#xff1f; 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件&#xff0c;主要以流量为切入点&#xff0c;从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、…

WebRTC音视频开发读书笔记(三)

当采集音频或视频时&#xff0c;设备会源源不断地产生媒体数据&#xff0c;这些数据就是媒体流&#xff0c;从Canvas&#xff0c;桌面&#xff0c;摄像头捕获的流为视频流&#xff0c;从麦克风捕获的的流称为音频流&#xff0c;媒体流中混入的可能是多种数据 &#xff0c;因此W…

IOS 06 OC调用Swift第三方框架

前面文章05讲的是在OC项目中&#xff0c;调用Swift代码&#xff0c;而在真实开发过程中&#xff0c;在OC项目中调用Swift第三方框架场景用的是非常多的&#xff0c;所以我们也了解在OC项目如何使用Swift写的三方框架。 实现流程&#xff1a; 1、OCUseSwiftTest&#xff1b;在…

力扣 3152. 特殊数字Ⅱ

题目描述 queries二维数组是nums数组待判断的索引区间&#xff08;左闭右闭&#xff09;。需要判断每个索引区间中的nums相邻元素奇偶性是否不同&#xff0c;如果都不同则该索引区间的搜索结果为True&#xff0c;否则为False。 暴力推演&#xff1a;也是我最开始的思路 遍历q…

景联文科技:图像标注的类型有哪些?

图像标注是计算机视觉领域中一个非常重要的步骤&#xff0c;它是创建训练数据集的关键组成部分&#xff0c;主要用于帮助机器学习算法理解图像内容。 以下是图像标注的一些主要类型&#xff1a; 1. 边界框标注&#xff1a; • 这是最常见的标注方式之一&#xff0c;通常用于…