QT学习开发笔记(UDP通信)

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 发送数据。

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

在这里插入图片描述

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 编程后的代码

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[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 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。

11.3.3.2 程序运行效果

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

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

在这里插入图片描述

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ManGo CHEN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值