目录
描述
网络编程,显然任何一个开发工具都绕不开的话题
很遗憾的C++并没有对网络进行封装,使用在C++中对于网络编程是比较麻烦的,但是 Qt 对此进行了修正,有了自己的一套网络编程,所以我们在 Qt 中使用网络编程是比较方便的,Qt 中对操作系统提供的 API 封装成了一个类,我们直接使用就可以了,不过首先我们还得引用 network模块
模块设计理念
Qt 对很多功能都进行了模块化,网络编程也算如此
模块化处理有很多优点,可以减少生成无用代码
Qt 提供了两个版本的网络编程版本
相关函数
主要的类有两个. QUdpSocket 和 QNetworkDatagram
QUdpSocket 表⽰⼀个 UDP 的 socket ⽂件.
|-----------------------------------------|----|-------------------------------------|----------------------|
| 名称 | 类型 | 说明 | 对标原⽣ API |
| bind(const QHostAddress&, quint16) | ⽅法 | 绑定指定的端⼝号. | bind |
| receiveDatagram() | ⽅法 | 返回 QNetworkDatagram . 读取⼀个 UDP 数据报. | recvfrom |
| writeDatagram(const QNetworkDatagram&) | ⽅法 | 发送⼀个 UDP 数据报. | sendto |
| readyRead | 信号 | 在收到数据并准备就绪后触发. | ⽆ (类似于 IO 多路复⽤的通知机制) |
QNetworkDatagram 表⽰⼀个 UDP 数据报.
|---------------------------------------------------------------------|------|---------------------------------------------------------|---------------------|
| 名称 | 类型 | 说明 | 对标原⽣ API |
| QNetworkDatagram(const QByteArray&, const QHostAddress& ,quint16) | 构造函数 | 通过 QByteArray , ⽬标 IP 地址, ⽬标端⼝号 构造⼀个 UDP 数据报.通常⽤于发送数据时. | ⽆ |
| data() | ⽅法 | 获取数据报内部持有的数据.返 QByteArray | ⽆ |
| senderAddress() | ⽅法 | 获取数据报中包含的对端的IP地 址. | ⽆, recvfrom 包含了该功能. |
| senderPort() | ⽅法 | 获取数据报中包含的对端的端⼝号. | ⽆, recvfrom 包含了该功能. |
使用
回显服务器
实现一个带有界面的 UDP 回显服务器,虽然服务器一般不使用图形化界面
使用 Qt 的信号槽机制,可以很好解决堵塞问题
首先我们初始出界面出来
第一步,在.pro文件中引用 network
引入并创建一个 QUdpSocket 对象
先连接信号槽再绑定端口号
绑定失败要有提升,不要让程序继续下去了,这一点 Qt 帮我们封装了,使用 errorString 即可知道绑定失败的原因
读取请求并解析
把响应写回给客户端
服务器处理逻辑
因为还没有客户端,所以我们先得构建出客户端才能进行测试
回显客户端
客户端初始界面
准备工作
在同一个窗口中,我们可以创建多个项目,为了便于演示,这里我们创建在了一起
但是我们需要主要不要混淆了
这里加黑的是主要活动项目,当我们运行的时候,就只会运行该项目
当然了你也可以进行修改主要活动项目
同样的加上 network
初始化界面布局
首先我们创建这样的一个界面
垂直布局5,1
垂直策略
lineEdit
QPushButton
运行如下
定义两个常量
人为规定为两个字节
初始化窗口
给发送按钮添加槽函数
这里填写IP地址的时候需要注意转换
连接信号槽
处理服务器返回
测试
单个客户端运行
先后运行发送消息,正常
多个客户端运行
想要运行多个客户端,要去找exe可执行程序,在Qt内部无法做的
找到对应的build文件夹
debug文件夹
exe可执行程序
如下,测试成功
代码
.pro
加上network
服务器
widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
QUdpSocket* socket;
void processRequest();
QString process(const QString& request);
};
#endif // WIDGET_H
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);
// 创建出这个对象
socket = new QUdpSocket(this);
// 设置窗口标题
this->setWindowTitle("服务器");
// 连接信号槽
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
// 绑定端口号
bool ret = socket->bind(QHostAddress::Any, 9090);
if(!ret){
// 绑定失败
QMessageBox::critical(this, "服务器启动出错", socket->errorString());
return;
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::processRequest()
{
// 1.读取请求并解析
const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
QString request = requestDatagram.data();
// 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;
}
客户端
widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
QUdpSocket* socket;
void processResponse();
};
#endif // WIDGET_H
widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QNetworkDatagram>
// 定义两个常量,描述服务器的 地址 和 端口
const QString& SERVER_IP = "127.0.0.1"; //本地回环
const quint16 SERVER_PORT = 9090;
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);
// 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);
}
关于服务器和客户端能否部署在云服务器上的一些问题
小补充
在服务器中,我们看到的消息中会多了一些消息,这是 IPV6 的本地回环,在绑定端口号的时候,我们的服务器指定了全部的地址
关于引用的认知
这一点也是很常见的一类问题了
自动添加头文件
在一些开发工具中,是支持自动添加头文件的,但是显然C++是不能够的,这一点有些遗憾