1 前言
在前面讲解了UDP的基本概念后,还是要通过实例代码来全新过一遍才是真正的走通,实践出真知嘛,所以下面会用一个实例来分别讲解单播、广播、组播的区别和特点,其实,一般用UDP也就是单播用的对,不过多了解必有用处!
公众号:Qt实战,各种开源作品、经验整理、项目实战技巧,专注Qt/C++软件开发,视频监控、物联网、工业控制、嵌入式软件、国产化系统应用软件开发。
公众号:Qt入门和进阶,专门介绍Qt/C++相关知识点学习,帮助Qt开发者更好的深入学习Qt。多位Qt元婴期大神,一步步带你从入门到进阶,走上财务自由之路。
官方店:https://shop114595942.taobao.com//
2 UDP通信实例程序功能
老规矩,先新建一个UDP项目,如下图:
项目名称就设为UDP,点击下一步生成项目,设计简单的界面构造,运行实例如下:
实例程序UDP 实现单播、广播、组播功能,其主窗口是继承自QWidget的类,界面用UI设计器设计。程序可以进行UDP数据报的发送和接收,启动同样两个实例,他们之间可以进行UDP通信,这两个实例可以运行在同一台计算机上,也可以运行在不同的计算机上。
在同一台计算机上运行时,两个运行实例需要绑定不同的端口,例如实例A绑定端口1001,实例B绑定端口1002。实例A向实例B 发送数据 报时,需要指定实例B所在主机的IP地址、绑定端口作为目标地址和目标端口,这样实例B才能 接收到数据报。
如果两个实例在不同计算机上运行,则可以使用相同的端口,因为IP地址不同了,不会导致绑定时发生冲突。一般的UDP通信程序都是在不同的计算机上运行的,约定一个固定的端口作为通信端口。
3 实例窗口类定义代码
主窗口是基于 QWidget 的类UDP,界面采用UI设计器设计。UDP 类的定义如下(省略了UI设计器为actions和按钮生成的槽函数声明):
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传入的数据
private:
Ui::UDP *ui;
QUdpSocket * mUdpSocket = nullptr;
bool mUdpisBind= false; //udp是否已经绑定端口
};
#endif // UDP_H
QUdpSocket 类型的私有变量 mUdpSocket 是用于UDP通信的socket。
定义了两个自定义槽函数,onSocketStateChange() 与mUdpSocket 的stateChange() 信号关联,用于显示 mUdpSocket 当前的状态; onSocketReadyRead() 信号与udpSocket的 readyRead() 信号关联,用于读取缓冲区的数据报。
UDP 的构造函数主要完成mUdpSocket 的创建、信号与槽函数的关联,代码如下:
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);
}
ui->label_socketState->setText("Socket 状态:");
QString localIP=getLocalIP();//本机 IP
this->setWindowTitle (this->windowTitle() +"----本机IP: "+localIP) ;
ui ->comboTargetIP->addItem(localIP) ;
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;
}
槽函数 onSocketStateChange() 的功能与上一章TCP通信程序里的完全一样, 不再显示其具体代码。
4 UDP通信的实现
要实现UDP数据的接收,必须先用 QUdpSocket::bind() 函数绑定一个端口, 用于监听传入的数据报,解除绑定则使用 abort() 函数。程序主窗口,上的 绑定端口和解除绑定 按钮的响应代码如下:
void UDP::on_pushButton_bindingPort_clicked()
{
if(mUdpisBind)
{
mUdpisBind = false;
//解除绑定
mUdpSocket->abort() ; //解除绑定
ui->plainTextEdit->appendPlainText("**已解除绑定") ;
ui->pushButton_bindingPort->setText("绑定端口");
}
else
{
//绑定端口
quint16 port =ui->spinBox_bindingPort->value(); //本机UDP端口
if (mUdpSocket->bind (port)) //绑定端口成功
{
mUdpisBind = true;
ui->plainTextEdit->appendPlainText("**已成功绑定") ;
ui->plainTextEdit->appendPlainText("**绑定端口:"+QString::number(mUdpSocket->localPort())) ;
ui->pushButton_bindingPort->setText("解除绑定");
}
else{
ui->plainTextEdit->appendPlainText("**绑定失败");
}
}
}
绑定端口后,socket 的状态变为已绑定状态BoundState,解除绑定后状态变为未连接状态UnconnectedState。
5 UDP单播、广播代码实现讲解
单播消息和广播消息都使用 QUdpSocket::writeDatagram() 函数, 窗口上 单播消息 和广播消息两个按钮的代码如下:
void UDP::on_pushButton_unicast_clicked()
{
//单播消息按钮.
QString targetIP = ui->comboTargetIP->currentText(); //目标IP
QHostAddress targetAddr (targetIP) ;
quint16 targetPort=ui->spinBox_Targetport->value() ;//目标port
QString msg=ui->lineEdit_editMsg->text() ;//发送的消息内容
QByteArray str=msg. toUtf8() ;
mUdpSocket->writeDatagram (str, targetAddr, targetPort); //发出数据报
ui->plainTextEdit->appendPlainText ("[out] "+msg) ;
ui->lineEdit_editMsg->clear();
ui ->lineEdit_editMsg->setFocus () ;
}
void UDP::on_pushButton_broadcast_clicked()
{
//广播消息按钮.
//quint16 targetPort=ui->spinBox_Targetport->value(); //目标端口
//因为是一台电脑,所以端口不能相同,为了后面的测试效果写死广播目标范围
quint16 targetPort[5] ={1001,1002,1003,1004,1005};
QString msg=ui->lineEdit_editMsg->text () ;
QByteArray str=msg.toUtf8();
for(auto port:targetPort){
mUdpSocket->writeDatagram (str, QHostAddress::Broadcast, port) ;
}
ui->plainTextEdit->appendPlainText (" [broadcast] "+msg) ;
ui->lineEdit_editMsg->clear () ;
ui ->lineEdit_editMsg->setFocus () ;
}
使用writeDatagram()函数向一个目标用户发送消息时,需要指定目标地址和端口。
在广播消息时,只需将目标地址更换为一个特殊地址,即广播地址QHostdres:Broadeast,一般是 255.255.255.255。
QUdpSocket发送的数据报是QByteArray类型的字节数组,数据报的长度一般不超过 512 字节。数据报的内容可以是文本字符串,也可以自定义格式的二进制数据,文本字符串无需以换行符结束。
QUdpSocket 接收到数据报后发射readyRead()信号,在关联的槽函数onSocketReadyRead() 里读取缓冲区的数据报,代码如下:
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) ;
}
}
-
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());
读取的数据报内容是QByteArray 字节数组,因为本程序只是传输字符串,所以简单地将其转换为字符串即可。如果传输的是自定义格式的字符串或二进制数据,需要对接收到的数据进行解析。
6 运行效果实践通信动态图
因为组播和单播、广播有差异区别,加上篇幅到此感觉已经很长了,在写下去,恐怕读者会有疲劳感,反而阅读效果不佳, 所有暂时写到这里,UDP组播单独写成一章,到此单播和广播的实现也就结束了,下面看下单播和广播的通信动态图效果: