今日头条/西瓜视频/抖音短视频 同名:正点原子
原子哥今日头条/西瓜视频/抖音短视频账号:正点原子原子哥
感谢各位的关注和支持,你们的关注和支持是正点原子无限前进的动力。
第十一章《网络编程》
由于本章内容较多,所以第十一章《网络编程》将会分为几个部分进行内容的发布,更多文章内容请持续关注今日头条正点原子官方账号。
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 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”具体代码如下。
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。
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发送数据。
本例不难,可能有点绕,大家多参考资料理解理解,知识点有点多,如果没有些通信基础的话,我们需要慢慢吃透。
未完待续....