目录
前言
我们之前学习的系统相关的内容,在不同的系统下的理论和实现可能都不太一样,但是对于计算机网络模块来说,因为网络提供了不同主机之间的信息通信,那么如何想让两个主机进行通信,那么他们的通信模块,也就是计算机模块的实现也就必须是一样的才可以。所以说计算机网络模块的实现对于不同的系统也是一样的,最主要的区别就是在于语言层面上,对于网络编程的系统接口的封装。对于C++标准库中,一直是没有提供网络编程相关的API库。
在整个网络协议栈来看,网络编程就是我们对于用户层的一种编写,但是我们想要传输数据的时候需要传输层协议的支持,但是传输层协议有两个,所以我们在应用层也需要采用一些接口去指定传输层协议。
一、UDP网络编程
1.Qt项目文件
.pro文件是 Qt 项目文件。它是一个文本文件,包含了项目的各种信息和配置,用于指导qmake如何构建 Qt 项目。所以在Qt中进行网络编程之前,需要在项目当中的.pro文件中添加network模块来引入网络编程模块。在我们之前使用的所有控件都是包含在QtCore模块当中的,只不过默认生成.pro文件的时候就已经添加了。而且Qt来提供了很多其他的模块。
Qt为什么要划分出这么多模块呢?因为Qt本身是一个非常大的框架,如果我们默认把所有Qt提高的功能模块全都一下引入项目当中,那么即使我们写一个简单的打印helloworld,那么生成的可执行程序也会 非常的大,可执行程序内部编译了许多用不到的内容。所以为了让生成的Qt项目更加的轻量化,那么就把不同的功能划分成了多个模块,我们需要使用的时候,就需要在.pro文件中引入包含了。
而且为了让多模块的项目更加的轻量化,Qt也提高了模块的动态库和静态库的两种版本。
2.UDP类
Qt对于UDP的类有两个,一个是QUdpSocket表示udp通信套接字文件类,另一个是表示一个UDP数据报的类QNetworkDatagram类。
QUdpSocket
|-----------------------------------------|---------------|
| 名称 | 说明 |
| bind(const QHostAddress&, quint16) | 绑定指定的端口号 |
| receiveDatagram() | 返回一个UDP数据报对象 |
| writeDatagram(const QNetworkDatagram&) | 发送一个UDP数据报 |
| readyRead信号 | 在收到数据并准备就绪后触发 |
- 对于读取数据的操作,在C++语言层面的接口或者系统的底层原生接口默认都是阻塞方式的等待,当然也可以设置非阻塞,而Qt在处理读取数据操作的时候,并不是阻塞或者非阻塞等待的方式,而是采用信号槽机制,当有数据到来并就绪只会,就会发送readRead信号。
- QHostAddress是一个用于表示IP地址的一个类,对于服务器来说设置为QHostAddress::Any即可,表示可以用于监听任何地址的连接,对于IPv4和IPv6都适用。
- 对于bind函数可能会绑定失败,但是他会将失败的原因存放起来,可以通过errorString成员函数获取到失败信息。
QNetworkDatagram
|--------------------------------------------------------------------|--------------------------------------|
| 名称 | 说明 |
| QNetworkDatagram(const QByteArray&, const HostAddress&, quint16) | 构造函数,通过QByteArray,目标ip和端口号构造一个UDP数据报 |
| data() | 获取数据报内部持有的数据,返回一个QByteArray类型对象 |
| senderAddress() | 获取数据报中包含的对端ip地址 |
| senderPort() | 获取数据报中包含的对端端口号 |
3.UDP回显服务器案例
只是一个使用网络接口的案例,因为一般来说服务器都不会带有图形化界面的。
细节
- 在readyRead信号绑定槽函数和bind操作的顺序来说,要先绑定信号槽,后bind,因为一旦绑定ip和端口号之后,就可能会有通信数据到来了。那么在进行信号槽的绑定,就会来不及了,早到的数据就因为没有槽函数,所以不进行处理了。
- 我们下面的服务器设计中带有了图形化界面,在配置文件.pro文件中也可以看到我们引入了gui模块,那么我们的云服务器默认是没有装配图形化界面的,所以是没有办法直接将我们下面写的服务器端代码放入到云服务器允许的。
- 而对于我们现在写的Qt的udp客户端是可以直接连接linux服务器下的udp服务器的。所以说网络底层协议的实现都是一样的。udp都是一套逻辑。一般Qt都不会写服务器,大多数都是写客户端,然后用Qt客户端连接linux服务器。
服务器设计
widget.h文件
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
QString porcess(const QString& request);
public slots:
void processRequest();
private:
Ui::Widget *ui;
//引入Udp成员
QUdpSocket* socket;
};
widget.cpp文件
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);
//创建udp对象
socket = new QUdpSocket(this);
//设置窗口标题
this->setWindowTitle("服务器");
//连接信号槽
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
//绑定端口号
bool retbind = socket->bind(QHostAddress::Any, 8082);
if(retbind == false)
{
QMessageBox::critical(this, "服务器启动出错", socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
//但是我们这里是回显,所以没有什么处理过程
QString Widget::porcess(const QString &request)
{
return request;
}
void Widget::processRequest()
{
//获取请求信息
const QNetworkDatagram& udp_request = socket->receiveDatagram();
//转化为字符串类型
QString data_request = udp_request.data();
//处理请求
const QString& data = porcess(data_request);
//构建相应数据报
QNetworkDatagram response(data.toUtf8(), udp_request.senderAddress(), udp_request.senderPort());
//发送回客户端
socket->writeDatagram(response);
//把客户端发的信息显示到listWidget控件上
QString log = "[" + udp_request.senderAddress().toString() + ":" + QString::number(udp_request.senderPort())
+ "]" + "message:" + data_request;
ui->listWidget->addItem(log);
}
客户端设计
widget.h文件
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
//引入udp对象
QUdpSocket* socket;
};
widget.cpp文件
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QNetworkDatagram>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//设置窗口名称
this->setWindowTitle("客户端");
//创建udp对象
socket = new QUdpSocket(this);
//连接信号槽
connect(socket, &QUdpSocket::readyRead, this, [=](){
//获取数据报
const QNetworkDatagram udp_response = socket->receiveDatagram();
//读取数据
const QString& data = udp_response.data();
//写入对话框中
ui->listWidget->addItem("服务器:" + data);
});
}
Widget::~Widget()
{
delete ui;
}
//发送按钮
void Widget::on_pushButton_clicked()
{
//获取输入框的内容
const QString& text = ui->lineEdit->text();
//构建请求数据报
const QString ip = "127.0.0.1";
const quint16 port = 8082;
QNetworkDatagram data_request(text.toUtf8(), QHostAddress(ip), port);
//发送数据
socket->writeDatagram(data_request);
//更新对话框中的内容
ui->listWidget->addItem("客户端:" + text);
//情况输入框
ui->lineEdit->clear();
}
二、TCP网络编程
1.TCP类
Qt对于TCP的类提供两个,第一个是QTcpServer用于监听端口,实现获取客户端连接的操作,相当于是listensocket以及对于bind、listen以及accept接口函数的一个封装。第二个是用于客户端和服务器之间数据交互的类QTcpSocket类。
QTcpServer
|--------------------------------------------|-----------------------------------------------------------|
| 名称 | 说明 |
| listen(const QHostAddress&, quint16 port) | 绑定ip和端口号,并开始监听,相当于bind和listen的结合 |
| nextPendingConnection() | 从系统当中获取到一个已经建立好的tcp连接,返回一个QTcpSocket对象,通过这个对象是实现与客户端之间的通信 |
| newConnection信号 | 有新的客户端建立连接完毕之后触发的信号 |
QTcpSocket
|--------------------------------------------------------|--------------------------------------|
| 名称 | 说明 |
| readAll() | 读取当前接收缓冲区中的所有数据,返回QByteArray对象 |
| write(const QByteArray&) | 把输入写入到socket的发送缓冲区中 |
| deleteLater() | 暂时把socket对象标记为无效,Qt会在下个事件循环中析构释放该对象、 |
| peerAddress() | 获取对端ip地址 |
| peerPort() | 获取对端端口号 |
| connectToHost( const QString &hostName, quint16 port) | 向服务器发起连接 |
| readyRead信号 | 有数据到来并准备就绪触发 |
| disconnected信号 | 连接断开的时候触发 |
2.TCP回显服务器案例
细节
- 对于客户端向服务器发起连接的connectToHost函数返回值为void类型,但是我们还是要判断是否连接成功了,所以QTcpSocket类中提供了waitForConnected函数,该函数的作用是让当前线程处理等待状态,直到连接成功,或者连接反馈错误为止,又或者说超时。
服务器设计
widget.h文件
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void processConnection();
private:
Ui::Widget *ui;
//引入Tcp对象
QTcpServer* socket;
};
widget.cpp文件
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("服务器");
//tcp对象实例化
socket = new QTcpServer(this);
//绑定newConnection的信号槽函数
connect(socket, &QTcpServer::newConnection, this, &Widget::processConnection);
//进行bind和listen操作
bool retlisten = socket->listen(QHostAddress::Any, 8082);
if(retlisten == false)
{
QMessageBox::critical(this, "服务器启动失败", socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
//获取新连接的信号处理函数
void Widget::processConnection()
{
//获取新连接
QTcpSocket* client = socket->nextPendingConnection();
//构建日志
QString log = "[" + client->peerAddress().toString() + ":" + QString::number(client->peerPort()) + "] 新客户端上线";
ui->listWidget->addItem(log);
//通过信号槽触发接收消息
connect(client, &QTcpSocket::readyRead, this, [=](){
//读取消息
QString message = client->readAll();
//处理消息--此处没有处理,只是回显
//写回到客户端
client->write(message.toUtf8());
//构建日志
QString log = "[" + client->peerAddress().toString() + ":" + QString::number(client->peerPort()) + "] say: " + message;
//写到界面
ui->listWidget->addItem(log);
});
//处理断开连接的信号槽函数
connect(client, &QTcpSocket::disconnected, this, [=](){
//构建日志
QString log = "[" + client->peerAddress().toString() + ":" + QString::number(client->peerPort()) + "] 客户端下线";
//释放对象
client->deleteLater();
});
}
客户端设计
widget.h文件
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
//构建tcp对象
QTcpSocket* socket;
};
widget.cpp文件
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("客户端 ");
//实例化tcp对象
socket = new QTcpSocket(this);
//向服务器发起连接
socket->connectToHost("127.0.0.1", 8082);
//确认是否连接成功
if(!socket->waitForConnected())
{
QMessageBox::critical(this, "连接服务器出错", socket->errorString());
return;
}
//绑定接收到消息的信号槽函数
connect(socket, &QTcpSocket::readyRead, this, [=](){
//获取接收到的内容
QString message = socket->readAll();
//将内容打印到对话框中
QString log = "服务器: " + message;
ui->listWidget->addItem(log);
});
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
//获取输入框的内容
QString message = ui->lineEdit->text();
//将内容发送给服务器
socket->write(message.toUtf8());
//将内容打印到对话框中
ui->listWidget->addItem("客户端: " + message);
//情况输入框
ui->lineEdit->clear();
}
三、HTTP客户端
1.HTTP客户端类
Qt中为HTTP协议的客户端主要提供了三个类,一个是QNetworkAccessManager类,该类提供了HTTP有关的核心操作。第二个类是QNetworkRequest类表示一个不含body的HTTP请求。第三个类就是QNetworkReply类,表示一个HTTP的响应。
上述也说了真正的服务器一定也不会使用Qt就实现,所以Qt就没有提供服务端相关的HTTP类。
QNetworkAccessManager
|---------------------------------------------------|--------------------------------------|
| 方法 | 说明 |
| get(const QNetworkRequest&) | 发起一个HTTP GET请求,返回一个QNetworkReply对象 |
| post(const QNetworkRequest&, const QByteArray&) | 发起一个HTTP POST请求,也返回一个QNetworkReply对象 |
QNetworkRequest
一个HTTP协议的完整报文应该是包括报头和有效载荷的,而这个类只是实现了一个报头,对于有效载荷该类不进行实现,而且对于请求报文来说一般都不会带有一些实质性的内容,只会传递一些kv形式的参数内容,通过post方法来传递,所以post方法也为我们提供了传递kv的参数。
|-------------------------------------------------------------------------|-----------------|
| 方法 | 说明 |
| QNetworkRequest(const QUrl&) | 通过Url构造一个HTTP请求 |
| setHeader(QNetworkRequest::knownHeaders header, const QVariant& value) | 设置请求头部字段 |
对于报头的请求行字段,Qt也进行了一定的封装,方便我们去设置请求行,Qt中采用的是枚举类型将常用的请求行字段进行一一列举出来,QNetworkRequest::knownHeaders就是该枚举对象。
对于QVariant类对象表示一个类型可变的值。
QNetworkReply
|----------------------------------------------|---------------------|
| 方法 | 说明 |
| error() | 获取出错状态 |
| errorString() | 获取出错原因的字符串描述 |
| readAll() | 获取响应的body字段 |
| header(QNetworkRequest::knownHeaders header) | 获取响应头部字段 |
| finished信号 | 当客户端收到一个完整的响应报文只会触发 |
2.HTTP客户端案例
细节
- 如何想要显示出来元素的HTML报文,那么就不可以使用QTextEdit控件,因为他会对HTML代码进行渲染,显示出来的就不是原始的HTML报文了,所以需要使用QPlainTextEidt控件。
- 对于get、post等系列函数只是负责发送请求,不负责请求的等待,所以说他返回的响应对象是一个没有实际内容的对象,那什么时候才收到响应呢?依靠的是finished信号,通过信号槽机制去处理finished信号,也就相当于处理响应了。
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QNetworkReply>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//实例化
manager = new QNetworkAccessManager(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
//获取到输入框中的url,并构建一个url对象
QUrl url(ui->lineEdit->text());
//构造一个http请求对象
QNetworkRequest request(url);
//发送get请求,返回一个响应
QNetworkReply* response = manager->get(request);
//信号槽
connect(response, &QNetworkReply::finished, this, [=](){
//正确响应了
if(response->error() == QNetworkReply::NoError)
{
QString html_data = response->readAll();
ui->plainTextEdit->setPlainText(html_data);
}
else
{
ui->plainTextEdit->setPlainText(response->errorString());
}
//释放response对象
response->deleteLater();
});
}