下图简单表示了组播的原理。UDP 组播是主机之间“一对一组”
的通信模式,当多个客户端加入由一个组播地址定义的多播组之后,客户端向组播地址和端口发送的 UDP 数据报,组内成员都可以接收到,其功能类似于 QQ 群。
组播报文的目的地址使用 D 类 IP 地址,D 类地址不能出现在 IP 报文的源 IP 地址字段。用同一个 IP 多播地址接收多播数据报的所有主机构成了一个组,称为多播组 (或组播组)。所有的信息接收者都加入到一个组内,并且一旦加入之后,流向组地址的数据报立即开始向接收者传输,组中的所有成员都能接收到数据报。组中的成员是动态的,主机可以在任何时间加入和离开组。
所以,采用 UDP 组播必须使用一个组播地址。组播地址是 D 类IP 地址,有特定的地址段。多播组可以是永久的也可以是临时的。多播组地址中,有一部分由官方分配,称为永久多播组。永久多播组保持不变的是它的 IP 地址,组中的成员构成可以发生变化。永久多播组中成员的数量可以是任意的,甚至可以为零。那些没有保留下来的供永久多播组使用的 IP 组播地址,可以被临时多播组利用。关于组播IP 地址,有如下的一些约定:
所以,若是在家庭或办公室局域网内测试 UDP 组播功能,可以使用的组播地址范围是239.0.0.0~239.255.255.255。
QUdpSocket 支持 UDP 组播,joinMulticastGroup()函数使主机加入一个多播组,leaveMulticastGroup()函数使主机离开一个多播组,UDP 组播的特点是使用组播地址,其他的端口绑定、数据报收发等功能的实现与单播 UDP 完全相同。
设计一个UDP 组播实例程序 Samp14_4,在两台计算机上分别运行,进行组播通信。图 14-10是运行于主机 192.168.1.104 上的程序,图 14-11 是运行于主机 192.168.1.106 上的程序。两个主机上的程序都加入地址为239.255.43.21的多播组,绑定端口 35320进行通信。
从图 14-10 和图14-11可以看到,两个 Samp14_4 程序都可以发送和接收组播数据报,且在自已主机上发出的数据报,自己也可以接收到。
程序的主窗口是基于 QMainWindow 的类 MainWindow,界面由 UI 设计器设计,其类定义如下(忽略 UI 设计器生成的槽函数):
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
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传入的数据
...
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
其中定义了一个QHostAddress 类型变量 groupAddress,用于记录组播地址。下面是 MainWindow的构造函数的代码:
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);
将 socket 的 QAbstractSocket::MulticastTtlOption 值设置为1。MulticastTtlOption 是 UDP组播的数据报的生存期,数据报每跨1个路由会减1。缺省值为 1,表示多播数据报只能在同一路由下的局域网内传播。
要进行UDP 组播通信,UDP 客户端必须先加入UDP 多播组,也可以随时退出多播组。主窗口上的“加入组播”和“退出组播”按钮的代码如下:
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("**已退出组播,解除端口绑定");
}
加入组播之前,必须先绑定端口,绑定端口的语句是:
udpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress)
这里指定地址为QHostAddress::AnyIPv4,端口为多播组统一的一个端口。
使用 QUdpSocket:: joinMulticastGroup()函数加入多播组,即:
udpSocket->joinMulticastGroup(groupAddress); //加入多播组
多播组地址 groupAddress 由界面上的组合框里输入。注意,局域网内的组播地址的范围239.0.0.0~239.255.255.255,绝对不能使用本机地址作为组播地址。
退出多播组,使用 QUdpSocket::leaveMulticastGroup()函数,即:
udpSocket->leaveMulticastGroup(groupAddress);//退出组播
加入多播组后,发送组播数据报也是使用 writeDatagram()函数,只是目标地址使用的是组播地址,在 readyRead()信号的槽函数里用 readDatagram()读取数据报。下面是发送和读取数据报的代码:
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();
}
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);
}
}
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
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
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
QString MainWindow::getLocalIP()
{
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;
}
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()));
}
MainWindow::~MainWindow()
{
udpSocket->abort();
delete udpSocket;
delete ui;
}
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);
// udpSocket->readDatagram(datagram.data(),datagram.size());
QString str=datagram.data();
QString peer="[From "+peerAddr.toString()+":"+QString::number(peerPort)+"] ";
ui->plainTextEdit->appendPlainText(peer+str);
}
}
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("**已退出组播,解除端口绑定");
}
void MainWindow::on_actClear_triggered()
{
ui->plainTextEdit->clear();
}
void MainWindow::on_actHostInfo_triggered()
{
QString hostName=QHostInfo::localHostName();//本地主机名
ui->plainTextEdit->appendPlainText("本机主机名:"+hostName+"\n");
QHostInfo hostInfo=QHostInfo::fromName(hostName);
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())
{
QString IP=aHost.toString();
ui->plainTextEdit->appendPlainText("本机IP地址:"+aHost.toString());
if (ui->comboIP->findText(IP)<0)
ui->comboIP->addItem(IP);
}
}
}
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();
}