目录
前言:
一、QUdpSocket
(一)核心 API 概览
(二)设计一个UDP回显服务器
二、QTCPSocket
(一)核心 API 概览
(二)设计一个TCP回显服务器
三、HTTP Client
四、Qt音视频
(一)Qt 音频
(二)Qt 视频
前言:
Qt 为了支持跨平台,对网络编程的API也重新封装了。 网络编程其实编写的是应用层代码,但是需要传输层的支持,传输层的核心协议有UDP和TCP,Qt 也提供了两套API,分别是QUdpSocket和QTcpSocket。实际 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.手动释放 clientSocket
clientSocket->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协议实现的,也就是封装了TcpSocket。Qt 只是提供了 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 的类型 |
ContentLengthHeader | ContentLengthHeader |
ContentLengthHeader | ⽤于重定向报⽂中指定重定向地址(响应中使⽤,请求⽤不到) |
CookieHeader | CookieHeader |
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. 获取到输入框中的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);
}
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_OBJECT
private 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;
}