Qt----网络编程

网络编程

Qt 网络模块为我们提供了编写 TCP / IP 客户端和服务器的类。它提供了较低级别的类,例如代表低级网络概念的 QTcpSocket,QTcpServer 和 QUdpSocket,以及诸如 QNetworkRequest,QNetworkReply 和 QNetworkAccessManager 之类的高级类来执行使用通用协议的网络操作。 它还提供了诸如QNetworkConfiguration QNetworkConfigurationManager和QNetworkSession等类,实现承载管理。

想要在程序中使用 Qt 网络模块,我们需要在 pro 项目配置文件里增加下面的一条语句。

QT += network

一、获取本机的网络信息

为什么先写获取本机网络信息的内容呢?在建立网络通信之前我们至少得获取对方的 IP地址。在网络应用中,经常需要用到本机的主机名、IP 地址、MAC 地址等网络信息,通常通在 Windows 通过过调出命令行 cmd 窗口输入 ipconfig 或者在 Linux 系统中使用ifconfig 命令就可以查看相关信息了,在这里我们利用 Qt 做出一个可以查询的界面和功能出来,为了后面的网络编程打下一个简单的基础。

Qt 提供了 QHostInfo 和 QNetworkInterface 类可以用于此类信息查询。更多关于QHostInfo和 QNetworkInterface 的相关函数可以在 Qt 的帮助文档中找到。下面我们写代码时会使用到相关的函数,有清楚的注释。

1. 应用实例

本例目的:了解如何通过 QHostInfo 和 QNetworkInterface 类获取本地网络所有接口的信息。

例 07_networkhostinfo , 获 取 本 机 网 络 接 口 信 息 ( 难 度 : 一 般 )。 项 目 路 径 为Qt/2/07_networkhostinfo。本例获取本机的网络接口信息,打印在文本浏览框上,点击按钮可直接获取,为了清楚看见是重新获取的过程,本例点击获取本机信息按钮后延时 1s 去刷新获取的信息。点击另一个清空文本信息按钮可以清空文本浏览框上的文本内容。

项目文件 07_networkhostinfo.pro 文件第一行添加的代码部分如下。

1 QT += core gui network
2
3 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
4
5 CONFIG += c++11
6
7 # The following define makes your compiler emit warnings if you use
8 # any Qt feature that has been marked deprecated (the exact warnings
9 # depend on your compiler). Please consult the documentation of the
10 # deprecated API in order to know how to port your code away from it.
11 DEFINES += QT_DEPRECATED_WARNINGS
12
13 # You can also make your code fail to compile if it uses deprecated APIs.
14 # In order to do so, uncomment the following line.
15 # You can also select to disable deprecated APIs only up to a certain
version of Qt.
16 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the
APIs deprecated before Qt 6.0.0
17
18 SOURCES += \
19 main.cpp \
20 mainwindow.cpp
21
22 HEADERS += \
23 mainwindow.h
24
25 # Default rules for deployment.
26 qnx: target.path = /tmp/$${TARGET}/bin
27 else: unix:!android: target.path = /opt/$${TARGET}/bin
28 !isEmpty(target.path): INSTALLS += target

在头文件“mainwindow.h”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 07_networkhostinfo
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-10
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QPushButton>
6 #include <QTextBrowser>
7 #include <QVBoxLayout>
8 #include <QHBoxLayout>
9 #include <QTimer>
10
11 class MainWindow : public QMainWindow
12 {
13 Q_OBJECT
14
15 public:
16 MainWindow(QWidget *parent = nullptr);
17 ~MainWindow();
18
19 private:
20 /* 点击获取和清空文本按钮 */
21 QPushButton *pushButton[2];
22
23 /* 文本浏览框用于显示本机的信息 */
24 QTextBrowser *textBrowser;
25
26 /* 水平 Widget 容器和垂直 Widget 容器*/
27 QWidget *hWidget;
28 QWidget *vWidget;
29
30 /* 水平布局和垂直布局 */
31 QHBoxLayout *hBoxLayout;
32 QVBoxLayout *vBoxLayout;
33
34 /* 定时器 */
35 QTimer *timer;
36
37 /* 获取本机的网络的信息,返回类型是 QString */
38 QString getHostInfo();
39
40 private slots:
41 /* 定时器槽函数,点击按钮后定时触发 */
42 void timerTimeOut();
43
44 /* 显示本机信息 */
45 void showHostInfo();
46
47 /* 启动定时器 */
48 void timerStart();
49
50 /* 清空 textBrowser 的信息 */
51 void clearHostInfo();
52 };
53 #endif // MAINWINDOW_H
54

头文件里主要是声明两个按钮和一个文本浏览框。另外还有一个定时器,声明一些槽函数,比较简单。

在源文件“mainwindow.cpp”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 07_networkhostinfo
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-10
*******************************************************************/
1 #include "mainwindow.h"
2 #include <QNetworkInterface>
3 #include <QHostInfo>
4 #include <QThread>
5 #include <QDebug>
6
7 MainWindow::MainWindow(QWidget *parent)
8 : QMainWindow(parent)
9 {
10 /* 设置位置与大小 */
11 this->setGeometry(0, 0, 800, 480);
12
13 /* 点击获取本地信息按钮和清空文本按钮 */
14 pushButton[0] = new QPushButton();
15 pushButton[1] = new QPushButton();
16
17 pushButton[0]->setText("获取本机信息");
18 pushButton[1]->setText("清空文本信息");
19
20 /* 按钮的大小根据文本自适应,
21 * 注意 setSizePolicy 需要在布局中使用 */
22 pushButton[0]->setSizePolicy(QSizePolicy::Fixed,
23 QSizePolicy::Fixed);
24 pushButton[1]->setSizePolicy(QSizePolicy::Fixed,
25 QSizePolicy::Fixed);
26
27 /* 水平 Widget 和垂直 Widget 用于添加布局 */
28 hWidget = new QWidget();
29 vWidget = new QWidget();
30
31 /* 水平布局和垂直布局 */
32 hBoxLayout = new QHBoxLayout();
33 vBoxLayout = new QVBoxLayout();
34
35 /* 文本浏览框 */
36 textBrowser = new QTextBrowser();
37
38 /* 添加到水平布局 */
39 hBoxLayout->addWidget(pushButton[0]);
40 hBoxLayout->addWidget(pushButton[1]);
41
42 /* 将水平布局设置为 hWidget 的布局 */
43 hWidget->setLayout(hBoxLayout);
44
45 /* 将文本浏览框和 hWidget 添加到垂直布局 */
46 vBoxLayout->addWidget(textBrowser);
47 vBoxLayout->addWidget(hWidget);
48
49 /* 将垂直布局设置为 vWidget 的布局 */
50 vWidget->setLayout(vBoxLayout);
51
52 /* 设置 vWidget 为中心部件 */
53 setCentralWidget(vWidget);
54
55 /* 定时器初始化 */
56 timer = new QTimer();
57
58 /* 信号槽连接 */
59 connect(pushButton[0], SIGNAL(clicked()),
60 this, SLOT(timerStart()));
61 connect(pushButton[1], SIGNAL(clicked()),
62 this, SLOT(clearHostInfo()));
63 connect(timer, SIGNAL(timeout()),
64 this, SLOT(timerTimeOut()));
65 }
66
67 MainWindow::~MainWindow()
68 {
69 }
70
71
72 void MainWindow::timerStart()
73 {
74 /* 清空文本 */
75 textBrowser->clear();
76
77 /* 定时 1s */
78 timer->start(1000);
79 }
80
81 void MainWindow::timerTimeOut()
82 {
83 /* 显示本机信息 */
84 showHostInfo();
85
86 /* 停止定时器 */
87 timer->stop();
88 }
89
90 QString MainWindow::getHostInfo()
91 {
92 /* 通过 QHostInfo 的 localHostName 函数获取主机名称 */
93 QString str = "主机名称:" + QHostInfo::localHostName() + "\n";
94
95 /* 获取所有的网络接口,
96 * QNetworkInterface 类提供主机的 IP 地址和网络接口的列表 */
97 QList<QNetworkInterface> list
98 = QNetworkInterface::allInterfaces();
99
100 /* 遍历 list */
101 foreach (QNetworkInterface interface, list) {
102 str+= "网卡设备:" + interface.name() + "\n";
103 str+= "MAC 地址:" + interface.hardwareAddress() + "\n";
104
105 /* QNetworkAddressEntry 类存储 IP 地址子网掩码和广播地址 */
106 QList<QNetworkAddressEntry> entryList
107 = interface.addressEntries();
108
109 /* 遍历 entryList */
110 foreach (QNetworkAddressEntry entry, entryList) {
111 /* 过滤 IPv6 地址,只留下 IPv4 */
112 if (entry.ip().protocol() ==
113 QAbstractSocket::IPv4Protocol) {
114 str+= "IP 地址:" + entry.ip().toString() + "\n";
115 str+= "子网掩码:" + entry.netmask().toString() + "\n";
116 str+= "广播地址:" + entry.broadcast().toString() + "\n\n";
117 }
118 }
119 }
120
121 /* 返回网络信息 */
122 return str;
123 }
124
125 void MainWindow::showHostInfo()
126 {
127 /* 获取本机信息后显示到 textBrowser */
128 textBrowser->insertPlainText(getHostInfo());
129 }
130
131 void MainWindow::clearHostInfo()
132 {
133 /* 判断 textBrowser 是否为空,如果不为空则清空文本 */
134 if (!textBrowser->toPlainText().isEmpty())
135
136 /* 清空文本 */
137 textBrowser->clear();
138 }

第 90~123 行,是本例最重要的代码。
第 93 行,通过 QHostInfo 的 localHostName 函数获取主机名称。
第 97~98 行,通过 QNetworkInterface::allInterfaces()获取网络接口列表 list 类存储 IP 地址子网掩码和广播地址。如果我们用 qDebug()函数打印出 list,可以发现获取了所有的网络信息。而我们要提取网络里面的网络信息使用 QNetworkAddressEntry。
第 106~107 行,使用 QNetworkAddressEntry 从 interface 接口里使用函数 addressEntries(),获取所有的条目。就可以使用 QNetworkAddressEntry 的对象 entry 获取 IP 地址子网掩码和广播地址。
第 110~118 行,因为获取的 entries 在一个 QNetworkInterface 下可能有两个 IP,分别是 ipv4和 ipv6。这里使用 ip().protocol()来判断协议的类型,只留下 ipv4 类型的信息。筛选信息在我们写程序常常需要的。

2. 程序运行效果

点击获取本机信息,在文本浏览框内就打印出本机的网络信息(包括了主机名,网卡名,ip 地址等)。这里因为过滤掉了 IPv6 的信息。通常一个网卡有两个 ip 地址,一个是ipv4,另一个是 ipv6 的地址。下面的网卡设备 lo,是本地回环网卡。另一个 ens33 是虚拟机的网卡,由VMware 虚拟出来的。点击清空文本信息会清空文本浏览框里的网络信息。

在这里插入图片描述

二、TCP通信

1. TCP简介

TCP 协议(Transmission Control Protocol)全称是传输控制协议是一种 面向连接的、可靠的、基于字节流的传输层通信协议。

TCP 通信必 须先建立 TCP 连接,通信端分为客户端和服务端。服务端通过监听某个端口来监听是否有客户端连接到来,如果有连接到来,则建立新的 socket 连接;客户端通过 ip 和port 连接服务端,当成功建立连接之后,就可进行数据的收发了。需要注意的是,在 Qt 中,Qt 把 socket 当成输入输出流来对待的,数据的收发是通过 read()和 write()来进行的,需要与我们常见的 send()与 recv()进行区分。

TCP 客户端与服务端通信示意图如下。
在这里插入图片描述

2. TCP服务端应用实例

本例目的:了解 TCP 服务端的使用。

例 08_tcpserver,TCP 服务端(难度:一般)。项目路径为 Qt/2/08_tcpserver。本例大体流程首先获取本地 IP 地址。创建一个 tcpSocket 套接字,一个 tcpServer 服务端。点击监听即监听本地的主机 IP 地址和端口,同时等待服务端的连接。此程序需要结合客户端一起使用。

项目文件 08_tcpserver.pro 文件第一行添加的代码部分如下。

1 QT += core gui network
2
3 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
4
5 CONFIG += c++11
6
7 # The following define makes your compiler emit warnings if you use
8 # any Qt feature that has been marked deprecated (the exact warnings
9 # depend on your compiler). Please consult the documentation of the
10 # deprecated API in order to know how to port your code away from it.
11 DEFINES += QT_DEPRECATED_WARNINGS
12
13 # You can also make your code fail to compile if it uses deprecated APIs.
14 # In order to do so, uncomment the following line.
15 # You can also select to disable deprecated APIs only up to a certain
version of Qt.
16 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the
APIs deprecated before Qt 6.0.0
17
18 SOURCES += \
19 main.cpp \
20 mainwindow.cpp
21
22 HEADERS += \
23 mainwindow.h
24
25 # Default rules for deployment.
26 qnx: target.path = /tmp/$${TARGET}/bin
27 else: unix:!android: target.path = /opt/$${TARGET}/bin
28 !isEmpty(target.path): INSTALLS += target

在头文件“mainwindow.h”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 08_tcpserver
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-13
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QTcpServer>
6 #include <QTcpSocket>
7 #include <QVBoxLayout>
8 #include <QHBoxLayout>
9 #include <QPushButton>
10 #include <QTextBrowser>
11 #include <QLabel>
12 #include <QComboBox>
13 #include <QSpinBox>
14 #include <QHostInfo>
15 #include <QLineEdit>
16 #include <QNetworkInterface>
17 #include <QDebug>
18
19 class MainWindow : public QMainWindow
20 {
21 Q_OBJECT
22
23 public:
24 MainWindow(QWidget *parent = nullptr);
25 ~MainWindow();
26
27 private:
28 /* tcp 服务器 */
29 QTcpServer *tcpServer;
30
31 /* 通信套接字 */
32 QTcpSocket *tcpSocket;
33
34 /* 按钮 */
35 QPushButton *pushButton[4];
36
37 /* 标签文本 */
38 QLabel *label[2];
39
40 /* 水平容器 */
41 QWidget *hWidget[3];
42
43 /* 水平布局 */
44 QHBoxLayout *hBoxLayout[3];
45
46 /* 垂直容器 */
47 QWidget *vWidget;
48
49 /* 垂直布局 */
50 QVBoxLayout *vBoxLayout;
51
52 /* 文本浏览框 */
53 QTextBrowser *textBrowser;
54
55 /* 用于显示本地 ip */
56 QComboBox *comboBox;
57
58 /* 用于选择端口 */
59 QSpinBox *spinBox;
60
61 /* 文本输入框 */
62 QLineEdit *lineEdit;
63
64 /* 存储本地的 ip 列表地址 */
65 QList<QHostAddress> IPlist;
66
67 /* 获取本地的所有 ip */
68 void getLocalHostIP();
69
70 private slots:
71 /* 客户端连接处理槽函数 */
72 void clientConnected();
73
74 /* 开始监听槽函数 */
75 void startListen();
76
77 /* 停止监听槽函数 */
78 void stopListen();
79
80 /* 清除文本框时的内容 */
81 void clearTextBrowser();
82
83 /* 接收到消息 */
84 void receiveMessages();
85
86 /* 发送消息 */
87 void sendMessages();
88
89 /* 连接状态改变槽函数 */
90 void socketStateChange(QAbstractSocket::SocketState);
91 };
92 #endif // MAINWINDOW_H

头文件里主要是声明界面用的元素,及一些槽函数。重点是声明 tcpServer 和 tcpSocket。

在源文件“mainwindow.cpp”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 08_tcpserver
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-13
*******************************************************************/
1 #include "mainwindow.h"
2
3 MainWindow::MainWindow(QWidget *parent)
4 : QMainWindow(parent)
5 {
6 /* 设置主窗体的位置与大小 */
7 this->setGeometry(0, 0, 800, 480);
8
9 /* 实例化 tcp 服务器与 tcp 套接字 */
10 tcpServer = new QTcpServer(this);
11 tcpSocket = new QTcpSocket(this);
12
13 /* 开始监听按钮 */
14 pushButton[0] = new QPushButton();
15 /* 停止监听按钮 */
16 pushButton[1] = new QPushButton();
17 /* 清空聊天文本按钮 */
18 pushButton[2] = new QPushButton();
19 /* 发送消息按钮 */
20 pushButton[3] = new QPushButton();
21
22 /* 水平布局一 */
23 hBoxLayout[0] = new QHBoxLayout();
24 /* 水平布局二 */
25 hBoxLayout[1] = new QHBoxLayout();
26 /* 水平布局三 */
27 hBoxLayout[2] = new QHBoxLayout();
28 /* 水平布局四 */
29 hBoxLayout[3] = new QHBoxLayout();
30
31 /* 水平容器一 */
32 hWidget[0] = new QWidget();
33 /* 水平容器二 */
34 hWidget[1] = new QWidget();
35 /* 水平容器三 */
36 hWidget[2] = new QWidget();
37
38 vWidget = new QWidget();
39 vBoxLayout = new QVBoxLayout();
40
41 /* 标签实例化 */
42 label[0] = new QLabel();
43 label[1] = new QLabel();
44
45 lineEdit = new QLineEdit();
46 comboBox = new QComboBox();
47 spinBox = new QSpinBox();
48 textBrowser = new QTextBrowser();
49
50 label[0]->setText("监听 IP 地址:");
51 label[1]->setText("监听端口:");
52
53 /* 设置标签根据文本文字大小自适应大小 */
54 label[0]->setSizePolicy(QSizePolicy::Fixed,
55 QSizePolicy::Fixed);
56 label[1]->setSizePolicy(QSizePolicy::Fixed,
57 QSizePolicy::Fixed);
58
59 /* 设置端口号的范围,注意不要与主机的已使用的端口号冲突 */
60 spinBox->setRange(10000, 99999);
61
62 pushButton[0]->setText("开始监听");
63 pushButton[1]->setText("停止监听");
64 pushButton[2]->setText("清空文本");
65 pushButton[3]->setText("发送消息");
66
67 /* 设置停止监听状态不可用 */
68 pushButton[1]->setEnabled(false);
69
70 /* 设置输入框默认的文本 */
71 lineEdit->setText("www.openedv.com 正点原子论坛");
72
73 /* 水平布局一添加内容 */
74 hBoxLayout[0]->addWidget(pushButton[0]);
75 hBoxLayout[0]->addWidget(pushButton[1]);
76 hBoxLayout[0]->addWidget(pushButton[2]);
77
78 /* 设置水平容器一的布局为水平布局一 */
79 hWidget[0]->setLayout(hBoxLayout[0]);
80
81 /* 水平布局二添加内容 */
82 hBoxLayout[1]->addWidget(label[0]);
83 hBoxLayout[1]->addWidget(comboBox);
84 hBoxLayout[1]->addWidget(label[1]);
85 hBoxLayout[1]->addWidget(spinBox);
86
87 /* 设置水平容器二的布局为水平布局二 */
88 hWidget[1]->setLayout(hBoxLayout[1]);
89
90 /* 水平布局三添加内容 */
91 hBoxLayout[2]->addWidget(lineEdit);
92 hBoxLayout[2]->addWidget(pushButton[3]);
93
94 /* 设置水平容器三的布局为水平布局一 */
95 hWidget[2]->setLayout(hBoxLayout[2]);
96
97 /* 垂直布局添加内容 */
98 vBoxLayout->addWidget(textBrowser);
99 vBoxLayout->addWidget(hWidget[1]);
100 vBoxLayout->addWidget(hWidget[0]);
101 vBoxLayout->addWidget(hWidget[2]);
102
103 /* 设置垂直容器的布局为垂直布局 */
104 vWidget->setLayout(vBoxLayout);
105
106 /* 居中显示 */
107 setCentralWidget(vWidget);
108
109 /* 获取本地 ip */
110 getLocalHostIP();
111
112 /* 信号槽连接 */
113 connect(pushButton[0], SIGNAL(clicked()),
114 this, SLOT(startListen()));
115 connect(pushButton[1], SIGNAL(clicked()),
116 this, SLOT(stopListen()));
117 connect(pushButton[2], SIGNAL(clicked()),
118 this, SLOT(clearTextBrowser()));
119 connect(pushButton[3], SIGNAL(clicked()),
120 this, SLOT(sendMessages()));
121 connect(tcpServer, SIGNAL(newConnection()),
122 this, SLOT(clientConnected()));
123 }
124
125 MainWindow::~MainWindow()
126 {
127 }
128
129 /* 新的客户端连接 */
130 void MainWindow::clientConnected()
131 {
132 /* 获取客户端的套接字 */
133 tcpSocket = tcpServer->nextPendingConnection();
134 /* 客户端的 ip 信息 */
135 QString ip = tcpSocket->peerAddress().toString();
136 /* 客户端的端口信息 */
137 qint16 port = tcpSocket->peerPort();
138 /* 在文本浏览框里显示出客户端的连接信息 */
139 textBrowser->append("客户端已连接");
140 textBrowser->append("客户端 ip 地址:"
141 + ip);
142 textBrowser->append("客户端端口:"
143 + QString::number(port));
144
145 connect(tcpSocket, SIGNAL(readyRead()),
146 this, SLOT(receiveMessages()));
147 connect(tcpSocket,
148 SIGNAL(stateChanged(QAbstractSocket::SocketState)),
149 this,
150 SLOT(socketStateChange(QAbstractSocket::SocketState)));
151 }
152
153 /* 获取本地 IP */
154 void MainWindow::getLocalHostIP()
155 {
156 // /* 获取主机的名称 */
157 // QString hostName = QHostInfo::localHostName();
158
159 // /* 主机的信息 */
160 // QHostInfo hostInfo = QHostInfo::fromName(hostName);
161
162 // /* ip 列表,addresses 返回 ip 地址列表,注意主机应能从路由器获取到
163 // * IP,否则可能返回空的列表(ubuntu 用此方法只能获取到环回 IP) */
164 // IPlist = hostInfo.addresses();
165 // qDebug()<<IPlist<<endl;
166
167 // /* 遍历 IPlist */
168 // foreach (QHostAddress ip, IPlist) {
169 // if (ip.protocol() == QAbstractSocket::IPv4Protocol)
170 // comboBox->addItem(ip.toString());
171 // }
172
173 /* 获取所有的网络接口,
174 * QNetworkInterface 类提供主机的 IP 地址和网络接口的列表 */
175 QList<QNetworkInterface> list
176 = QNetworkInterface::allInterfaces();
177
178 /* 遍历 list */
179 foreach (QNetworkInterface interface, list) {
180
181 /* QNetworkAddressEntry 类存储 IP 地址子网掩码和广播地址 */
182 QList<QNetworkAddressEntry> entryList
183 = interface.addressEntries();
184
185 /* 遍历 entryList */
186 foreach (QNetworkAddressEntry entry, entryList) {
187 /* 过滤 IPv6 地址,只留下 IPv4 */
188 if (entry.ip().protocol() ==
189 QAbstractSocket::IPv4Protocol) {
190 comboBox->addItem(entry.ip().toString());
191 /* 添加到 IP 列表中 */
192 IPlist<<entry.ip();
193 }
194 }
195 }
196 }
197
198 /* 开始监听 */
199 void MainWindow::startListen()
200 {
201 /* 需要判断当前主机是否有 IP 项 */
202 if (comboBox->currentIndex() != -1) {
203 qDebug()<<"start listen"<<endl;
204 tcpServer->listen(IPlist[comboBox->currentIndex()],
205 spinBox->value());
206
207 /* 设置按钮与下拉列表框的状态 */
208 pushButton[0]->setEnabled(false);
209 pushButton[1]->setEnabled(true);
210 comboBox->setEnabled(false);
211 spinBox->setEnabled(false);
212
213 /* 在文本浏览框里显示出服务端 */
214 textBrowser->append("服务器 IP 地址:"
215 + comboBox->currentText());
216 textBrowser->append("正在监听端口:"
217 + spinBox->text());
218 }
219 }
220
221 /* 停止监听 */
222 void MainWindow::stopListen()
223 {
224 qDebug()<<"stop listen"<<endl;
225 /* 停止监听 */
226 tcpServer->close();
227
228 /* 如果是连接上了也应该断开,如果不断开客户端还能继续发送信息,
229 * 因为 socket 未断开,还在监听上一次端口 */
230 if (tcpSocket->state() == tcpSocket->ConnectedState)
231 tcpSocket->disconnectFromHost();
232
233 /* 设置按钮与下拉列表框的状态 */
234 pushButton[1]->setEnabled(false);
235 pushButton[0]->setEnabled(true);
236 comboBox->setEnabled(true);
237 spinBox->setEnabled(true);
238
239 /* 将停止监听的信息添加到文本浏览框中 */
240 textBrowser->append("已停止监听端口:"
241 + spinBox->text());
242 }
243
244 /* 清除文本浏览框里的内容 */
245 void MainWindow::clearTextBrowser()
246 {
247 /* 清除文本浏览器的内容 */
248 textBrowser->clear();
249 }
250
251 /* 服务端接收消息 */
252 void MainWindow::receiveMessages()
253 {
254 /* 读取接收到的消息 */
255 QString messages = "客户端:" + tcpSocket->readAll();
256 textBrowser->append(messages);
257 }
258
259 /* 服务端发送消息 */
260 void MainWindow::sendMessages()
261 {
262 if(NULL == tcpSocket)
263 return;
264
265 /* 如果已经连接 */
266 if(tcpSocket->state() == tcpSocket->ConnectedState) {
267 /* 发送消息 */
268 tcpSocket->write(lineEdit->text().toUtf8().data());
269
270 /* 在服务端插入发送的消息 */
271 textBrowser->append("服务端:" + lineEdit->text());
272 }
273 }
274
275 /* 服务端状态改变 */
276 void MainWindow::socketStateChange(QAbstractSocket::SocketState
state)
277 {
278 switch (state) {
279 case QAbstractSocket::UnconnectedState:
280 textBrowser->append("scoket 状态:UnconnectedState");
281 break;
282 case QAbstractSocket::ConnectedState:
283 textBrowser->append("scoket 状态:ConnectedState");
284 break;
285 case QAbstractSocket::ConnectingState:
286 textBrowser->append("scoket 状态:ConnectingState");
287 break;
288 case QAbstractSocket::HostLookupState:
289 textBrowser->append("scoket 状态:HostLookupState");
290 break;
291 case QAbstractSocket::ClosingState:
292 textBrowser->append("scoket 状态:ClosingState");
293 break;
294 case QAbstractSocket::ListeningState:
295 textBrowser->append("scoket 状态:ListeningState");
296 break;
297 case QAbstractSocket::BoundState:
298 textBrowser->append("scoket 状态:BoundState");
299 break;
300 default:
301 break;
302 }
303 }

上面的代码主要是服务端开启监听,如果有客户端连到服务端,就会发射 newConnection()信号,同时也连接到接收消息的信号与槽函数。点击发送消息按钮就可以使用 tcpSocket 发送消息。注意发送消息和接收消息都是通过 tcpSocket 的 read()和 write()进行。

3. TCP客户端应用实例

本例目的:了解 TCP 客户的使用。

例 09_tcpclient,TCP 客户端(难度:一般)。项目路径为 Qt/2/09_ tcpclient。本例大体流程:首先获取本地 IP 地址。创建一个 tcpSocket 套接字,然后用 tcpSocket 套接字使用 connectToHost函数连接服务端的主机 IP 地址和端口,即可相互通信。

项目文件 08_tcpserver.pro 文件第一行添加的代码部分如下。

1 QT += core gui network
2
3 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
4
5 CONFIG += c++11
6
7 # The following define makes your compiler emit warnings if you use
8 # any Qt feature that has been marked deprecated (the exact warnings
9 # depend on your compiler). Please consult the documentation of the
10 # deprecated API in order to know how to port your code away from it.
11 DEFINES += QT_DEPRECATED_WARNINGS
12
13 # You can also make your code fail to compile if it uses deprecated APIs.
14 # In order to do so, uncomment the following line.
15 # You can also select to disable deprecated APIs only up to a certain
version of Qt.
16 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the
APIs deprecated before Qt 6.0.0
17
18 SOURCES += \
19 main.cpp \
20 mainwindow.cpp
21
22 HEADERS += \
23 mainwindow.h
24
25 # Default rules for deployment.
26 qnx: target.path = /tmp/$${TARGET}/bin
27 else: unix:!android: target.path = /opt/$${TARGET}/bin
28 !isEmpty(target.path): INSTALLS += target

在头文件“mainwindow.h”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 09_tcpclient
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-13
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QTcpServer>
6 #include <QTcpSocket>
7 #include <QVBoxLayout>
8 #include <QHBoxLayout>
9 #include <QPushButton>
10 #include <QTextBrowser>
11 #include <QLabel>
12 #include <QComboBox>
13 #include <QSpinBox>
14 #include <QHostInfo>
15 #include <QLineEdit>
16 #include <QNetworkInterface>
17 #include <QDebug>
18
19 class MainWindow : public QMainWindow
20 {
21 Q_OBJECT
22
23 public:
24 MainWindow(QWidget *parent = nullptr);
25 ~MainWindow();
26
27 private:
28 /* 通信套接字 */
29 QTcpSocket *tcpSocket;
30
31 /* 按钮 */
32 QPushButton *pushButton[4];
33
34 /* 标签文本 */
35 QLabel *label[2];
36
37 /* 水平容器 */
38 QWidget *hWidget[3];
39
40 /* 水平布局 */
41 QHBoxLayout *hBoxLayout[3];
42
43 /* 垂直容器 */
44 QWidget *vWidget;
45
46 /* 垂直布局 */
47 QVBoxLayout *vBoxLayout;
48
49 /* 文本浏览框 */
50 QTextBrowser *textBrowser;
51
52 /* 用于显示本地 ip */
53 QComboBox *comboBox;
54
55 /* 用于选择端口 */
56 QSpinBox *spinBox;
57
58 /* 文本输入框 */
59 QLineEdit *lineEdit;
60
61 /* 存储本地的 ip 列表地址 */
62 QList<QHostAddress> IPlist;
63
64 /* 获取本地的所有 ip */
65 void getLocalHostIP();
66
67 private slots:
68 /* 连接 */
69 void toConnect();
70
71 /* 断开连接 */
72 void toDisConnect();
73
74 /* 已连接 */
75 void connected();
76
77 /* 已断开连接 */
78 void disconnected();
79
80 /* 清除文本框时的内容 */
81 void clearTextBrowser();
82
83 /* 接收到消息 */
84 void receiveMessages();
85
86 /* 发送消息 */
87 void sendMessages();
88
89 /* 连接状态改变槽函数 */
90 void socketStateChange(QAbstractSocket::SocketState);
91 };
92 #endif // MAINWINDOW_H

头文件里主要是声明界面用的元素,及一些槽函数。重点是声明 tcpSocket。

在源文件“mainwindow.cpp”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 09_tcpclient
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-13
*******************************************************************/
1 #include "mainwindow.h"
2
3 MainWindow::MainWindow(QWidget *parent)
4 : QMainWindow(parent)
5 {
6 /* 设置主窗体的位置与大小 */
7 this->setGeometry(0, 0, 800, 480);
8
9 /* tcp 套接字 */
10 tcpSocket = new QTcpSocket(this);
11
12 /* 开始监听按钮 */
13 pushButton[0] = new QPushButton();
14 /* 停止监听按钮 */
15 pushButton[1] = new QPushButton();
16 /* 清空聊天文本按钮 */
17 pushButton[2] = new QPushButton();
18 /* 发送消息按钮 */
19 pushButton[3] = new QPushButton();
20
21 /* 水平布局一 */
22 hBoxLayout[0] = new QHBoxLayout();
23 /* 水平布局二 */
24 hBoxLayout[1] = new QHBoxLayout();
25 /* 水平布局三 */
26 hBoxLayout[2] = new QHBoxLayout();
27 /* 水平布局四 */
28 hBoxLayout[3] = new QHBoxLayout();
29
30 /* 水平容器一 */
31 hWidget[0] = new QWidget();
32 /* 水平容器二 */
33 hWidget[1] = new QWidget();
34 /* 水平容器三 */
35 hWidget[2] = new QWidget();
36
37
38 vWidget = new QWidget();
39 vBoxLayout = new QVBoxLayout();
40
41 /* 标签实例化 */
42 label[0] = new QLabel();
43 label[1] = new QLabel();
44
45 lineEdit = new QLineEdit();
46 comboBox = new QComboBox();
47 spinBox = new QSpinBox();
48 textBrowser = new QTextBrowser();
49
50 label[0]->setText("服务器地址:");
51 label[1]->setText("服务器端口:");
52
53 /* 设置标签根据文本文字大小自适应大小 */
54 label[0]->setSizePolicy(QSizePolicy::Fixed,
55 QSizePolicy::Fixed);
56 label[1]->setSizePolicy(QSizePolicy::Fixed,
57 QSizePolicy::Fixed);
58
59 /* 设置端口号的范围,注意不要与主机的已使用的端口号冲突 */
60 spinBox->setRange(10000, 99999);
61
62 pushButton[0]->setText("连接服务器");
63 pushButton[1]->setText("断开连接");
64 pushButton[2]->setText("清空文本");
65 pushButton[3]->setText("发送消息");
66
67 /* 设置停止监听状态不可用 */
68 pushButton[1]->setEnabled(false);
69
70 /* 设置输入框默认的文本 */
71 lineEdit->setText("广州星翼电子科技有限公司");
72
73 /* 水平布局一添加内容 */
74 hBoxLayout[0]->addWidget(pushButton[0]);
75 hBoxLayout[0]->addWidget(pushButton[1]);
76 hBoxLayout[0]->addWidget(pushButton[2]);
77
78 /* 设置水平容器的布局为水平布局一 */
79 hWidget[0]->setLayout(hBoxLayout[0]);
80
81 hBoxLayout[1]->addWidget(label[0]);
82 hBoxLayout[1]->addWidget(comboBox);
83 hBoxLayout[1]->addWidget(label[1]);
84 hBoxLayout[1]->addWidget(spinBox);
85
86 /* 设置水平容器的布局为水平布局二 */
87 hWidget[1]->setLayout(hBoxLayout[1]);
88
89 /* 水平布局三添加内容 */
90 hBoxLayout[2]->addWidget(lineEdit);
91 hBoxLayout[2]->addWidget(pushButton[3]);
92
93 /* 设置水平容器三的布局为水平布局一 */
94 hWidget[2]->setLayout(hBoxLayout[2]);
95
96 /* 垂直布局添加内容 */
97 vBoxLayout->addWidget(textBrowser);
98 vBoxLayout->addWidget(hWidget[1]);
99 vBoxLayout->addWidget(hWidget[0]);
100 vBoxLayout->addWidget(hWidget[2]);
101
102 /* 设置垂直容器的布局为垂直布局 */
103 vWidget->setLayout(vBoxLayout);
104
105 /* 居中显示 */
106 setCentralWidget(vWidget);
107
108 /* 获取本地 ip */
109 getLocalHostIP();
110
111 /* 信号槽连接 */
112 connect(pushButton[0], SIGNAL(clicked()),
113 this, SLOT(toConnect()));
114 connect(pushButton[1], SIGNAL(clicked()),
115 this, SLOT(toDisConnect()));
116 connect(pushButton[2], SIGNAL(clicked()),
117 this, SLOT(clearTextBrowser()));
118 connect(pushButton[3], SIGNAL(clicked()),
119 this, SLOT(sendMessages()));
120 connect(tcpSocket, SIGNAL(connected()),
121 this, SLOT(connected()));
122 connect(tcpSocket, SIGNAL(disconnected()),
123 this, SLOT(disconnected()));
124 connect(tcpSocket, SIGNAL(readyRead()),
125 this, SLOT(receiveMessages()));
126 connect(tcpSocket,
127 SIGNAL(stateChanged(QAbstractSocket::SocketState)),
128 this,
129 SLOT(socketStateChange(QAbstractSocket::SocketState)));
130 }
131
132 MainWindow::~MainWindow()
133 {
134 }
135
136 void MainWindow::toConnect()
137 {
138 /* 如果连接状态还没有连接 */
139 if (tcpSocket->state() != tcpSocket->ConnectedState) {
140 /* 指定 IP 地址和端口连接 */
141 tcpSocket->connectToHost(IPlist[comboBox->currentIndex()],
142 spinBox->value());
143 }
144 }
145
146 void MainWindow::toDisConnect()
147 {
148 /* 断开连接 */
149 tcpSocket->disconnectFromHost();
150
151 /* 关闭 socket*/
152 tcpSocket->close();
153 }
154
155 void MainWindow::connected()
156 {
157 /* 显示已经连接 */
158 textBrowser->append("已经连上服务端");
159
160 /* 设置按钮与下拉列表框的状态 */
161 pushButton[0]->setEnabled(false);
162 pushButton[1]->setEnabled(true);
163 comboBox->setEnabled(false);
164 spinBox->setEnabled(false);
165 }
166
167 void MainWindow::disconnected()
168 {
169 /* 显示已经断开连接 */
170 textBrowser->append("已经断开服务端");
171
172 /* 设置按钮与下拉列表框的状态 */
173 pushButton[1]->setEnabled(false);
174 pushButton[0]->setEnabled(true);
175 comboBox->setEnabled(true);
176 spinBox->setEnabled(true);
177 }
178
179 /* 获取本地 IP */
180 void MainWindow::getLocalHostIP()
181 {
182 // /* 获取主机的名称 */
183 // QString hostName = QHostInfo::localHostName();
184
185 // /* 主机的信息 */
186 // QHostInfo hostInfo = QHostInfo::fromName(hostName);
187
188 // /* ip 列表,addresses 返回 ip 地址列表,注意主机应能从路由器获取到
189 // * IP,否则可能返回空的列表(ubuntu 用此方法只能获取到环回 IP) */
190 // IPlist = hostInfo.addresses();
191 // qDebug()<<IPlist<<endl;
192
193 // /* 遍历 IPlist */
194 // foreach (QHostAddress ip, IPlist) {
195 // if (ip.protocol() == QAbstractSocket::IPv4Protocol)
196 // comboBox->addItem(ip.toString());
197 // }
198
199 /* 获取所有的网络接口,
200 * QNetworkInterface 类提供主机的 IP 地址和网络接口的列表 */
201 QList<QNetworkInterface> list
202 = QNetworkInterface::allInterfaces();
203
204 /* 遍历 list */
205 foreach (QNetworkInterface interface, list) {
206
207 /* QNetworkAddressEntry 类存储 IP 地址子网掩码和广播地址 */
208 QList<QNetworkAddressEntry> entryList
209 = interface.addressEntries();
210
211 /* 遍历 entryList */
212 foreach (QNetworkAddressEntry entry, entryList) {
213 /* 过滤 IPv6 地址,只留下 IPv4 */
214 if (entry.ip().protocol() ==
215 QAbstractSocket::IPv4Protocol) {
216 comboBox->addItem(entry.ip().toString());
217 /* 添加到 IP 列表中 */
218 IPlist<<entry.ip();
219 }
220 }
221 }
222 }
223
224 /* 清除文本浏览框里的内容 */
225 void MainWindow::clearTextBrowser()
226 {
227 /* 清除文本浏览器的内容 */
228 textBrowser->clear();
229 }
230
231 /* 客户端接收消息 */
232 void MainWindow::receiveMessages()
233 {
234 /* 读取接收到的消息 */
235 QString messages = tcpSocket->readAll();
236 textBrowser->append("服务端:" + messages);
237 }
238
239 /* 客户端发送消息 */
240 void MainWindow::sendMessages()
241 {
242 if(NULL == tcpSocket)
243 return;
244
245 if(tcpSocket->state() == tcpSocket->ConnectedState) {
246 /* 客户端显示发送的消息 */
247 textBrowser->append("客户端:" + lineEdit->text());
248
249 /* 发送消息 */
250 tcpSocket->write(lineEdit->text().toUtf8().data());
251 }
252 }
253
254 /* 客户端状态改变 */
255 void MainWindow::socketStateChange(QAbstractSocket::SocketState
state)
256 {
257 switch (state) {
258 case QAbstractSocket::UnconnectedState:
259 textBrowser->append("scoket 状态:UnconnectedState");
260 break;
261 case QAbstractSocket::ConnectedState:
262 textBrowser->append("scoket 状态:ConnectedState");
263 break;
264 case QAbstractSocket::ConnectingState:
265 textBrowser->append("scoket 状态:ConnectingState");
266 break;
267 case QAbstractSocket::HostLookupState:
268 textBrowser->append("scoket 状态:HostLookupState");
269 break;
270 case QAbstractSocket::ClosingState:
271 textBrowser->append("scoket 状态:ClosingState");
272 break;
273 case QAbstractSocket::ListeningState:
274 textBrowser->append("scoket 状态:ListeningState");
275 break;
276 case QAbstractSocket::BoundState:
277 textBrowser->append("scoket 状态:BoundState");
278 break;
279 default:
280 break;
281 }
282 }

上面的代码主要是客户端开使用 connectToHost 通过 IP 地址和端口与服务端连接,如果连接成功,就会发射 connected ()信号,同时也连接到接收消息的信号与槽函数。点击发送消息按钮就可以使用tcpSocket发送消息。注意发送消息和接收消息都是通过tcpSocket的read()和write()进行。

4. 程序运行效果

开启服务端后,需要选择本地监听的 IP 地址和监听的端口(特别需要注意,不要选择监听的端口与本地主机的已经使用的端口,所以编者把端口号设置的特别大,查看本地已经使用的端口号可以使用 netstat 指令。)

启动客户端后,选择需要连接的服务器 IP 地址和服务器监听的端口。点击连接后就可以相互发送消息了。

注意服务端和客户端都本例都是选择了本地环回 IP 127.0.0.1 测试。也可以选择本地的其他IP 地址进行测试。

TCP 服务端:
在这里插入图片描述
TCP 客户端:
在这里插入图片描述

三、UDP通信

1. UDP简介

UDP(User Datagram Protocol 即用户数据报协议)是一个 轻量级的,不可靠的,面向数据报的无连接协议。我们日常生活中使用的 QQ,其聊天时的文字内容是使用 UDP 协议进行消息发送的。因为 QQ 有很多用户,发送的大部分都是短消息,要求能及时响应,并且对安全性要求不是很高的情况下使用 UDP 协议。但是 QQ 也并不是完全使用 UDP 协议,比如我们在传输文件时就会选择 TCP 协议,保证文件正确传输。像 QQ 语音和 QQ 视频通话,UDP 的优势就很突出了。在选择使用协议的时候,选择 UDP 必须要谨慎。在网络质量令人十分不满意的环境下,UDP 协议数据包丢失会比较严重。但是由于 UDP 的特性:它 不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用 UDP 较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

QUdpSocket 类提供了一个 UDP 套接字。QUdpSocket 是 QAbstractSocket 的子类,允许发送和接收 UDP 数据报。使用该类最常见的方法是使用 bind()绑定到一个地址和端口,然后调用writeDatagram()和 readDatagram() / receiveDatagram()来传输数据。注意发送数据一般少于 512字节。如果发送多于 512 字节的数据,即使我们发送成功了,也会在 IP 层被分片(分成小片段)。

如果您想使用标准的 QIODevice 函数 read()、readLine()、write()等,您必须首先通过调用connectToHost()将套接字直接连接到对等体。每次将数据报写入网络时,套接字都会发出bytesWritten()信号。

如果您只是想发送数据报,您不需要调用 bind()。readyRead()信号在数据报到达时发出。
在这种情况下,hasPendingDatagrams()返回 true。调用 pendingDatagramSize()来获取第一个待处理数据报的大小,并调用 readDatagram()或 receiveDatagram()来读取它。注意:当您接收到readyRead()信号时,一个传入的数据报应该被读取,否则这个信号将不会被发送到下一个数据报。

UDP 通信示意图如下。重点是 QUdpSocket 类,已经为我们提供了 UDP 通信的基础
在这里插入图片描述
UDP 消息传送有三种模式,分别是单播、广播和组播三种模式。
在这里插入图片描述

单播(unicast):单播用于两个主机之间的端对端通信,需要知道对方的 IP 地址与端口。
 广播(broadcast):广播 UDP 与单播 UDP 的区别就是 IP 地址不同,广播一般使用广播地址255.255.255.255,将消息发送到在同一广播(也就是局域网内同一网段)网络上的每个主机。值得强调的是:本地广播信息是不会被路由器转发。当然这是十分容易理解的,因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。这也是为什么 IP 协议的设计者故意没有定义互联网范围的广播机制。广播地址通常用于在网络游戏中处于同一本地网络的玩家之间交流状态信息等。其实广播顾名思义,就是想局域网内所有的人说话,但是广播还是要指明接收者的端口号的,因为不可能接受者的所有端口都来收听广播。
 组播(multicast):组播(多点广播),也称为“多播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。在广域网上广播的时候,其中的交换机和路由器只向需要获取数据的主机复制并转发数据。主机可以向路由器请求加入或退出某个组,网络中的路由器和交换机有选择地复制并传输数据,将数据仅仅传输给组内的主机。多播的这种功能,可以一次将数据发送到多个主机,又能保证不影响其他不需要(未加入组)的主机的其他通信。

注意:单播一样和多播是允许在广域网即 Internet 上进行传输的,而广播仅仅在同一局域
网上才能进行。

2. UDP单播与广播

广播 UDP 与单播 UDP 的区别就是 IP 地址不同,所以我们的实例可以写成一个。我们可以这么理解,单播实际上是通信上对应一对一,广播则是一对多(多,这里指广播地址内的所有主机)。

1. 应用实例

本例目的:了解 QUdpSocket 单播和广播使用。

例 10_udp_unicast_broadcast,UDP 单播与广播应用(难度:一般)。项目路径为Qt/2/10_udp_unicast_broadcast。本例大体流程首先获取本地 IP 地址。创建一个 udpSocket 套接字,然后绑定本地主机的端口(也就是监听端口)。我们可以使用 QUdpSocket 类提供的读写函数 readDatagram 和 writeDatagram,知道目标 IP 地址和端口,即可完成消息的接收与发送。

项目文件 10_udp_unicast_broadcast.pro 文件第一行添加的代码部分如下。

1 QT += core gui network
2
3 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
4
5 CONFIG += c++11
6
7 # The following define makes your compiler emit warnings if you use
8 # any Qt feature that has been marked deprecated (the exact warnings
9 # depend on your compiler). Please consult the documentation of the
10 # deprecated API in order to know how to port your code away from it.
11 DEFINES += QT_DEPRECATED_WARNINGS
12
13 # You can also make your code fail to compile if it uses deprecated APIs.
14 # In order to do so, uncomment the following line.
15 # You can also select to disable deprecated APIs only up to a certain
version of Qt.
16 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the
APIs deprecated before Qt 6.0.0
17
18 SOURCES += \
19 main.cpp \
20 mainwindow.cpp
21
22 HEADERS += \
23 mainwindow.h
24
25 # Default rules for deployment.
26 qnx: target.path = /tmp/$${TARGET}/bin
27 else: unix:!android: target.path = /opt/$${TARGET}/bin
28 !isEmpty(target.path): INSTALLS += target

在头文件“mainwindow.h”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 10_udp_unicast_broadcast
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-14
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QUdpSocket>
6 #include <QVBoxLayout>
7 #include <QHBoxLayout>
8 #include <QPushButton>
9 #include <QTextBrowser>
10 #include <QLabel>
11 #include <QComboBox>
12 #include <QSpinBox>
13 #include <QHostInfo>
14 #include <QLineEdit>
15 #include <QNetworkInterface>
16 #include <QDebug>
17
18 class MainWindow : public QMainWindow
19 {
20 Q_OBJECT
21
22 public:
23 MainWindow(QWidget *parent = nullptr);
24 ~MainWindow();
25
26 private:
27 /* Udp 通信套接字 */
28 QUdpSocket *udpSocket;
29
30 /* 按钮 */
31 QPushButton *pushButton[5];
32
33 /* 标签文本 */
34 QLabel *label[3];
35
36 /* 水平容器 */
37 QWidget *hWidget[3];
38
39 /* 水平布局 */
40 QHBoxLayout *hBoxLayout[3];
41
42 /* 垂直容器 */
43 QWidget *vWidget;
44
45 /* 垂直布局 */
46 QVBoxLayout *vBoxLayout;
47
48 /* 文本浏览框 */
49 QTextBrowser *textBrowser;
50
51 /* 用于显示本地 ip */
52 QComboBox *comboBox;
53
54 /* 用于选择端口 */
55 QSpinBox *spinBox[2];
56
57 /* 文本输入框 */
58 QLineEdit *lineEdit;
59
60 /* 存储本地的 ip 列表地址 */
61 QList<QHostAddress> IPlist;
62
63 /* 获取本地的所有 ip */
64 void getLocalHostIP();
65
66 private slots:
67 /* 绑定端口 */
68 void bindPort();
69
70 /* 解绑端口 */
71 void unbindPort();
72
73 /* 清除文本框时的内容 */
74 void clearTextBrowser();
75
76 /* 接收到消息 */
77 void receiveMessages();
78
79 /* 发送消息 */
80 void sendMessages();
81
82 /* 广播消息 */
83 void sendBroadcastMessages();
84
85 /* 连接状态改变槽函数 */
86 void socketStateChange(QAbstractSocket::SocketState);
87 };
88 #endif // MAINWINDOW_H

头文件里主要是声明界面用的元素,及一些槽函数。重点是声明 udpSocket。

在源文件“mainwindow.cpp”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 10_udp_unicast_broadcast
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-14
*******************************************************************/
1 #include "mainwindow.h"
2
3 MainWindow::MainWindow(QWidget *parent)
4 : QMainWindow(parent)
5 {
6 /* 设置主窗体的位置与大小 */
7 this->setGeometry(0, 0, 800, 480);
8
9 /* udp 套接字 */
10 udpSocket = new QUdpSocket(this);
11
12 /* 绑定端口按钮 */
13 pushButton[0] = new QPushButton();
14 /* 解绑端口按钮 */
15 pushButton[1] = new QPushButton();
16 /* 清空聊天文本按钮 */
17 pushButton[2] = new QPushButton();
18 /* 发送消息按钮 */
19 pushButton[3] = new QPushButton();
20 /* 广播消息按钮 */
21 pushButton[4] = new QPushButton();
22
23 /* 水平布局一 */
24 hBoxLayout[0] = new QHBoxLayout();
25 /* 水平布局二 */
26 hBoxLayout[1] = new QHBoxLayout();
27 /* 水平布局三 */
28 hBoxLayout[2] = new QHBoxLayout();
29 /* 水平布局四 */
30 hBoxLayout[3] = new QHBoxLayout();
31
32 /* 水平容器一 */
33 hWidget[0] = new QWidget();
34 /* 水平容器二 */
35 hWidget[1] = new QWidget();
36 /* 水平容器三 */
37 hWidget[2] = new QWidget();
38
39
40 vWidget = new QWidget();
41 vBoxLayout = new QVBoxLayout();
42
43 /* 标签实例化 */
44 label[0] = new QLabel();
45 label[1] = new QLabel();
46 label[2] = new QLabel();
47
48 lineEdit = new QLineEdit();
49 comboBox = new QComboBox();
50 spinBox[0] = new QSpinBox();
51 spinBox[1] = new QSpinBox();
52 textBrowser = new QTextBrowser();
53
54 label[0]->setText("目标 IP 地址:");
55 label[1]->setText("目标端口:");
56 label[2]->setText("绑定端口:");
57
58 /* 设置标签根据文本文字大小自适应大小 */
59 label[0]->setSizePolicy(QSizePolicy::Fixed,
60 QSizePolicy::Fixed);
61 label[1]->setSizePolicy(QSizePolicy::Fixed,
62 QSizePolicy::Fixed);
63 label[2]->setSizePolicy(QSizePolicy::Fixed,
64 QSizePolicy::Fixed);
65
66 /* 设置端口号的范围,注意不要与主机的已使用的端口号冲突 */
67 spinBox[0]->setRange(10000, 99999);
68 spinBox[1]->setRange(10000, 99999);
69
70 pushButton[0]->setText("绑定端口");
71 pushButton[1]->setText("解除绑定");
72 pushButton[2]->setText("清空文本");
73 pushButton[3]->setText("发送消息");
74 pushButton[4]->setText("广播消息");
75
76 /* 设置停止监听状态不可用 */
77 pushButton[1]->setEnabled(false);
78
79 /* 设置输入框默认的文本 */
80 lineEdit->setText("您好!");
81
82 /* 水平布局一添加内容 */
83 hBoxLayout[0]->addWidget(pushButton[0]);
84 hBoxLayout[0]->addWidget(pushButton[1]);
85 hBoxLayout[0]->addWidget(pushButton[2]);
86
87 /* 设置水平容器的布局为水平布局一 */
88 hWidget[0]->setLayout(hBoxLayout[0]);
89
90 hBoxLayout[1]->addWidget(label[0]);
91 hBoxLayout[1]->addWidget(comboBox);
92 hBoxLayout[1]->addWidget(label[1]);
93 hBoxLayout[1]->addWidget(spinBox[0]);
94 hBoxLayout[1]->addWidget(label[2]);
95 hBoxLayout[1]->addWidget(spinBox[1]);
96
97 /* 设置水平容器的布局为水平布局二 */
98 hWidget[1]->setLayout(hBoxLayout[1]);
99
100 /* 水平布局三添加内容 */
101 hBoxLayout[2]->addWidget(lineEdit);
102 hBoxLayout[2]->addWidget(pushButton[3]);
103 hBoxLayout[2]->addWidget(pushButton[4]);
104
105 /* 设置水平容器三的布局为水平布局一 */
106 hWidget[2]->setLayout(hBoxLayout[2]);
107
108 /* 垂直布局添加内容 */
109 vBoxLayout->addWidget(textBrowser);
110 vBoxLayout->addWidget(hWidget[1]);
111 vBoxLayout->addWidget(hWidget[0]);
112 vBoxLayout->addWidget(hWidget[2]);
113
114 /* 设置垂直容器的布局为垂直布局 */
115 vWidget->setLayout(vBoxLayout);
116
117 /* 居中显示 */
118 setCentralWidget(vWidget);
119
120 /* 获取本地 ip */
121 getLocalHostIP();
122
123 /* 信号槽连接 */
124 connect(pushButton[0], SIGNAL(clicked()),
125 this, SLOT(bindPort()));
126 connect(pushButton[1], SIGNAL(clicked()),
127 this, SLOT(unbindPort()));
128 connect(pushButton[2], SIGNAL(clicked()),
129 this, SLOT(clearTextBrowser()));
130 connect(pushButton[3], SIGNAL(clicked()),
131 this, SLOT(sendMessages()));
132 connect(pushButton[4], SIGNAL(clicked()),
133 this, SLOT(sendBroadcastMessages()));
134 connect(udpSocket, SIGNAL(readyRead()),
135 this, SLOT(receiveMessages()));
136 connect(udpSocket,
137 SIGNAL(stateChanged(QAbstractSocket::SocketState)),
138 this,
139 SLOT(socketStateChange(QAbstractSocket::SocketState)));
140 }
141
142 MainWindow::~MainWindow()
143 {
144 }
145
146 void MainWindow::bindPort()
147 {
148 quint16 port = spinBox[0]->value();
149
150 /* 绑定端口需要在 socket 的状态为 UnconnectedState */
151 if (udpSocket->state() != QAbstractSocket::UnconnectedState)
152 udpSocket->close();
153
154 if (udpSocket->bind(port)) {
155 textBrowser->append("已经成功绑定端口:"
156 + QString::number(port));
157
158 /* 设置界面中的元素的可用状态 */
159 pushButton[0]->setEnabled(false);
160 pushButton[1]->setEnabled(true);
161 spinBox[1]->setEnabled(false);
162 }
163 }
164
165 void MainWindow::unbindPort()
166 {
167 /* 解绑,不再监听 */
168 udpSocket->abort();
169
170 /* 设置界面中的元素的可用状态 */
171 pushButton[0]->setEnabled(true);
172 pushButton[1]->setEnabled(false);
173 spinBox[1]->setEnabled(true);
174 }
175
176 /* 获取本地 IP */
177 void MainWindow::getLocalHostIP()
178 {
179 // /* 获取主机的名称 */
180 // QString hostName = QHostInfo::localHostName();
181
182 // /* 主机的信息 */
183 // QHostInfo hostInfo = QHostInfo::fromName(hostName);
184
185 // /* ip 列表,addresses 返回 ip 地址列表,注意主机应能从路由器获取到
186 // * IP,否则可能返回空的列表(ubuntu 用此方法只能获取到环回 IP) */
187 // IPlist = hostInfo.addresses();
188 // qDebug()<<IPlist<<endl;
189
190 // /* 遍历 IPlist */
191 // foreach (QHostAddress ip, IPlist) {
192 // if (ip.protocol() == QAbstractSocket::IPv4Protocol)
193 // comboBox->addItem(ip.toString());
194 // }
195
196 /* 获取所有的网络接口,
197 * QNetworkInterface 类提供主机的 IP 地址和网络接口的列表 */
198 QList<QNetworkInterface> list
199 = QNetworkInterface::allInterfaces();
200
201 /* 遍历 list */
202 foreach (QNetworkInterface interface, list) {
203
204 /* QNetworkAddressEntry 类存储 IP 地址子网掩码和广播地址 */
205 QList<QNetworkAddressEntry> entryList
206 = interface.addressEntries();
207
208 /* 遍历 entryList */
209 foreach (QNetworkAddressEntry entry, entryList) {
210 /* 过滤 IPv6 地址,只留下 IPv4 */
211 if (entry.ip().protocol() ==
212 QAbstractSocket::IPv4Protocol) {
213 comboBox->addItem(entry.ip().toString());
214 /* 添加到 IP 列表中 */
215 IPlist<<entry.ip();
216 }
217 }
218 }
219 }
220
221 /* 清除文本浏览框里的内容 */
222 void MainWindow::clearTextBrowser()
223 {
224 /* 清除文本浏览器的内容 */
225 textBrowser->clear();
226 }
227
228 /* 客户端接收消息 */
229 void MainWindow::receiveMessages()
230 {
231 /* 局部变量,用于获取发送者的 IP 和端口 */
232 QHostAddress peerAddr;
233 quint16 peerPort;
234
235 /* 如果有数据已经准备好 */
236 while (udpSocket->hasPendingDatagrams()) {
237 /* udpSocket 发送的数据报是 QByteArray 类型的字节数组 */
238 QByteArray datagram;
239
240 /* 重新定义数组的大小 */
241 datagram.resize(udpSocket->pendingDatagramSize());
242
243 /* 读取数据,并获取发送方的 IP 地址和端口 */
244 udpSocket->readDatagram(datagram.data(),
245 datagram.size(),
246 &peerAddr,
247 &peerPort);
248 /* 转为字符串 */
249 QString str = datagram.data();
250
251 /* 显示信息到文本浏览框窗口 */
252 textBrowser->append("接收来自"
253 + peerAddr.toString()
254 + ":"
255 + QString::number(peerPort)
256 + str);
257 }
258 }
259
260 /* 客户端发送消息 */
261 void MainWindow::sendMessages()
262 {
263 /* 文本浏览框显示发送的信息 */
264 textBrowser->append("发送:" + lineEdit->text());
265
266 /* 要发送的信息,转为 QByteArray 类型字节数组,数据一般少于 512 个字节 */
267 QByteArray data = lineEdit->text().toUtf8();
268
269 /* 要发送的目标 Ip 地址 */
270 QHostAddress peerAddr = IPlist[comboBox->currentIndex()];
271
272 /* 要发送的目标端口号 */
273 quint16 peerPort = spinBox[1]->value();
274
275 /* 发送消息 */
276 udpSocket->writeDatagram(data, peerAddr, peerPort);
277 }
278
279 void MainWindow::sendBroadcastMessages()
280 {
281 /* 文本浏览框显示发送的信息 */
282 textBrowser->append("发送:" + lineEdit->text());
283
284 /* 要发送的信息,转为 QByteArray 类型字节数组,数据一般少于 512 个字节 */
285 QByteArray data = lineEdit->text().toUtf8();
286
287 /* 广播地址,一般为 255.255.255.255,
288 * 同一网段内监听目标端口的程序都会接收到消息 */
289 QHostAddress peerAddr = QHostAddress::Broadcast;
290
291 /* 要发送的目标端口号 */
292 quint16 peerPort = spinBox[1]->text().toInt();
293
294 /* 发送消息 */
295 udpSocket->writeDatagram(data, peerAddr, peerPort);
296 }
297 /* socket 状态改变 */
298 void MainWindow::socketStateChange(QAbstractSocket::SocketState
state)
299 {
300 switch (state) {
301 case QAbstractSocket::UnconnectedState:
302 textBrowser->append("scoket 状态:UnconnectedState");
303 break;
304 case QAbstractSocket::ConnectedState:
305 textBrowser->append("scoket 状态:ConnectedState");
306 break;
307 case QAbstractSocket::ConnectingState:
308 textBrowser->append("scoket 状态:ConnectingState");
309 break;
310 case QAbstractSocket::HostLookupState:
311 textBrowser->append("scoket 状态:HostLookupState");
312 break;
313 case QAbstractSocket::ClosingState:
314 textBrowser->append("scoket 状态:ClosingState");
315 break;
316 case QAbstractSocket::ListeningState:
317 textBrowser->append("scoket 状态:ListeningState");
318 break;
319 case QAbstractSocket::BoundState:
320 textBrowser->append("scoket 状态:BoundState");
321 break;
322 default:
323 break;
324 }
325 }

第 146~163 行,绑定端口。使用 bind 方法,即可绑定一个端口。注意我们绑定的端口不能和主机已经使用的端口冲突!
第 165~174 行,解绑端口。使用 abort 方法即可解绑。
第 229~258 行,接收消息,注意接收消息是 QByteArray 字节数组。读数组使用的是readDatagram 方法,在 readDatagram 方法里可以获取对方的套接字 IP 地址与端口号。
第 261~277 行,单播消息,需要知道目标 IP 与目标端口号。即可用 writeDatagram 方法发送消息。
第 279~296 行,广播消息与单播消息不同的是将目标 IP 地址换成了广播地址,一般广播地址为 255.255.255.255。

2. 程序运行效果

本实例可以做即是发送者,也是接收者。如果在同一台主机同一个系统里运行两个本例程序。不能绑定同一个端口!否则会冲突!当您想测试在同一局域网内不同主机上运行此程序,那么绑定的端口号可以相同。

本例设置目标 IP 地址为 127.0.0.1,此 IP 地址是 Ubuntu/Windows 上的环回 IP 地址,可以用于无网络时测试。绑定端口号与目标端口号相同,也就是说,此程序正在监听端口号为 10000的数据,此程序也向目标 IP 地址 127.0.0.1 的 10000 端口号发送数据,实际上此程序就完成了自发自收。

当我们点击发送消息按钮时,文本消息窗口显示发送的数据“您好!”,同时接收到由本地IP 127.0.0.1 发出的数据“您好!”。其中 ffff:是通信套接字的标识。呵呵!您可能会问为什么不是本主机的其它地址如(192.168.1.x)发出的呢?因为我们选择了目标的 IP 地址为 127.0.0.1,那么要与此目标地址通信,必须使用相同网段的 IP 设备与之通信。注意不能用本地环回发送消息到其他主机上。因为本地环回 IP 只适用于本地主机上的 IP 通信。

当我们点击广播消息按钮时,广播发送的目标 IP 地址变成了广播地址 255.255.255.255。那么我们将收到从本地 IP 地址 192.168.x.x 的数据。如下图,收到了从 192.168.1.129 发送过来的数据。因为环回 IP 127.0.0.1 的广播地址为 255.0.0.0,所以要与255.255.255.255 的网段里的 IP通信数据必须是由 192.168.x.x 上发出的。如果其他同一网段上的其他主机正在监听目标端口,那么它们将同时收到消息。这也验证了上一小节为什么会从 127.0.0.1 发送数据。

本例不难,可能有点绕,大家多参考资料理解理解,知识点有点多,如果没有些通信基础的话,我们需要慢慢吃透。
在这里插入图片描述

3. UDP组播

通常,在传统的网络通讯中,有两种方式,一种是源主机和目标主机两台主机之间进行的“一对一”的通讯方式,即单播,第二种是一台源主机与网络中所有其他主机之间进行的通讯,即广播。那么,如果需要将信息从源主机发送到网络中的多个目标主机,要么采用广播方式,这样网络中所有主机都会收到信息,要么,采用单播方式,由源主机分别向各个不同目标主机发送信息。可以看出来,在广播方式下,信息会发送到不需要该信息的主机从而浪费带宽资源,甚至引起广播风暴:而单播方式下,会因为数据包的多次重复而浪费带宽资源,同时,源主机的负荷会因为多次的数据复制而加大,所以,单播与广播对于多点发送问题有缺陷。在此情况下,组播技术就应用而生了。

组播类似于 QQ 群,如果把腾讯向 QQ 每个用户发送推送消息比作广播,那么组播就像是QQ 群一样,只有群内的用户才能收到消息。想要收到消息,我们得先加群。

一个 D 类 IP 地址的第一个字节必须以“1110”开始,D 类 IP 地址不分网络地址和主机地址,是一个专门保留的地址,其地址范围为 224.0.0.0~239.255.255.255。D 类 IP 地址主要用于多点广播(Multicast,也称为 多播) (组播))之中作为多播组 IP 地址。其中,多播组 IP 地址让源主机能够将分组发送给网络中的一组主机,属于多播组的主机将被分配一个多播组 lP 地址。

由于多播组 lP 地址标识了一组主机(也称为主机组),因此多播组 IP 地址只能作为目标地址,源地址总是为单播地址。

224.0.0.0~224.0.0.255 为预留的组播地址(永久组地址),地址 224.0.0.0 保留不做分配,其它地址供路由协议使用。
224.0.1.0~238.255.255.255 为用户可用的组播地址(临时组地址),全网范围内有效。
239.0.0.0~239.255.255.255 为本地管理组播地址,

通过以上的信息,我们只需要关注,哪些组播地址可以被我们在本地主机使用即可。在家庭网络和办公网络局域网内使用 UDP 组播功能,那么可用的组播地址范围是 239.0.0.0~ ~239.255.255.255。

QUdpSocket 类支持 UDP 组播,提供了 joinMulticastGroup 方法使本地主机加入多播组,
leaveMulticastGroup 离开多播组。其他绑定端口,发送接收功能与 UDP 单播和广播完全一样。实际上我们在上一个实例学会使用 joinMulticastGroup 和 leaveMulticastGroup 的应用即可!

1. 应用实例

本例目的:了解 QUdpSocket 组播使用。

例 11_udp_multicast,UDP 单播与广播应用(难度:一般)。项目路径为Qt/2/11_udp_multicast。本例大体流程首先获取本地 IP 地址。创建一个 udpSocket 套接字,加入组播前必须绑定本机主机的端口。加入组播使用 joinMulticastGroup,退出组播使用 leaveMulticastGroup。其他收发消息的功能与上一节单播和广播一样。

项目文件10_udp_unicast_broadcast.pro 文件第一行添加的代码部分如下。

1 QT += core gui network
2
3 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
4
5 CONFIG += c++11
6
7 # The following define makes your compiler emit warnings if you use
8 # any Qt feature that has been marked deprecated (the exact warnings
9 # depend on your compiler). Please consult the documentation of the
10 # deprecated API in order to know how to port your code away from it.
11 DEFINES += QT_DEPRECATED_WARNINGS
12
13 # You can also make your code fail to compile if it uses deprecated APIs.
14 # In order to do so, uncomment the following line.
15 # You can also select to disable deprecated APIs only up to a certain
version of Qt.
16 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the
APIs deprecated before Qt 6.0.0
17
18 SOURCES += \
19 main.cpp \
20 mainwindow.cpp
21
22 HEADERS += \
23 mainwindow.h
24
25 # Default rules for deployment.
26 qnx: target.path = /tmp/$${TARGET}/bin
27 else: unix:!android: target.path = /opt/$${TARGET}/bin
28 !isEmpty(target.path): INSTALLS += target

在头文件“mainwindow.h”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 10_udp_unicast_broadcast
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-14
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QUdpSocket>
6 #include <QVBoxLayout>
7 #include <QHBoxLayout>
8 #include <QPushButton>
9 #include <QTextBrowser>
10 #include <QLabel>
11 #include <QComboBox>
12 #include <QSpinBox>
13 #include <QHostInfo>
14 #include <QLineEdit>
15 #include <QNetworkInterface>
16 #include <QDebug>
17
18 class MainWindow : public QMainWindow
19 {
20 Q_OBJECT
21
22 public:
23 MainWindow(QWidget *parent = nullptr);
24 ~MainWindow();
25
26 private:
27 /* Udp 通信套接字 */
28 QUdpSocket *udpSocket;
29
30 /* 按钮 */
31 QPushButton *pushButton[4];
32
33 /* 标签文本 */
34 QLabel *label[3];
35
36 /* 水平容器 */
37 QWidget *hWidget[3];
38
39 /* 水平布局 */
40 QHBoxLayout *hBoxLayout[3];
41
42 /* 垂直容器 */
43 QWidget *vWidget;
44
45 /* 垂直布局 */
46 QVBoxLayout *vBoxLayout;
47
48 /* 文本浏览框 */
49 QTextBrowser *textBrowser;
50
51 /* 用于显示本地 ip */
52 QComboBox *comboBox[2];
53
54 /* 用于选择端口 */
55 QSpinBox *spinBox;
56
57 /* 文本输入框 */
58 QLineEdit *lineEdit;
59
60 /* 存储本地的 ip 列表地址 */
61 QList<QHostAddress> IPlist;
62
63 /* 获取本地的所有 ip */
64 void getLocalHostIP();
65
66 private slots:
67 /* 加入组播 */
68 void joinGroup();
69
70 /* 退出组播 */
71 void leaveGroup();
72
73 /* 清除文本框时的内容 */
74 void clearTextBrowser();
75
76 /* 接收到消息 */
77 void receiveMessages();
78
79 /* 组播消息 */
80 void sendMessages();
81
82 /* 连接状态改变槽函数 */
83 void socketStateChange(QAbstractSocket::SocketState);
84 };
85 #endif // MAINWINDOW_H

头文件里主要是声明界面用的元素,及一些槽函数。重点是声明 udpSocket。

在源文件“mainwindow.cpp”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 10_udp_unicast_broadcast
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-14
*******************************************************************/
1 #include "mainwindow.h"
2
3 MainWindow::MainWindow(QWidget *parent)
4 : QMainWindow(parent)
5 {
6 /* 设置主窗体的位置与大小 */
7 this->setGeometry(0, 0, 800, 480);
8
9 /* udp 套接字 */
10 udpSocket = new QUdpSocket(this);
11
12 /* 参数 1 是设置 IP_MULTICAST_TTL 套接字选项允许应用程序主要限制数据包在
Internet 中的生存时间,
13 * 并防止其无限期地循环,数据报跨一个路由会减一,默认值为 1,表示多播仅适用于
本地子网。*/
14 udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,
1);
15
16 /* 加入组播按钮 */
17 pushButton[0] = new QPushButton();
18 /* 退出组播按钮 */
19 pushButton[1] = new QPushButton();
20 /* 清空聊天文本按钮 */
21 pushButton[2] = new QPushButton();
22 /* 组播消息按钮 */
23 pushButton[3] = new QPushButton();
24
25 /* 水平布局一 */
26 hBoxLayout[0] = new QHBoxLayout();
27 /* 水平布局二 */
28 hBoxLayout[1] = new QHBoxLayout();
29 /* 水平布局三 */
30 hBoxLayout[2] = new QHBoxLayout();
31 /* 水平布局四 */
32 hBoxLayout[3] = new QHBoxLayout();
33
34 /* 水平容器一 */
35 hWidget[0] = new QWidget();
36 /* 水平容器二 */
37 hWidget[1] = new QWidget();
38 /* 水平容器三 */
39 hWidget[2] = new QWidget();
40
41
42 vWidget = new QWidget();
43 vBoxLayout = new QVBoxLayout();
44
45 /* 标签实例化 */
46 label[0] = new QLabel();
47 label[1] = new QLabel();
48 label[2] = new QLabel();
49
50 lineEdit = new QLineEdit();
51 comboBox[0] = new QComboBox();
52 comboBox[1] = new QComboBox();
53 spinBox = new QSpinBox();
54 textBrowser = new QTextBrowser();
55
56 label[0]->setText("本地 IP 地址:");
57 label[1]->setText("组播地址:");
58 label[2]->setText("组播端口:");
59
60 /* 设置标签根据文本文字大小自适应大小 */
61 label[0]->setSizePolicy(QSizePolicy::Fixed,
62 QSizePolicy::Fixed);
63 label[1]->setSizePolicy(QSizePolicy::Fixed,
64 QSizePolicy::Fixed);
65 label[2]->setSizePolicy(QSizePolicy::Fixed,
66 QSizePolicy::Fixed);
67
68 /* 设置端口号的范围,注意不要与主机的已使用的端口号冲突 */
69 spinBox->setRange(10000, 99999);
70
71 pushButton[0]->setText("加入组播");
72 pushButton[1]->setText("退出组播");
73 pushButton[2]->setText("清空文本");
74 pushButton[3]->setText("组播消息");
75
76 /* 设置停止监听状态不可用 */
77 pushButton[1]->setEnabled(false);
78
79 /* 设置输入框默认的文本 */
80 lineEdit->setText("您好!");
81
82 /* 默认添加范围内的一个组播地址 */
83 comboBox[1]->addItem("239.255.255.1");
84
85 /* 设置可编辑,用户可自行修改此地址 */
86 comboBox[1]->setEditable(true);
87
88 /* 水平布局一添加内容 */
89 hBoxLayout[0]->addWidget(pushButton[0]);
90 hBoxLayout[0]->addWidget(pushButton[1]);
91 hBoxLayout[0]->addWidget(pushButton[2]);
92
93 /* 设置水平容器的布局为水平布局一 */
94 hWidget[0]->setLayout(hBoxLayout[0]);
95
96 hBoxLayout[1]->addWidget(label[0]);
97 hBoxLayout[1]->addWidget(comboBox[0]);
98 hBoxLayout[1]->addWidget(label[1]);
99 hBoxLayout[1]->addWidget(comboBox[1]);
100 hBoxLayout[1]->addWidget(label[2]);
101 hBoxLayout[1]->addWidget(spinBox);
102
103 /* 设置水平容器的布局为水平布局二 */
104 hWidget[1]->setLayout(hBoxLayout[1]);
105
106 /* 水平布局三添加内容 */
107 hBoxLayout[2]->addWidget(lineEdit);
108 hBoxLayout[2]->addWidget(pushButton[3]);
109
110 /* 设置水平容器三的布局为水平布局一 */
111 hWidget[2]->setLayout(hBoxLayout[2]);
112
113 /* 垂直布局添加内容 */
114 vBoxLayout->addWidget(textBrowser);
115 vBoxLayout->addWidget(hWidget[1]);
116 vBoxLayout->addWidget(hWidget[0]);
117 vBoxLayout->addWidget(hWidget[2]);
118
119 /* 设置垂直容器的布局为垂直布局 */
120 vWidget->setLayout(vBoxLayout);
121
122 /* 居中显示 */
123 setCentralWidget(vWidget);
124
125 /* 获取本地 ip */
126 getLocalHostIP();
127
128 /* 信号槽连接 */
129 connect(pushButton[0], SIGNAL(clicked()),
130 this, SLOT(joinGroup()));
131 connect(pushButton[1], SIGNAL(clicked()),
132 this, SLOT(leaveGroup()));
133 connect(pushButton[2], SIGNAL(clicked()),
134 this, SLOT(clearTextBrowser()));
135 connect(pushButton[3], SIGNAL(clicked()),
136 this, SLOT(sendMessages()));
137 connect(udpSocket, SIGNAL(readyRead()),
138 this, SLOT(receiveMessages()));
139 connect(udpSocket,
140 SIGNAL(stateChanged(QAbstractSocket::SocketState)),
141 this,
142 SLOT(socketStateChange(QAbstractSocket::SocketState)));
143 }
144
145 MainWindow::~MainWindow()
146 {
147 }
148
149 void MainWindow::joinGroup()
150 {
151 /* 获取端口 */
152 quint16 port = spinBox->value();
153 /* 获取组播地址 */
154 QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());
155
156 /* 绑定端口需要在 socket 的状态为 UnconnectedState */
157 if (udpSocket->state() != QAbstractSocket::UnconnectedState)
158 udpSocket->close();
159
160 /* 加入组播前必须先绑定端口 */
161 if (udpSocket->bind(QHostAddress::AnyIPv4,
162 port, QUdpSocket::ShareAddress)) {
163
164 /* 加入组播组,返回结果给 ok 变量 */
165 bool ok = udpSocket->joinMulticastGroup(groupAddr);
166
167 textBrowser->append(ok ? "加入组播成功" : "加入组播失败");
168
169 textBrowser->append("组播地址 IP:"
170 + comboBox[1]->currentText());
171
172 textBrowser->append("绑定端口:"
173 + QString::number(port));
174
175 /* 设置界面中的元素的可用状态 */
176 pushButton[0]->setEnabled(false);
177 pushButton[1]->setEnabled(true);
178 comboBox[1]->setEnabled(false);
179 spinBox->setEnabled(false);
180 }
181 }
182
183 void MainWindow::leaveGroup()
184 {
185 /* 获取组播地址 */
186 QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());
187
188 /* 退出组播 */
189 udpSocket->leaveMulticastGroup(groupAddr);
190
191 /* 解绑,不再监听 */
192 udpSocket->abort();
193
194 /* 设置界面中的元素的可用状态 */
195 pushButton[0]->setEnabled(true);
196 pushButton[1]->setEnabled(false);
197 comboBox[1]->setEnabled(true);
198 spinBox->setEnabled(true);
199 }
200
201 /* 获取本地 IP */
202 void MainWindow::getLocalHostIP()
203 {
204 // /* 获取主机的名称 */
205 // QString hostName = QHostInfo::localHostName();
206
207 // /* 主机的信息 */
208 // QHostInfo hostInfo = QHostInfo::fromName(hostName);
209
210 // /* ip 列表,addresses 返回 ip 地址列表,注意主机应能从路由器获取到
211 // * IP,否则可能返回空的列表(ubuntu 用此方法只能获取到环回 IP) */
212 // IPlist = hostInfo.addresses();
213 // qDebug()<<IPlist<<endl;
214
215 // /* 遍历 IPlist */
216 // foreach (QHostAddress ip, IPlist) {
217 // if (ip.protocol() == QAbstractSocket::IPv4Protocol)
218 // comboBox->addItem(ip.toString());
219 // }
220
221 /* 获取所有的网络接口,
222 * QNetworkInterface 类提供主机的 IP 地址和网络接口的列表 */
223 QList<QNetworkInterface> list
224 = QNetworkInterface::allInterfaces();
225
226 /* 遍历 list */
227 foreach (QNetworkInterface interface, list) {
228
229 /* QNetworkAddressEntry 类存储 IP 地址子网掩码和广播地址 */
230 QList<QNetworkAddressEntry> entryList
231 = interface.addressEntries();
232
233 /* 遍历 entryList */
234 foreach (QNetworkAddressEntry entry, entryList) {
235 /* 过滤 IPv6 地址,只留下 IPv4,并且不需要环回 IP */
236 if (entry.ip().protocol() ==
237 QAbstractSocket::IPv4Protocol &&
238 ! entry.ip().isLoopback()) {
239 /* 添加本地 IP 地址到 comboBox[0] */
240 comboBox[0]->addItem(entry.ip().toString());
241 /* 添加到 IP 列表中 */
242 IPlist<<entry.ip();
243 }
244 }
245 }
246 }
247
248 /* 清除文本浏览框里的内容 */
249 void MainWindow::clearTextBrowser()
250 {
251 /* 清除文本浏览器的内容 */
252 textBrowser->clear();
253 }
254
255 /* 客户端接收消息 */
256 void MainWindow::receiveMessages()
257 {
258 /* 局部变量,用于获取发送者的 IP 和端口 */
259 QHostAddress peerAddr;
260 quint16 peerPort;
261
262 /* 如果有数据已经准备好 */
263 while (udpSocket->hasPendingDatagrams()) {
264 /* udpSocket 发送的数据报是 QByteArray 类型的字节数组 */
265 QByteArray datagram;
266
267 /* 重新定义数组的大小 */
268 datagram.resize(udpSocket->pendingDatagramSize());
269
270 /* 读取数据,并获取发送方的 IP 地址和端口 */
271 udpSocket->readDatagram(datagram.data(),
272 datagram.size(),
273 &peerAddr,
274 &peerPort);
275 /* 转为字符串 */
276 QString str = datagram.data();
277
278 /* 显示信息到文本浏览框窗口 */
279 textBrowser->append("接收来自"
280 + peerAddr.toString()
281 + ":"
282 + QString::number(peerPort)
283 + str);
284 }
285 }
286
287 /* 客户端发送消息 */
288 void MainWindow::sendMessages()
289 {
290 /* 文本浏览框显示发送的信息 */
291 textBrowser->append("发送:" + lineEdit->text());
292
293 /* 要发送的信息,转为 QByteArray 类型字节数组,数据一般少于 512 个字节 */
294 QByteArray data = lineEdit->text().toUtf8();
295
296 /* 要发送的目标 Ip 地址 */
297 QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());
298
299 /* 要发送的目标端口号 */
300 quint16 groupPort = spinBox->value();
301
302 /* 发送消息 */
303 udpSocket->writeDatagram(data, groupAddr, groupPort);
304 }
305
306 /* socket 状态改变 */
307 void MainWindow::socketStateChange(QAbstractSocket::SocketState
state)
308 {
309 switch (state) {
310 case QAbstractSocket::UnconnectedState:
311 textBrowser->append("scoket 状态:UnconnectedState");
312 break;
313 case QAbstractSocket::ConnectedState:
314 textBrowser->append("scoket 状态:ConnectedState");
315 break;
316 case QAbstractSocket::ConnectingState:
317 textBrowser->append("scoket 状态:ConnectingState");
318 break;
319 case QAbstractSocket::HostLookupState:
320 textBrowser->append("scoket 状态:HostLookupState");
321 break;
322 case QAbstractSocket::ClosingState:
323 textBrowser->append("scoket 状态:ClosingState");
324 break;
325 case QAbstractSocket::ListeningState:
326 textBrowser->append("scoket 状态:ListeningState");
327 break;
328 case QAbstractSocket::BoundState:
329 textBrowser->append("scoket 状态:BoundState");
330 break;
331 default:
332 break;
333 }
334 }

第 161~162 行,绑定端口。使用 bind 方法,即可绑定一个端口。注意我们绑定的端口不能和主机已经使用的端口冲突!
第 165 行,使用 joinMulticastGroup 加入组播,QHostAddress::AnyIPv4,是加入 Ipv4 组播的一个接口,所有操作系统都不支持不带接口选择的加入 IPv6 组播组。加入的结果返回给变量ok。组播地址可由用户点击 comboBox[1]控件输入(默认编者已经输入一个地址为239.255.255.1),注意组播地址的范围必须是 239.0.0.0~239.255.255.255 中的一个数。
第 189 行,使用 leaveMulticastGroup 退出组播。
第 192 行,解绑端口。使用 abort 方法即可解绑。
第 256~285 行,接收消息,注意接收消息是 QByteArray 字节数组。读数组使用的是readDatagram 方法,在 readDatagram 方法里可以获取对方的套接字 IP 地址与端口号。
第 288~304 行,发送消息,组播与广播消息或单播消息不同的是将目标 IP 地址换成了 组播址 地址 239.255.255.1。

2. 程序运行效果

运行程序后,点击加入组播,然后点击组播消息,本实例可以做即是发送者,也是接收者。如果在同一台主机同一个系统里运行两个本例程序。不能绑定同一个端口!否则会冲突!当您想测试在同一局域网内不同主机上运行此程序,那么绑定的端口号可以相同。

因为是组播消息,所以自己也会收到消息,如果在局域网内其他主机运行此程序,当点击加入组播后,就可以收发消息了。
在这里插入图片描述

四、网络下载实例

Qt 网 络 模 块 还 提 供 了 直 接 访 问 如 HTTP , FTP 等 网 络 协 议 的 类 , 这 些 类 是QNetworkAccessManager、QNetworkRequest 和 QNetworkReply。

通常需要这三个类协作才能完成一个网络操作。可以用于从网络获取时间,天气和图片等等数据。比如本例需要下载一张图片,大概流程如下。

由 QNetworkRequest 类设置一个 URL 地址发起网络协议请求,QNetworkRequest 类保存要用 QNetworkAccessManager 发送的请求。QNetworkRequest 是网络访问 API 的一部分,是一个持有通过网络发送请求所需信息的类。它包含一个 URL 和一些可用于修改请求的辅助信息。

QNetworkAccessManager 类允许应用程序发送网络请求并接收响应。在QNetworkRequest发起网络请求后,QNetworkAccessManager 负责发送网络请求,创建网络响应。

QNetworkReply类就用于QNetworkAccessManager创建的网络响应。最终由QNetworkReply处理网络响应。它提供了 finished()、readyRead()和 downloadProgress()等信号,可以监测网络响应的执行情况。并且 QNetworkReply 继承于 QIODevice,所以 QNetworkReply 支持流读写,可以直接用 read()和 write 等功能。

1. 应用实例

本例目的:了解 QNetworkAccessManager、QNetworkRequest 和 QNetworkReply 类的使用。

例 12_imagedownload,下载小图片(难度:一般)。项目路径为Qt/2/12_imagedownload。本例大体流程,设置一个下载图片的 URL,通过 networkReply 处理响应后,从流中读取图片的数据,然后保存到本地。

项目文件 12_imagedownload.pro 文件第一行添加的代码部分如下。

1 QT += core gui network
2
3 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
4
5 CONFIG += c++11
6
7 # The following define makes your compiler emit warnings if you use
8 # any Qt feature that has been marked deprecated (the exact warnings
9 # depend on your compiler). Please consult the documentation of the
10 # deprecated API in order to know how to port your code away from it.
11 DEFINES += QT_DEPRECATED_WARNINGS
12
13 # You can also make your code fail to compile if it uses deprecated APIs.
14 # In order to do so, uncomment the following line.
15 # You can also select to disable deprecated APIs only up to a certain
version of Qt.
16 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the
APIs deprecated before Qt 6.0.0
17
18 SOURCES += \
19 main.cpp \
20 mainwindow.cpp
21
22 HEADERS += \
23 mainwindow.h
24
25 # Default rules for deployment.
26 qnx: target.path = /tmp/$${TARGET}/bin
27 else: unix:!android: target.path = /opt/$${TARGET}/bin
28 !isEmpty(target.path): INSTALLS += target

在头文件“mainwindow.h”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 12_imagedownload
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-16
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QNetworkAccessManager>
6 #include <QNetworkReply>
7 #include <QFile>
8 #include <QLabel>
9 #include <QPushButton>
10 #include <QProgressBar>
11 #include <QHBoxLayout>
12 #include <QVBoxLayout>
13 #include <QLineEdit>
14
15 class MainWindow : public QMainWindow
16 {
17 Q_OBJECT
18
19 public:
20 MainWindow(QWidget *parent = nullptr);
21 ~MainWindow();
22 private:
23 /* 网络管理 */
24 QNetworkAccessManager *networkAccessManager;
25
26 /* 标签 */
27 QLabel *label[3];
28
29 /* 按钮 */
30 QPushButton *pushButton;
31
32 /* 下载进度条 */
33 QProgressBar *progressBar;
34
35 /* 水平布局 */
36 QHBoxLayout *hBoxLayout[2];
37
38 /* 垂直布局 */
39 QVBoxLayout *vBoxLayout;
40
41 /* 水平容器 */
42 QWidget *hWidget[2];
43
44 /* 垂直容器 */
45 QWidget *vWidget;
46
47 /* 链接输入框 */
48 QLineEdit *lineEdit;
49
50 private slots:
51 /* 读取数据 */
52 void readyReadData();
53
54 /* 响应完成处理 */
55 void replyFinished();
56
57 /* 下载进度管理 */
58 void imageDownloadProgress(qint64, qint64);
59
60 /* 点击开始下载 */
61 void startDownload();
62
63 /* 响应错误处理函数 */
64 void networkReplyError(QNetworkReply::NetworkError);
65 };
66 #endif // MAINWINDOW_H

头文件里主要是声明界面用的元素,及一些槽函数。重点是声明networkAccessManager。

在源文件“mainwindow.cpp”具体代码如下。

/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 12_imagedownload
* * @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-16
*******************************************************************/
1 #include "mainwindow.h"
2 #include <QMessageBox>
3 #include <QCoreApplication>
4
5 MainWindow::MainWindow(QWidget *parent)
6 : QMainWindow(parent)
7 {
8 /* 设置主窗体的位置与大小 */
9 this->setGeometry(0, 0, 800, 480);
10
11 /* 标签 0, 显示下载的图像 */
12 label[0] = new QLabel();
13 /* 标签 1, 显示 URL 标签 */
14 label[1] = new QLabel();
15 /* 下载进度标签 */
16 label[2] = new QLabel();
17
18 /* 下载图片链接输入框 */
19 lineEdit = new QLineEdit();
20
21 /* 下载按钮 */
22 pushButton = new QPushButton();
23
24 /* 下载进度条 */
25 progressBar = new QProgressBar();
26
27 /* 水平布局 */
28 hBoxLayout[0] = new QHBoxLayout();
29 hBoxLayout[1] = new QHBoxLayout();
30
31 /* 垂直布局 */
32 vBoxLayout = new QVBoxLayout();
33
34 /* 水平容器 */
35 hWidget[0] = new QWidget();
36 hWidget[1] = new QWidget();
37
38 /* 垂直容器 */
39 vWidget = new QWidget();
40
41 label[1]->setText("URL 链接:");
42 label[2]->setText("文件下载进度:");
43
44 pushButton->setText("下载");
45
46 /* 设置下载链接地址 */
47 lineEdit->setText("https://ss0.bdstatic.com/70cFuH"
48 "Sh_Q1YnxGkpoWK1HF6hhy/it/u=42710"
49 "87328,1384669424&fm=11&gp=0.jpg");
50 /* 设置标签的最小显示大小 */
51 label[0]->setMinimumSize(this->width(),
52 this->height() * 0.75);
53
54 /* 根据文本文字大小自适应大小 */
55 label[1]->setSizePolicy(QSizePolicy::Fixed,
56 QSizePolicy::Fixed);
57 label[2]->setSizePolicy(QSizePolicy::Fixed,
58 QSizePolicy::Fixed);
59 pushButton->setSizePolicy(QSizePolicy::Fixed,
60 QSizePolicy::Fixed);
61
62 /* 水平布局 0 添加元素 */
63 hBoxLayout[0]->addWidget(label[1]);
64 hBoxLayout[0]->addWidget(lineEdit);
65 hBoxLayout[0]->addWidget(pushButton);
66
67 /* 设置水平布局 0 为水平容器的布局 0 */
68 hWidget[0]->setLayout(hBoxLayout[0]);
69
70 /* 水平布局 1 添加元素 */
71 hBoxLayout[1]->addWidget(label[2]);
72 hBoxLayout[1]->addWidget(progressBar);
73
74 /* 设置水平布局 1 为水平容器的布局 1 */
75 hWidget[1]->setLayout(hBoxLayout[1]);
76
77 /* 垂直布局添加元素 */
78 vBoxLayout->addWidget(label[0]);
79 vBoxLayout->addWidget(hWidget[0]);
80 vBoxLayout->addWidget(hWidget[1]);
81
82 /* 设置垂直布局为垂直容器的布局 */
83 vWidget->setLayout(vBoxLayout);
84
85 /* 设置居中 */
86 setCentralWidget(vWidget);
87
88 /* 网络管理 */
89 networkAccessManager = new QNetworkAccessManager(this);
90
91 /* 信号槽连接 */
92 connect(pushButton, SIGNAL(clicked()),
93 this, SLOT(startDownload()));
94
95 }
96
97 MainWindow::~MainWindow()
98 {
99 }
100
101 void MainWindow::startDownload()
102 {
103 /* 获取 URL 链接 */
104 QUrl newUrl(QUrl(lineEdit->text()));
105
106 /* 如果下载链接无效,则直接返回 */
107 if (!newUrl.isValid()) {
108 QMessageBox::information(this, "error", "invalid url");
109 return;
110 }
111
112 /* 网络请求 */
113 QNetworkRequest networkRequest;
114
115 /* 设置下载的地址 */
116 networkRequest.setUrl(newUrl);
117
118 /* 网络响应 */
119 QNetworkReply *newReply =
120 networkAccessManager->get(networkRequest);
121
122 /* 信号槽连接 */
123 connect(newReply, SIGNAL(finished()),
124 this, SLOT(replyFinished()));
125 connect(newReply, SIGNAL(readyRead()),
126 this, SLOT(readyReadData()));
127 connect(newReply, SIGNAL(downloadProgress(qint64, qint64)),
128 this, SLOT(imageDownloadProgress(qint64, qint64)));
129 connect(newReply,
130 SIGNAL(error(QNetworkReply::NetworkError)),
131 this,
132 SLOT(networkReplyError(QNetworkReply::NetworkError )));
133 }
134
135 void MainWindow::readyReadData()
136 {
137 /* 设置按钮不可用,防止未完成,再次点击 */
138 pushButton->setEnabled(false);
139
140 /* 获取信号发送者 */
141 QNetworkReply *reply = (QNetworkReply *)sender();
142
143 QFile imageFile;
144 /* 保存到当前路径,名称为"下载的.jpg" */
145 imageFile.setFileName(QCoreApplication::applicationDirPath()
146 + "/下载的.jpg");
147
148 /* 如果此图片已经存在,则删除 */
149 if (imageFile.exists())
150 imageFile.remove();
151
152 /* 读取数据 */
153 QByteArray data = reply->readAll();
154 /* 如果数据为空,返回 */
155 if (data.isEmpty()) {
156 qDebug()<<"data is null, please try it again!"<<endl;
157 return;
158 }
159
160 /* 判断是不是 JPG 格式的图片,如果不是则返回 */
161 if (! (data[0] == (char)0xff
162 && data[1] == (char)0xd8
163 && data[data.size() - 2] == (char)0xff
164 && data[data.size() - 1] == (char)0xd9)) {
165 qDebug()<<"not JPG data, please try it again!"<<endl;
166 return;
167 }
168
169 /* 转为 QPixmap */
170 QPixmap pixmap;
171 pixmap.loadFromData(data);
172 pixmap.save(imageFile.fileName());
173 }
174
175 void MainWindow::replyFinished()
176 {
177 /* 获取信号发送者 */
178 QNetworkReply *reply = (QNetworkReply *)sender();
179
180 /* 防止内存泄漏 */
181 reply->deleteLater();
182
183 /* 判断当前执行程序下的图像是否下载完成 */
184 QFile imageFile(QCoreApplication::applicationDirPath()
185 + "/下载的.jpg");
186 if (imageFile.exists()) {
187 /* 显示下载的图像 */
188 label[0]->setPixmap(QPixmap(imageFile.fileName()));
189 qDebug() <<"已经成功下载,文件路径为:"
190 <<imageFile.fileName()<<endl;
191 } else
192 /* 清空显示 */
193 label[0]->clear();
194
195 /* 设置按钮可用 */
196 pushButton->setEnabled(true);
197 }
198
199 void MainWindow::imageDownloadProgress(qint64 bytes,
200 qint64 totalBytes)
201 {
202 /* 设置进度条的最大值 */
203 progressBar->setMaximum(totalBytes);
204 /* 设置当前值 */
205 progressBar->setValue(bytes);
206 }
207
208 /* 网络响应处理函数 */
209 void MainWindow::networkReplyError(QNetworkReply::NetworkError
210 error)
211 {
212 switch (error) {
213 case QNetworkReply::ConnectionRefusedError:
214 qDebug()<<"远程服务器拒绝连接"<<endl;
215 break;
216 case QNetworkReply::HostNotFoundError:
217 qDebug()<<"找不到远程主机名"<<endl;
218 break;
219 case QNetworkReply::TimeoutError:
220 qDebug()<<"与远程服务器连接超时"<<endl;
221 break;
222 default:
223 break;
224 }
225 }

第 89 行,全局变量 networkAccessManager 实例化。
第 101 行~133 行,首先从单行输入框里获取 URL 链接为 newUrl,判断链接的有效性。然后创建局部变量 networkRequest,设置 networkRequest 请求 URL 为 newUrl。QNetworkReply*newReply = networkAccessManager->get(networkRequest);为最重要的代码,所有响应本次的操作都交给了 newReply。通过信号槽处理对应的操作。
第 135~173 行,这部分代码就是从 newReply 流里读取网络下载的数据。
第 160~167 行,这里编者做了一些处理,从网络下载的数据可能遇到数据丢失或者下载错误的情况。本例是从百度里下载一张 JPG 格式的图片,因为 JPG 图片的判断依据是第一个字节和第二个字节的数据是 0xff 和 0xd8,倒数第一个和倒数第二个字节数据分别是 0xd9 和 0xff。如果都对,那么判断此数据为 JPG 图片数据。然后进行保存,否则则不保存图片。处理数据往往是需要的,我们经常要对下载的数据进行处理。
第 174~197 行,网络响应完成。记得要删除 reply,防止内存泄漏。如果下载成功图片,则显示图片到 label[0]上。
第 209~225 行,网络响应错误处理函数。

2. 程序运行效果

点击下载按钮后,可以看到本次下载的图片已经保存到当前编译输出的路径下,名字叫“下载的.jpg”,并显示到界面上。由于文件下载的速度非常快,所以下载的进度条一下子就变成了100%。若想看见下载进度条下载进度缓慢一些,可以修改本例去下载其他文件,注意不要保存为 jpg 图片了。

在这里插入图片描述

  • 7
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值