1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
3)对正点原子Linux感兴趣的同学可以加群讨论:935446741
4)关注正点原子公众号,获取最新资料更新
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的帮助文档中找到。下面我们写代码时会使用到相关的函数,有清楚的注释。
本例目的:了解如何通过QHostInfo和QNetworkInterface类获取本地网络所有接口的信息。
例07_networkhostinfo,获取本机网络接口信息(难度:一般)。项目路径为Qt/2/07_networkhostinfo。本例获取本机的网络接口信息,打印在文本浏览框上,点击按钮可直接获取,为了清楚看见是重新获取的过程,本例点击获取本机信息按钮后延时1s去刷新获取的信息。点击另一个清空文本信息按钮可以清空文本浏览框上的文本内容。
项目文件07_networkhostinfo.pro文件第一行添加的代码部分如下。
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”具体代码如下。
mainwindow.h编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 07_networkhostinfo
* @brief mainwindow.h
* @author Deng Zhimao
* @email [email protected]
* @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”具体代码如下。
mainwindow.cpp编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 07_networkhostinfo
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email [email protected]
* @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类型的信息。筛选信息在我们写程序常常需要的。
点击获取本机信息,在文本浏览框内就打印出本机的网络信息(包括了主机名,网卡名,ip地址等)。这里因为过滤掉了IPv6的信息。通常一个网卡有两个ip地址,一个是ipv4,另一个是ipv6的地址。下面的网卡设备lo,是本地回环网卡。另一个ens33是虚拟机的网卡,由VMware虚拟出来的。点击清空文本信息会清空文本浏览框里的网络信息。
11.2 TCP通信
11.2.1 TCP简介
TCP协议(Transmission Control Protocol)全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP通信必须先建立TCP连接,通信端分为客户端和服务端。服务端通过监听某个端口来监听是否有客户端连接到来,如果有连接到来,则建立新的socket连接;客户端通过ip和port连接服务端,当成功建立连接之后,就可进行数据的收发了。需要注意的是,在Qt中,Qt把socket当成输入输出流来对待的,数据的收发是通过read()和write()来进行的,需要与我们常见的send()与recv()进行区分。
TCP客户端与服务端通信示意图如下。
11.2.2 TCP服务端应用实例
本例目的:了解TCP服务端的使用。
例08_tcpserver,TCP服务端(难度:一般)。项目路径为Qt/2/08_tcpserver。本例大体流程首先获取本地IP地址。创建一个tcpSocket套接字,一个tcpServer服务端。点击监听即监听本地的主机IP地址和端口,同时等待服务端的连接。此程序需要结合客户端一起使用。
项目文件08_tcpserver.pro文件第一行添加的代码部分如下。
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”具体代码如下。
mainwindow.h编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 08_tcpserver
* @brief mainwindow.h
* @author Deng Zhimao
* @email [email protected]
* @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”具体代码如下。
mainwindow.cpp编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 08_tcpserver
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email [email protected]
* @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 quint16 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()<
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()进行。
11.2.3 TCP客户端应用实例
本例目的:了解TCP客户的使用。
例09_tcpclient,TCP客户端(难度:一般)。项目路径为Qt/2/09_ tcpclient。本例大体流程:首先获取本地IP地址。创建一个tcpSocket套接字,然后用tcpSocket套接字使用connectToHost函数连接服务端的主机IP地址和端口,即可相互通信。
项目文件08_tcpserver.pro文件第一行添加的代码部分如下。
09_tcpclient.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”具体代码如下。
mainwindow.h编程后的代码
头文件里主要是声明界面用的元素,及一些槽函数。重点是声明tcpSocket。
在源文件“mainwindow.cpp”具体代码如下。
mainwindow.cpp编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 09_tcpclient
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email [email protected]
* @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()<
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()进行。
11.2.4 程序运行效果
开启服务端后,需要选择本地监听的IP地址和监听的端口(特别需要注意,不要选择监听的端口与本地主机的已经使用的端口,所以编者把端口号设置的特别大,查看本地已经使用的端口号可以使用netstat指令。)
启动客户端后,选择需要连接的服务器IP地址和服务器监听的端口。点击连接后就可以相互发送消息了。
注意服务端和客户端都本例都是选择了本地环回IP 127.0.0.1测试。也可以选择本地的其他IP地址进行测试。
TCP服务端:
TCP客户端:
11.3 UDP通信
11.3.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上进行传输的,而广播仅仅在同一局域网上才能进行。
11.3.2 UDP单播与广播
广播UDP与单播UDP的区别就是IP地址不同,所以我们的实例可以写成一个。我们可以这么理解,单播实际上是通信上对应一对一,广播则是一对多(多,这里指广播地址内的所有主机)。
11.3.2.1 应用实例
本例目的:了解QUdpSocket单播和广播使用。
例10_udp_unicast_broadcast,UDP单播与广播应用(难度:一般)。项目路径为Qt/2/10_udp_unicast_broadcast。本例大体流程首先获取本地IP地址。创建一个udpSocket套接字,然后绑定本地主机的端口(也就是监听端口)。我们可以使用QUdpSocket类提供的读写函数readDatagram和writeDatagram,知道目标IP地址和端口,即可完成消息的接收与发送。
项目文件10_udp_unicast_broadcast.pro文件第一行添加的代码部分如下。
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”具体代码如下。
mainwindow.h编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 10_udp_unicast_broadcast
* @brief mainwindow.h
* @author Deng Zhimao
* @email [email protected]
* @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”具体代码如下。
mainwindow.cpp编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 10_udp_unicast_broadcast
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email [email protected]
* @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()<
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。
11.3.2.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发送数据。
本例不难,可能有点绕,大家多参考资料理解理解,知识点有点多,如果没有些通信基础的话,我们需要慢慢吃透。
11.3.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的应用即可!
11.3.3.1 应用实例
本例目的:了解QUdpSocket组播使用。
例11_udp_multicast,UDP单播与广播应用(难度:一般)。项目路径为Qt/2/11_udp_multicast。本例大体流程首先获取本地IP地址。创建一个udpSocket套接字,加入组播前必须绑定本机主机的端口。加入组播使用joinMulticastGroup,退出组播使用leaveMulticastGroup。其他收发消息的功能与上一节单播和广播一样。
项目文件10_udp_unicast_broadcast.pro文件第一行添加的代码部分如下。
11_udp_multicast.pro编程后的代码
在头文件“mainwindow.h”具体代码如下。
mainwindow.h编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 10_udp_unicast_broadcast
* @brief mainwindow.h
* @author Deng Zhimao
* @email [email protected]
* @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”具体代码如下。
mainwindow.cpp编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 10_udp_unicast_broadcast
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email [email protected]
* @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()<
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。
11.3.3.2 程序运行效果
运行程序后,点击加入组播,然后点击组播消息,本实例可以做即是发送者,也是接收者。如果在同一台主机同一个系统里运行两个本例程序。不能绑定同一个端口!否则会冲突!当您想测试在同一局域网内不同主机上运行此程序,那么绑定的端口号可以相同。
因为是组播消息,所以自己也会收到消息,如果在局域网内其他主机运行此程序,当点击加入组播后,就可以收发消息了。
11.4 网络下载实例
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等功能。
11.4.1 应用实例
本例目的:了解QNetworkAccessManager、QNetworkRequest和QNetworkReply类的使用。
例12_imagedownload,下载小图片(难度:一般)。项目路径为Qt/2/12_imagedownload。本例大体流程,设置一个下载图片的URL,通过networkReply处理响应后,从流中读取图片的数据,然后保存到本地。
项目文件12_imagedownload.pro文件第一行添加的代码部分如下。
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”具体代码如下。
mainwindow.h编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 12_imagedownload
* @brief mainwindow.h
* @author Deng Zhimao
* @email [email protected]
* @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”具体代码如下。
mainwindow.cpp编程后的代码
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 12_imagedownload
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email [email protected]
* @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行,网络响应错误处理函数。
点击下载按钮后,可以看到本次下载的图片已经保存到当前编译输出的路径下,名字叫“下载的.jpg”,并显示到界面上。由于文件下载的速度非常快,所以下载的进度条一下子就变成了100%。若想看见下载进度条下载进度缓慢一些,可以修改本例去下载其他文件,注意不要保存为jpg图片了。注意:此程序里的下载链接可能失效,请替换自己的图片链接。