UDP 通信
简介
UDP概述
-
UDP(User Datagram Protocol)是一个轻量级、无连接且不可靠的面向数据报的协议
-
由于其快速响应和资源消耗少,适用于文字聊天、音视频通信等要求快速响应但丢包影响不大的场景
UDP的应用
-
QQ聊天使用UDP发送文字内容,因其用户量大、对实时性要求高,但安全性要求较低
-
传输文件时,QQ则使用TCP协议以保证传输的可靠性
UDP的优缺点
-
优点:资源消耗小、处理速度快
-
缺点:不可靠,在网络质量差的情况下数据包丢失严重
QUdpSocket类
-
QUdpSocket是QAbstractSocket的子类,提供UDP数据报的发送和接收功能
-
常用方法包括bind()绑定地址和端口、writeDatagram()发送数据、readDatagram()和receiveDatagram()接收数据
数据传输和信号
-
发送数据通常少于512字节,超过512字节的数据会在IP层被分片
-
调用connectToHost()可以使用标准的QIODevice函数进行数据传输
-
readyRead()信号在数据报到达时发出,需及时读取数据报
UDP通信模式
-
单播(unicast):用于两个主机之间的端对端通信,需要指定对方的IP地址和端口
-
广播(broadcast):发送消息到同一局域网内的所有主机,使用广播地址255.255.255.255
-
组播(multicast):将同一业务类型的主机进行逻辑分组,只在组内进行数据传输,避免影响其他主机
注意事项
-
广播仅在局域网内有效,不会被路由器转发
-
单播和组播可在广域网(Internet)上进行传输,但广播仅限于局域网内
UDP 单播与广播
简述
-
单播和广播UDP可以通过相同的程序结构实现,区别仅在于发送数据时使用的IP地址不同
-
在编写代码时,可以根据需要选择单播或广播模式,通过设置不同的目标IP地址来实现
应用实例
-
大致流程
-
获取本地 IP 地址
- 首先需要获取设备的本地 IP 地址,这是进行网络通信的基础,确保设备能够在本地网络中被识别和访问
-
创建 UDP 套接字
- 使用 QUdpSocket 类创建一个 UDP 套接字。UDP(用户数据报协议)是一种无连接的传输层协议,适用于需要快速传输而可以容忍少量丢失的数据场景
-
绑定本地主机的端口
- 将创建的 UDP 套接字绑定到本地主机的特定端口上,这个端口将用于监听传入的数据报
-
使用读写函数进行数据交互
- 通过 QUdpSocket 类提供的 readDatagram 和 writeDatagram 函数,实现数据的接收和发送。这些函数需要知道目标 IP 地址和端口,以确保数据能够正确地发送到指定的目的地
-
完成消息的接收与发送
- 利用上述步骤和函数,可以实现消息的接收和发送,确保网络通信的顺利进行
-
-
项目文件文件第一行添加的代码部分如下
- 1 QT += core gui network
-
mainwindow.h
-
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
10 #include
11 #include
12 #include
13 #include
14 #include
15 #include
16 #include
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 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
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 //
194 // }
195
comboBox->addItem(ip.toString());
196 / 获取所有的网络接口,
197 * QNetworkInterface 类提供主机的 IP 地址和网络接口的列表 /
198 QList list
199 = QNetworkInterface::allInterfaces();
200
201 / 遍历 list /
202 foreach (QNetworkInterface interface, list) {
203
204 / QNetworkAddressEntry 类存储 IP 地址子网掩码和广播地址 /
205 QList 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
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 方法发送消息到指定的目标 IP 和端口
- 广播消息(第 279~296 行)
- 与单播消息的区别在于目标 IP 地址为广播地址
- 一般的广播地址为 255.255.255.255
- 使用 writeDatagram 方法将消息发送到广播地址
程序运行效果
-
程序角色与端口绑定
-
既可以作为发送者也可以作为接收者
-
在同一台主机上运行两个本例程序时,不能绑定同一个端口,否则会导致冲突
-
在同一局域网内不同主机上运行此程序时,可以绑定相同的端口号
-
-
环回地址测试
-
设置目标 IP 地址为 127.0.0.1,这是 Ubuntu/Windows 上的环回 IP 地址,适用于无网络时的测试
-
绑定端口号与目标端口号相同,例如都为 10000,实现自发自收
-
-
发送与接收消息
-
点击发送消息按钮时,文本消息窗口显示发送的数据“您好!”,同时接收到由本地 IP 127.0.0.1 发出的相同数据
-
通信套接字的标识为 ffff:
-
选择目标 IP 地址为 127.0.0.1,因此通信必须使用相同网段的 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 发出
-
同一网段上的其他主机如果正在监听目标端口,它们将同时收到广播消息
-
UDP 组播
概述
-
网络通讯方式
-
传统网络通讯有两种方式:单播(一对一通讯)和广播(一台源主机与网络中所有其他主机通讯)
-
单播和广播在多点发送时存在缺陷,如广播可能引起带宽浪费和广播风暴,单播则可能导致带宽和源主机负荷的浪费
-
-
组播技术的引入
-
组播技术类似于 QQ 群,只有加入群的用户才能收到消息,解决了单播和广播的缺陷
-
组播使用 D 类 IP 地址(224.0.0.0~239.255.255.255),这些地址用于多点广播,标识一组主机
-
-
D 类 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 类支持
-
QUdpSocket 类支持 UDP 组播,提供了 joinMulticastGroup 和 leaveMulticastGroup 方法来加入和离开多播组
-
绑定端口、发送接收功能与 UDP 单播和广播相同,只需学会使用 joinMulticastGroup 和 leaveMulticastGroup 方法即可
-
应用实例
-
大致流程
-
获取本地 IP 地址
- 首先需要获取本地 IP 地址,这一步是初始化网络通讯的基础
-
创建 udpSocket 套接字
- 创建一个 udpSocket 套接字,用于进行 UDP 通讯
-
绑定本机主机的端口
- 在加入组播之前,必须先绑定本机主机的端口。绑定端口是为了能够接收和发送数据
-
加入组播
- 使用 joinMulticastGroup 方法将本地主机加入指定的多播组
-
退出组播
- 使用 leaveMulticastGroup 方法将本地主机从多播组中移除
-
收发消息功能
- 其他消息的收发功能与上一节的单播和广播相同。可以使用 readDatagram 方法接收数据和 writeDatagram 方法发送数据
-
-
项目文件文件第一行添加的代码部分如下
- 1 QT += core gui network
-
mainwindow.h
-
165
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 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
10 #include
11 #include
12 #include
13 #include
14 #include
15 #include
16 #include
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 IPlist;
62
63 / 获取本地的所有 ip */
64 void getLocalHostIP(); -
主要是声明界面用的元素,及一些槽函数。重点是声明 udpSocket
-
-
mainwindow.cpp
-
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 list
224 = QNetworkInterface::allInterfaces();
225
226 / 遍历 list /
227 foreach (QNetworkInterface interface, list) {
228
229 / QNetworkAddressEntry 类存储 IP 地址子网掩码和广播地址 /
230 QList 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
-
程序运行效果
-
运行程序
-
运行程序后,用户需要点击“加入组播”按钮,然后点击“组播消息”按钮
-
可以作为消息的发送者,也可以作为消息的接收者
-
-
在同一台主机上运行多个实例
- 如果在同一台主机上运行两个实例程序,不能绑定同一个端口,否则会产生冲突
-
在同一局域网内不同主机上运行
- 如果在同一局域网内的不同主机上运行此程序,绑定的端口号可以相同
-
组播消息的接收
- 由于是组播消息,因此发送消息的主机也会接收到自己发送的消息
-
局域网内其他主机的通信
- 如果在局域网内的其他主机上运行此程序,当点击“加入组播”按钮后,这些主机也可以收发组播消息