1 前言
上文因为篇幅和组播特性的问题,我只写了单播和广播的实例程序,所以这篇主打UDP组播的程序实现和相关讲解。
公众号:Qt实战,各种开源作品、经验整理、项目实战技巧,专注Qt/C++软件开发,视频监控、物联网、工业控制、嵌入式软件、国产化系统应用软件开发。
公众号:Qt入门和进阶,专门介绍Qt/C++相关知识点学习,帮助Qt开发者更好的深入学习Qt。多位Qt元婴期大神,一步步带你从入门到进阶,走上财务自由之路。
官方店:https://shop114595942.taobao.com//
2 组播的特性
在上一篇文章中,给出了组播示意图,简单表示了组播的原理。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.255255255为本地管理组播地址,仅在特定的本地范围内有效。
所以,若是在家庭或办公室局域网内测试UDP组播功能,可以使用的组播地址范围是239.0.0.0 ~ 239.255.255.255。
QUdpSocket支持UDP组播, joinMulticastGroup(函数使主机加入一个 多播组,leaveMulticastGroup() 函数使主机离开一个多播组,UDP组播的特点是使用组播地址,其他的端口绑定、数据报收发等功能的实现与单播UDP完全相同。
3 UDP组播实例程序讲解
设计一个UDP组播实例程序 ,是基于前面UDP单播和广播上稍微改动的,这个和前面不同,不能再同一台机器上面跑多个实例后绑定端口和地址然后通信,组播需要在不同的ip地址的电脑上面玩,因为必须绑定的端口和地址是一致的,所以这边演示就会有点问题,只能靠各位大大自己去实践延时了,我只能跑通代码,如下图所示:
设计的程序是可以发送和接收组播数据报,且在自己主机上发出的数据报,自己也可以接收到。
4 组播功能的程序实现
还是和之前一样程序的主窗口是基于QWidget的类UDP,界面由UI设计器设计,其类定义如下(忽略UI设计器生成的槽函数):
class UDP : public QWidget
{
Q_OBJECT
public:
UDP(QWidget *parent = nullptr);
~UDP();
void initDataSlot();
QString getLocalIP();
private slots:
//自定义槽函数
void onSocketStateChange(QAbstractSocket::SocketState socketState) ;
void onSocketReadyRead() ; //读取socket传入的数据
void on_pushButton_JoinGroupCast_clicked();
void on_pushButton_GroupCast_clicked();
void on_pushButton_CleanMsgBox_clicked();
private:
Ui::UDP *ui;
QUdpSocket * mUdpSocket = nullptr;
bool mUdpisJoinGroupCast= false; //udp是否已经绑定端口
QHostAddress mGroupAddress; //组播地址
};
#endif // UDP_H
其中定义了一个QHostAddress类型变量mGroupAddress,用于记录组播地址。下面是UDP的构造函数的代码:
UDP::UDP(QWidget *parent)
: QWidget(parent)
, ui(new Ui::UDP)
{
ui->setupUi(this);
initDataSlot();
}
UDP::~UDP()
{
delete ui;
}
void UDP::initDataSlot()
{
if(mUdpSocket == nullptr)
{
mUdpSocket = new QUdpSocket(this);
}
mUdpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 1) ;
ui->label_socketState->setText("Socket 状态:");
QString localIP=getLocalIP();//本机 IP
this->setWindowTitle (this->windowTitle() +"----本机IP: "+localIP) ;
//随机4个局域网范围IP
QStringList iPList = {"239.0.0.0", "239.0.1.1", "240.0.0.0", "241.0.10.1"};
ui->comboIP->addItems(iPList);
connect(mUdpSocket, &QUdpSocket::stateChanged, this, &UDP::onSocketStateChange);
connect(mUdpSocket, &QUdpSocket::readyRead, this, &UDP::onSocketReadyRead);
}
QString UDP::getLocalIP()
{
//获取本机IPv4地址
QString hostName=QHostInfo::localHostName() ;//本地主机名
QHostInfo hostInfo=QHostInfo::fromName (hostName) ;
QString localIP="";
QList<QHostAddress> addList=hostInfo.addresses () ;
if (!addList. isEmpty())
{
for (int i=0; i <addList.count() ;i++)
{
QHostAddress aHost=addList.at(i) ;
if (QAbstractSocket::IPv4Protocol == aHost. protocol())
{
localIP=aHost. toString() ;
break;
}
}
}
return localIP;
}
其中使用了QUdpSocket::setSocketOption()函数, 对socket 进行参数设置。
将socket的QAbstractSocket::MulticastTtlOption值设置为1。MulticastTtlOption 是UDP组播的数据报的生存期,数据报每跨1个路由会减1。缺省值为1,表示多播数据报只能在同一路由下的局域网内传播。
要进行UDP组播通信,UDP客户端必须先加入UDP多播组,也可以随时退出多播组。主窗口上的加入组播和退出组播按钮的代码如下:
void UDP::on_pushButton_JoinGroupCast_clicked()
{
if(mUdpisJoinGroupCast)
{
//退出组播
mUdpSocket->leaveMulticastGroup (mGroupAddress) ; //退出组播
mUdpSocket->abort(); //解除绑定
ui ->comboIP->setEnabled(true) ;
ui->plainTextEdit->appendPlainText ("**已退出组播,解除端口绑定");
ui->pushButton_JoinGroupCast->setText("加入组播");
}
else
{
//加入组播
QString IP=ui ->comboIP->currentText() ;
mGroupAddress=QHostAddress (IP) ;//多播组地址
quint16 groupPort=ui->spinBox_Port->value();//端口
if(mUdpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress))
{
mUdpisJoinGroupCast = true;
mUdpSocket->joinMulticastGroup (mGroupAddress); //加入多 播组
ui->plainTextEdit->appendPlainText ("**加入组播成功") ;
ui->plainTextEdit->appendPlainText ("**组播地址IP: "+IP) ;
ui->plainTextEdit->appendPlainText ("**绑定端口: "+ QString::number(groupPort)) ;
ui->comboIP->setEnabled(false) ;
ui->pushButton_JoinGroupCast->setText("退出组播");
}
else
{
ui->plainTextEdit->appendPlainText ("**加入组播失败") ;
ui->plainTextEdit->appendPlainText ("**绑定端口失败") ;
}
}
}
加入组播之前,必须先绑定端口,绑定端口的语句是:
mUdpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress)
这里指定地址为QHostAddes::AnyIPv4,端口为多播组统一的一个端口。
使用 QUdpSocket: joinMulticastGroup() 函数加入多播组,即:
mUdpSocket->joinMulticastGroup(mGroupAddress);
多播组地址groupAddress由界面上的组合框里输入。注意,局域网内的组播地址的范围是239.0.0.0~ 239.255.255.255,绝对不能使用本机地址作为组播地址。
退出多播组,使用QUdpSocket:leaveMulticastGroup()函数,即:
mUdpSocket->leaveMulticastGroup(groupAddress);
加入多播组后,发送组播数据报也是使用 writeDatagram() 函数, 只是目标地址使用的是组播地址,在 readyRead() 信号的槽函数里用 readDatagram() 读取数据报。下面是发送和读取数据报的代码:
void UDP::on_pushButton_GroupCast_clicked()
{
//发送组播消息
quint16 groupPort=ui->spinBox_Port->value() ;
QString msg=ui ->lineEdit_editMsg->text () ;
QByteArray datagram=msg. toUtf8() ;
mUdpSocket->writeDatagram (datagram, mGroupAddress, groupPort) ;
ui->plainTextEdit->appendPlainText (" [multicst] "+msg) ;
ui->lineEdit_editMsg->clear() ;
ui->lineEdit_editMsg->setFocus() ;
}
void UDP::onSocketReadyRead()
{
//读取数据报
while(mUdpSocket->hasPendingDatagrams())
{
QByteArray datagram;
datagram.resize(mUdpSocket->pendingDatagramSize()) ;
QHostAddress peerAddr;
quint16 peerPort;
mUdpSocket->readDatagram (datagram.data(), datagram.size(), &peerAddr, &peerPort) ;
QString str=datagram.data() ;
QString peer=" [From "+peerAddr.toString()+":" +QString::number (peerPort)+"] ";
ui ->plainTextEdit->appendPlainText(peer+str) ;
}
}