文章目录
Qt 网络模块:
-
Qt基础 | 主机信息查询 | QHostInfo的介绍和使用 | QNetworkInterface的介绍和使用
-
Qt基础 | TCP通信 | TCP服务端与客户端程序设计 | QTcpServer | QTcpSocket
-
Qt基础 | UDP通信 | UDP单播、广播、组播的介绍与实现
-
Qt基础 | 基于HTTP协议的网络文件下载
一、QUdpSocket 实现 UDP 通信
1.UDP 通信概述
UDP(用户数据报协议)是轻量的、不可靠的、面向数据报、无连接的协议,它可以用于对可靠性要求不高的场合。与 TCP 通信不同,两个程序之间进行 UDP 通信无需预先建立持久的 socket 连接,UDP 每次发送数据报都需要指定目标地址和端口。
QUdpSocket 类用于实现 UDP 通信,它从 QAbstractSocket 类继承,因而与 QTcpScoket 共享大部分的接口函数。主要区别是 QUdpSocket 以数据报传输数据,而不是以连续的数据流。QUdpSocket 类从 QAbstractSocket 类继承而来,但是又定义了较多新的功能函数用于实现 UDP 特有的一些功能,如数据报读写和多播通信功能。QUdpSocket 没有定义新的信号。
QUdpSocket 的主要功能函数:
发送数据报使用函数 QUdpSocket::writeDatagram()
,数据报的长度一般少于 512 个字节,每个数据报包含发送者和接受者的 IP 地址和端口等信息。
要进行 UDP 数据接收,要用 QUdpSocket::bind()
函数先绑定一个端口,用于接收传入的数据报。当有数据报传入时会发射 readyRead()
信号,在其槽函数中使用 readDatagram()
函数来读取接收到的数据报。
UDP 消息传送有单播、广播、组播三种方式,
-
单播(unicast) 模式:一个 UDP 客户端发出的数据报只发送到另一个指定地址和端口的 UDP 客户端,是一对一的数据传输。
-
广播(broadcast)模式:一个 UDP 客户端发出的数据报,在同一网络范围内其他所有的 UDP 客户端都可以收到。QUdpSocket 支待 IPv4 广播。广播经常用于实现网络发现的协议。要获取广播数据只需在数据报中指定接收端地址为
QHostAddress::Brodcast
,一般的广播地址是 255.255.255.255。 -
组播(multicast)模式:也称为多播。UDP 客户端加入到另一个组播 IP 地址指定的多播组,成员向组播地址发送的数据报,组内成员都可以接收到,类似于 QQ 群的功能。
QUdpSocket::joinMulticastGroup()
函数实现加入多播组的功能, 加入多播组后,UDP 数据的收发与正常的 UDP 数据收发方法一样。
使用广播和多播模式,UDP可以实现一些比较灵活的通信功能,而 TCP 通信只有单播模式,没有广播和多播模式。所以,UDP 通信虽然不能保证数据传输的准确性,但是具有灵活性,一般的即时通信软件都是基于 UDP 通信的。
在单播、广播和多播模式下,UDP 程序都是对等的(即两个实例之间可以进行 UDP 通信,这两个实例可以运行在同一台计算机上,也可以运行在不同的计算机上),不像 TCP 通信那样分为客户端和服务器端。多播和广播的实现方式基本相同,只是数据报的目标 IP 地址设置不同,多播模式需要加入多播组,实现方式有较大差异。
2.UDP 单播和广播
UDP 程序应具有以下功能:
- 可以进行 UDP 数据报的发送和接收
- 两个实例之间可以进行 UDP 通信,这两个实例可以运行在同一台计算机上,也可以运行在不同的计算机上 。
- 在同一台计算机上运行时,两个运行实例需要绑定不同的端口,例如实例 A 绑定端口 1200,实例 B 绑定端口 3355 。实例 A 向实例 B 发送数据报时,需要指定实例 B 所在主机的 IP 地址、绑定端口作为目标地址和目标端口,这样实例 B 才能接收到数据报。
- 在不同计算机上运行时,则可以使用相同的端口,因为 IP 地址不同了,不会导致绑定时发生冲突。一般的 UDP 通信程序都是在不同的计算机上运行的,约定一个固定的端口作为通信端口。
2.1 主窗口类定义和构造函数
主窗口是一个基于 QMainWindow 的应用程序,界面是由 UI 设计器设计,设计如下:
MainWindow 类的定义如下:
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QUdpSocket>
#include <QLabel>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QLabel *LabSocketState;//socket状态显示标签
QUdpSocket *udpSocket;//
QString getLocalIP();//获取本机IP地址
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//自定义槽函数
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onSocketReadyRead();//读取socket传入的数据
//
void on_actStart_triggered();
void on_actStop_triggered();
void on_actClear_triggered();
void on_btnSend_clicked();
void on_actHostInfo_triggered();
void on_btnBroadcast_clicked();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
私有变量:
- udpSocket:用于 UDP 通信的 socket
- LabSocketState:用于显示的 socket 状态标签
槽函数:
-
onSocketReadyRead() 函数
这个槽函数与 udpSocket 的 readyRead() 信号关联,用于读取缓冲区的数据报
-
onSocketStateChange() 函数
这个槽函数与 udpSocket 的 stateChanged() 信号关联,用于显示 udpSocket 当前的状态
构造函数:
构造函数主要完成了 udpSocket 的创建、信号与槽函数的连接。代码如下:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
LabSocketState=new QLabel("Socket状态:");//
LabSocketState->setMinimumWidth(200);
ui->statusBar->addWidget(LabSocketState);
QString localIP=getLocalIP();//本机IP
this->setWindowTitle(this->windowTitle()+"----本机IP:"+localIP);
ui->comboTargetIP->addItem(localIP);
udpSocket=new QUdpSocket(this);//用于与连接的客户端通讯的QTcpSocket
connect(udpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
onSocketStateChange(udpSocket->state());
connect(udpSocket,SIGNAL(readyRead()),
this,SLOT(onSocketReadyRead()));
}
其对应的槽函数如下:
void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{
switch(socketState)
{
case QAbstractSocket::UnconnectedState:
LabSocketState->setText("scoket状态:UnconnectedState");
break;
case QAbstractSocket::HostLookupState:
LabSocketState->setText("scoket状态:HostLookupState");
break;
case QAbstractSocket::ConnectingState:
LabSocketState->setText("scoket状态:ConnectingState");
break;
case QAbstractSocket::ConnectedState:
LabSocketState->setText("scoket状态:ConnectedState");
break;
case QAbstractSocket::BoundState:
LabSocketState->setText("scoket状态:BoundState");
break;
case QAbstractSocket::ClosingState:
LabSocketState->setText("scoket状态:ClosingState");
break;
case QAbstractSocket::ListeningState:
LabSocketState->setText("scoket状态:ListeningState");
}
}
void MainWindow::onSocketReadyRead()
{//读取收到的数据报
while(udpSocket->hasPendingDatagrams())
{
QByteArray datagram;
datagram.resize(udpSocket->pendingDatagramSize());
QHostAddress peerAddr;
quint16 peerPort;
udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
QString str=datagram.data();
QString peer="[From "+peerAddr.toString()+":"+QString::number(peerPort)+"] ";
ui->plainTextEdit->appendPlainText(peer+str);
}
}
2.2 UDP通信实现
要实现 UDP 数据的接收,必须先用 QUdpSocket::bind()
函数绑定一个端口,用于监听传入的数据报,解除绑定则使用 abort()
函数。
绑定端口:
单击 “绑定端口” 按钮绑定端口后,socket 的状态变为 BoundState。
void MainWindow::on_actStart_triggered()
{//绑定端口
quint16 port=ui->spinBindPort->value(); //本机UDP端口
if (udpSocket->bind(port))//绑定端口成功
{
ui->plainTextEdit->appendPlainText("**已成功绑定");
ui->plainTextEdit->appendPlainText("**绑定端口:"
+QString::number(udpSocket->localPort()));
ui->actStart->setEnabled(false);
ui->actStop->setEnabled(true);
}
else
ui->plainTextEdit->appendPlainText("**绑定失败");
}
解除绑定:
单击 “解除绑定”按钮后完成解绑,socket 的状态为 UnconnectedState。
void MainWindow::on_actStop_triggered()
{//解除绑定
udpSocket->abort(); //解除绑定
ui->actStart->setEnabled(true);
ui->actStop->setEnabled(false);
ui->plainTextEdit->appendPlainText("**已解除绑定");
}
发送消息(数据报):
发送点对点消息和广播消息都使用 QUdpSocket::writeDatagram()
函数,使用 writeDatagram() 函数向一个目标用户发送消息时,需要指定目标 IP 地址和目标端口。只不过在广播消息时,需要将目标地址更换为一个特殊地址,即广播地址 QHostAddress::Broadcast
,一般是 255.255.255.255。QUdpSocket 发送的数据报是 QByteArray 类型的字节数组,数据报的长度一般不超过 512 字节。数据报的内容可以是文本字符串,也可以自定义格式的二进制数据,文本字符串无需以换行符结束。
代码如下:
void MainWindow::on_btnSend_clicked()
{//发送消息 按钮
QString targetIP=ui->comboTargetIP->currentText(); //目标IP
QHostAddress targetAddr(targetIP);
quint16 targetPort=ui->spinTargetPort->value();//目标port
QString msg=ui->editMsg->text();//发送的消息内容
QByteArray str=msg.toUtf8();
udpSocket->writeDatagram(str,targetAddr,targetPort); //发出数据报
ui->plainTextEdit->appendPlainText("[out] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
}
void MainWindow::on_btnBroadcast_clicked()
{ //广播消息 按钮
quint16 targetPort=ui->spinTargetPort->value(); //目标端口
QString msg=ui->editMsg->text();
QByteArray str=msg.toUtf8();
udpSocket->writeDatagram(str,QHostAddress::Broadcast,targetPort);
ui->plainTextEdit->appendPlainText("[broadcast] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
}
接收消息(数据报):
QUdpSocket 接收到数据报后发射 readyRead() 信号,在关联的槽函数 onSocketReadyRead() 中调用 readDatagram() 函数来读取缓冲区的数据报。代码如下:
void MainWindow::onSocketReadyRead()
{//读取收到的数据报
while(udpSocket->hasPendingDatagrams())
{
QByteArray datagram;
datagram.resize(udpSocket->pendingDatagramSize());
QHostAddress peerAddr;
quint16 peerPort;
udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
QString str=datagram.data();
QString peer="[From "+peerAddr.toString()+":"+QString::number(peerPort)+"] ";
ui->plainTextEdit->appendPlainText(peer+str);
}
}
-
hasPendingDatagrams() 函数:
表示是否有待读取的传入数据报
-
pendingDatagramSize() 函数
返回待读取数据报的字节数
-
readDatagram() 函数
用于读取数据报的内容,其函数原型为:
qint64 QUdpSocket::readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr)
-
函数说明:
输入参数 data 和 maxSize 是必须的,表示最多读取 maxSize 字节的数据到变量 data 里。 address 和 port 变量是可选的,用于获取数据报来源的地址和端口。上面的代码中 使用了完整的参数形式,从而可以获得数据报来源的地址 peerAddr 和端口 peerPort。 如果无需获取来源地址和端口,可以采用简略形式,即:
udpSocket->readDatagram(datagram.data(),datagram.size());
-
由于只是传输字符串,所以简单地将其转换为字符串即可。如果传输的是自定义格式的字符串或二进制数据,需要对接收到的数据进行解析。
运行结果:绑定的端口号是该实例接收数据报的端口
3.UDP 组播
UDP 组播是主机之间 “一对一组” 的通信模式,当多个客户端加入由一个组播地址定义的多播组之后,客户端向组播地址和端口发送的 UDP 数据报,组内成员都可以接收到,其功能类似于 QQ群组。
组播报文的目的地址使用 D 类 IP 地址,D 类地址不能出现在 IP 报文的源 IP 地址字段。用同一个 IP 多播地址接收多播数据报的所有主机构成一个组,称为多播组。所有的信息接收者都加入到一个组内,并且一旦加入之后,流向组地址的数据报立即开始向接收者传输,组中的所有成员都能接收到数据报。组中的成员是动态的,主机可以在任何时间加入和离开组。所以,采用 UDP 组播必须使用一个组播地址 。组播地址是 D 类 IP 地址,有特定的地址段。多播组可以是永久的也可以是临时的。多播组地址中,有一部分由官方分配,称为永久多播组。永久多播组保持不变的是它的 IP 地址,组中的成员构成可以发生变化。永久多播组中成员的数量可以是任意的,甚至可以为零。那些没有保留下来的供永久多播组使用的 IP 组播地址, 可以被临时多播组利用。
关于组播 IP 地址,有如下一些约定:
-
224.0.0.0~224.0.0.255 为预留的组播地址(永久组地址),地址 224.0.0.0 保留不做分配,其他地址供路由协议使用;
-
224.0.1.0~224.0.1.255 是公用组播地址,可以用 Internet;
-
224.0.2.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 组播的特点是使用组播地址,其他的端口绑定、数据报收发等功能与 UDP 单播完全相同。
UDP 组播程序应具有以下功能:
- 可以发送和接收组播数据报,且在自己主机上发出的数据报,自己也可以接收到
3.1 主窗口类定义和构造函数
主窗口是一个基于 QMainWindow 的应用程序,界面是由 UI 设计器设计,设计如下:
主窗口类定义如下:
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QUdpSocket>
#include <QLabel>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QLabel *LabSocketState;
QUdpSocket *udpSocket;//用于与连接的客户端通讯的QTcpSocket
QHostAddress groupAddress;//组播地址
QString getLocalIP();//获取本机IP地址
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//自定义槽函数
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onSocketReadyRead();//读取socket传入的数据
//
void on_actStart_triggered();
void on_actStop_triggered();
void on_actClear_triggered();
void on_actHostInfo_triggered();
void on_btnMulticast_clicked();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
私有变量:
- udpSocket:用于 UDP 通信的 socket
- LabSocketState:用于显示的 socket 状态标签
- groupAddress:表示记录组播地址
槽函数:
-
onSocketReadyRead() 函数
这个槽函数与 udpSocket 的 readyRead() 信号关联,用于读取缓冲区的数据报
-
onSocketStateChange() 函数
这个槽函数与 udpSocket 的 stateChanged() 信号关联,用于显示 udpSocket 当前的状态
构造函数:
构造函数主要完成了 udpSocket 的创建、信号与槽函数的连接。代码如下:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
LabSocketState=new QLabel("Socket状态:");//
LabSocketState->setMinimumWidth(200);
ui->statusBar->addWidget(LabSocketState);
QString localIP=getLocalIP();//本地主机名
this->setWindowTitle(this->windowTitle()+"----本机IP:"+localIP);
udpSocket=new QUdpSocket(this);//用于与连接的客户端通讯的QTcpSocket
//Multicast路由层次,1表示只在同一局域网内
//组播TTL: 生存时间,每跨1个路由会减1,多播无法跨过大多数路由所以为1
//默认值是1,表示数据包只能在本地的子网中传送。
udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);
// udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,ui->spinTTL->value());
connect(udpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
onSocketStateChange(udpSocket->state());
connect(udpSocket,SIGNAL(readyRead()),
this,SLOT(onSocketReadyRead()));
}
-
使用
QUdpSocket::setSocketOption()
函数,对 socket 进行参数设置udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);
MulticastTtlOption
是 UDP 组播的数据报的生存期,数据报每跨 1 个路由会减 1。缺省值为 1 ,表示多播数据报只能在同一路由下的局域网内传播。
3.2 组播功能的程序实现
要进行 UDP 组播通信, UDP 客户端必须先加入 UDP 多播组,也可以随时退出多播组。加入组播之前,必须先绑定端口,绑定端口的语句是:
udpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress)
使用 joinMulticastGroup()
函数加入多播组,使用 leaveMulticastGroup()
函数退出多播组。
单击 “加入组播” 和 “退出组播” 按钮对应的槽函数:
void MainWindow::on_actStart_triggered()
{//加入组播
QString IP=ui->comboIP->currentText();
groupAddress=QHostAddress(IP);//多播组地址
quint16 groupPort=ui->spinPort->value();//端口
if (udpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress))//先绑定端口
{
udpSocket->joinMulticastGroup(groupAddress); //加入多播组
ui->plainTextEdit->appendPlainText("**加入组播成功");
ui->plainTextEdit->appendPlainText("**组播地址IP:"+IP);
ui->plainTextEdit->appendPlainText("**绑定端口:"+QString::number(groupPort));
ui->actStart->setEnabled(false);
ui->actStop->setEnabled(true);
ui->comboIP->setEnabled(false);
}
else
ui->plainTextEdit->appendPlainText("**绑定端口失败");
}
void MainWindow::on_actStop_triggered()
{//退出组播
udpSocket->leaveMulticastGroup(groupAddress);//退出组播
udpSocket->abort(); //解除绑定
ui->actStart->setEnabled(true);
ui->actStop->setEnabled(false);
ui->comboIP->setEnabled(true);
ui->plainTextEdit->appendPlainText("**已退出组播,解除端口绑定");
}
发送消息(组播消息):
加入多播组后,发送组播数据报是使用 writeDatagram() 函数,只是目标地址使用的是组播地址。
void MainWindow::on_btnMulticast_clicked()
{//发送组播消息
quint16 groupPort=ui->spinPort->value();
QString msg=ui->editMsg->text();
QByteArray datagram=msg.toUtf8();
udpSocket->writeDatagram(datagram,groupAddress,groupPort);
// udpSocket->writeDatagram(datagram.data(),datagram.size(),
// groupAddress,groupPort);
ui->plainTextEdit->appendPlainText("[multicst] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
}
接收消息(组播消息):
在 readyRead() 信号的槽函数里用 readDatagram() 函数来读取数据报。
void MainWindow::onSocketReadyRead()
{//读取数据报
while(udpSocket->hasPendingDatagrams())
{
QByteArray datagram;
datagram.resize(udpSocket->pendingDatagramSize());
QHostAddress peerAddr;
quint16 peerPort;
udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
// udpSocket->readDatagram(datagram.data(),datagram.size());
QString str=datagram.data();
QString peer="[From "+peerAddr.toString()+":"+QString::number(peerPort)+"] ";
ui->plainTextEdit->appendPlainText(peer+str);
}
}