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消息传送有三种模式,分别是单播、广播和组播三种模式。
l 单播(unicast):单播用于两个主机之间的端对端通信,需要知道对方的IP地址与端口。
l 广播(broadcast):广播UDP与单播UDP的区别就是IP地址不同,广播一般使用广播地址255.255.255.255,将消息发送到在同一广播(也就是局域网内同一网段)网络上的每个主机。值得强调的是:本地广播信息是不会被路由器转发。当然这是十分容易理解的,因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。这也是为什么IP协议的设计者故意没有定义互联网范围的广播机制。广播地址通常用于在网络游戏中处于同一本地网络的玩家之间交流状态信息等。其实广播顾名思义,就是想局域网内所有的人说话,但是广播还是要指明接收者的端口号的,因为不可能接受者的所有端口都来收听广播。
l 组播(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”具体代码如下。
头文件里主要是声明界面用的元素,及一些槽函数。重点是声明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地址只能作为目标地址,源地址总是为单播地址。
l 224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用。
l 224.0.1.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效。
l 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”具体代码如下。
头文件里主要是声明界面用的元素,及一些槽函数。重点是声明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 程序运行效果运行程序后,点击加入组播,然后点击组播消息,本实例可以做即是发送者,也是接收者。如果在同一台主机同一个系统里运行两个本例程序。不能绑定同一个端口!否则会冲突!当您想测试在同一局域网内不同主机上运行此程序,那么绑定的端口号可以相同。
因为是组播消息,所以自己也会收到消息,如果在局域网内其他主机运行此程序,当点击加入组播后,就可以收发消息了。