目录
计算机网络可以参考: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 介绍
主要涉及的类是两个,QUdpSocket 和 QNetworkDatagram
QUdpSocket 表示一个 UDP 的 socket 文件,核心方法如下:
API | 类型 | 说明 | 对标原生 API |
---|---|---|---|
bind(const QHostAddress&, quint16) | 方法 | 绑定指定的端口号 | bind |
receiveDatagram() | 方法 | 返回 QNetworkDatagram,是一个 UDP 数据包对象 | recvfrom |
writeDatagram(const QNetworkDatagram&) | 方法 | 发送一个数据包 |
sendto |
readyRead | 信号 | 收到数据并准备就绪后触发该信号 |
|
QNetworkDatagram 表示一个 UDP 数据报,核心方法如下:
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
QNetworkDatagram(const QByteArray&, const QHostAddress&, quint16) | 构造函数 | 通过 QByteArray,添加目标IP、目标端口,构造一个 UDP 数据报,通常用于发送数据 | 无 |
data() | 方法 | 获取数据报内部持有的数据,返回 QByteArray | 无 |
senderAddress() | 方法 | 获取数据包中包含的对端的 IP 地址 | 无,recvfrom 包含了该功能 |
senderPort() | 方法 | 获取数据报中包含的对端的端口号 | 无,recvfrom 包含了该功能 |
2.2 回显服务器
关于回显服务器:
回显服务器是指用于测试和验证网络连接的服务器。它的主要功能是返回客户端发送的数据,以便客户端确认网络连接是否正常。回显服务器一般是一个简单的程序,它接收来自客户端的数据,并将该数据原样返回给客户端。回显服务器的工作原理如下:
- 当客户端向回显服务器发送数据时,服务器将接收到的数据存储起来,然后将它原样返回给客户端
- 客户端收到服务器返回的数据后,可以比对原始数据和返回数据是否一致,从而验证网络连接的准确性和稳定性
回显服务器常用于网络测试和诊断,可以帮助用户判断网络是否通畅。例如:
- 在网络故障排查时,可以使用回显服务器来测试网络连接是否正常
- 此外,回显服务器还可以用于测试服务器的性能和负载能力,帮助管理员监控网络性能
在实际应用中,回显服务器有多种实现方式:
- 一种常见的方式是使用简单的套接字编程来实现回显服务器,通过编写程序来进行数据的接收和返回
- 另一种方式是使用专门的回显服务器软件,这些软件提供了更多的功能和配置选项,能够更好地满足用户的需求
总之,回显服务器是一种用于测试和验证网络连接的服务器,它通过返回客户端发送的数据来确认网络连接的正常性,常用于网络测试、诊断以及性能监控。
我们先新建一个基于 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多出很多
主要涉及两个类,QTcpServer 和 QTcpSocket
QTcpServer 用于监听端口,和获取客户端连接,核心方法如下:
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
listen(const QHostAddress&, quint16 port) | 方法 | 绑定指定的地址和端口号,并开始监听 | bind 和 listen |
nextPendingConnection() | 方法 |
|
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 介绍
主要涉及的类有三个:QNetworkAccessManager,QNetworkRequest,QNetworkReply
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();
});
}