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 程序运行效果

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

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

QT学习开发笔记(UDP通信)相关推荐

  1. Qt学习(六):UDP通信

    知识点 qt中UDP通信 组播 获取编辑器内容,发送到套接字 完整项目github地址: https://github.com/taw19960426/Qt_study/tree/main/QUdpS ...

  2. QT学习开发笔记(项目实战之智能家居物联 UI 界面开发 )

    智能家居物联 UI 界面开发 项目路径为 4/01_smarthome/01_smarthome/01_smarthome.pro,先看项目界面.项目界面如 下,采用暗黑主题设计,结合黄色作为亮色,让 ...

  3. Qt+QtWebApp开发笔记(一):QtWebApp介绍、下载和搭建基础封装http轻量级服务器Demo

    若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/130631547 红胖子网络科技博文大全:开发技术集合( ...

  4. Qt+ECharts开发笔记(五):ECharts的动态排序柱状图介绍、基础使用和Qt封装Demo

    若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/127171413 红胖子(红模仿)的博文大全:开发技术集 ...

  5. Java学习笔记—UDP通信

    一.UDP通信原理 UDP协议(用户数据报协议 User Datagram Protocol)是一种无连接通信协议, 即在数据传输时数据发送端和接收端不建立逻辑链接.因此UDP协议是一种 不可靠的网络 ...

  6. ROS学习小笔记(Topic通信 ,service通信,参数服务器)

    (菜鸡整理的学习笔记,原文链接已经挂上,不小心过来的朋友建议去博主那边学习,谢谢) 原文链接:https://blog.csdn.net/LoongEmbedded/article/details/1 ...

  7. QT学习:基于UDP的网络广播程序

    用户数据报协议(User Data Protocol,UDP)是一种简单轻量级.不可靠.面向数据报.无连接的 传输层协议,可以应用在可靠性不是十分重要的场合,如短消息.广播信息等. 适合应用的情况有以 ...

  8. MPC5748G开发笔记-----CAN通信

    CAN通信 文章目录 CAN通信 前言 一.CAN_PAL配置 二.编写代码 1.采用轮询方式 2.采用中断形式 3.CANoe测试 总结 前言 基于MPC5748G开发板的CAN通信 一.CAN_P ...

  9. Qt学习(五):TCP通信

    知识点 通信套接字和监听套接字 获取编辑器内容,发送到套接字 完整项目github地址: https://github.com/taw19960426/Qt_study/tree/main/QTcpT ...

最新文章

  1. 福利 | 大数据新媒体平台面向清华校内师生开放!
  2. NoSQL开篇——为什么要使用NoSQL
  3. easyPOI基本用法详解
  4. 【java】java 扩展可回调的Future
  5. JavaScript学习-函数
  6. 设计模式(六) : 创建型模式--原型模式
  7. C调用Lua与解决Lua环境问题
  8. Web后端的基础知识
  9. Python爬取全站妹子图片,差点硬盘走火了!
  10. 打印机脱机了怎么恢复打印
  11. 10年外贸人工具:邮件群发软件工具哪个好?
  12. axure 折线图部件_Axure教程:折线图
  13. do vis是什么意思_餐前酒和餐后酒有什么区别
  14. Java job interview:网页设计HTML+CSS前端开发与PS前台美化案例分析
  15. 电子学会图形化一级编程题解析:字母AB点头问好
  16. vue+h5仿微信网页版聊天室vueWebChat项目
  17. Hash(散列)冲突解决 线性探测再散列和二次探测再散列
  18. Linux df命令的使用
  19. QT中Camera相机调用
  20. 统一资源定位器:URL的组成

热门文章

  1. Apache-Plc4x-Modbus-Tcp学习记录(三)
  2. 关于解题的思路与方法
  3. 如何布局私域社群营销?做好这两点就够了
  4. pandas打印某一列_Pandas速查手册中文版
  5. C#--编写旅行社程序
  6. 五.SpringSecurity基础-授权流程
  7. vscode微信小程序插件配置
  8. 聚类算法实践(一)——层次聚类、K-means聚类
  9. UE拖拽UI生成Actor
  10. 达人评测 i9 13900k和i9 12900k差距