QT网络(三):UDP通信
UDP简介
UDP用户数据报协议(User Datagram Protocol,UDP)是轻量的、不可靠的、面向数据报(datagram) 的、无连接的协议,它可以用于对可靠性要求不高的场合(如实时网络视频通话)。UDP 通信不区分客户端和服务器端,UDP 程序都是客户端程序。
两个 UDP 客户端之间进行 UDP通信时,无须预先建立持久的 socket 连接,UDP 客 户端每次发送数据报时指定目标地址和端口即可。
相关的类
QUdpSocket 类用于实现 UDP 通信,它与 QTcpSocket 具有相同的父类 QAbstractSocket,因而 这两个类共享大部分的接口函数。主要区别是 QUdpSocket 以数据报传输数据,而不是以连续的数据流传输数据。QUdpSocket::writeDatagram()函数用于发送数据报,数据报一般少于 512 字节,每个数据报包含发送者和接收者的 IP 地址和端口等信息。
UDP接收数据时,要先用 QUdpSocket::bind()函数绑定一个端口,用于接收传入的数据报。当有数据报传入时 QUdpSocket 会发射 readyRead()信号,使用 QUdpSocket::readDatagram()函数可以读取接收到的数据报。
UDP消息传送有三种模式:单播、广播、组播模式,各个模式示意如下:
• 单播(unicast):一个 UDP 客户端发出的数据报只发送到一个指定地址和端口的 UDP 客户端,是一对一的数据传输。
• 广播(broadcast):一个 UDP 客户端发出的数据报,在同一网络范围内所有的 UDP 客户端都可以收到。QUdpSocket 支持 IPv4 广播,广播经常用于实现网络发现的协议。要广播数据,只需在数据报中指定接收端地址为 QHostAddress::Broadcast,一般的广播地址是 255.255.255.255。
• 组播(multicast):也称为多播。UDP 客户端加入一个由组播 IP 地址指定的多播组(用同 一个组播 IP 地址接收组播数据报的所有主机构成一个组,称为多播组或组播组),成员向组播地址发送的数据报组内成员都可以接收到,类似于 QQ 群的功能。
QUdpSocket::joinMulticastGroup()函数可实现加入多播组的功能,加入多播组后,UDP 数据的收发与正常的 UDP 数据的收发方法一样。
TCP 通信只有单播模式, 没有广播和组播模式。UDP 通信虽然不能保证数据传输的准确性,但是它具有灵活性,一般的即时通信软件都是基于 UDP 通信的。
QUdpSocket 类扩展了一些函数用于支持UDP特有的一些功能,如数据报读写和组播通信功能。在单播、广播和组播模式下,UDP 程序都是对等的。组播和广播的实现方式基本相同,只是数据报的目标 IP 地址设置不同。组播模式需要加入多播组,实现方式有较大差异。
UDP单播和广播
可以打开两个UDP实例,在同一台计算机上运行时,两个运行实例需要绑定不同的端口,如果两个实例在不同计算机上运行,则可以绑定相同的端口,因为 IP 地址不同了,不会导致绑定时发生冲突。一般的 UDP 通信程序都是在不同计算机上运行的,约定一个固定的端口作为通信端口。
主窗口头文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QUdpSocket>
#include <QLabel>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QLabel *labSocketState; //状态栏上的标签
QUdpSocket *udpSocket;
QString getLocalIP(); //获取本机IP地址
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
//自定义槽函数
void do_socketStateChange(QAbstractSocket::SocketState socketState);
void do_socketReadyRead(); //读取socket传入的数据 接收消息报文
void on_actStart_triggered(); //绑定本机端口
void on_actStop_triggered(); //解除绑定
void on_actClear_triggered(); //清空文本框
void on_btnSend_clicked(); //单播发送消息报
void on_btnBroadcast_clicked(); //广播发送消息报文
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
在构造函数中获取本机IP与TCP通信一致(往期博客),然后创建了udpSocket用于进行UDP通信,并将其stateChanged()信号与自定义槽函数do_socketStateChange() 关联,将其 readyRead()信号与自定义槽函数 do_socketReadyRead()关联。
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); //创建socket
connect(udpSocket,&QUdpSocket::stateChanged,this,&MainWindow::do_socketStateChange);
do_socketStateChange(udpSocket->state()); //执行一次,显示当前状态
connect(udpSocket,SIGNAL(readyRead()), this,SLOT(do_socketReadyRead()));
}
UDP通信实现
绑定端口
使用bind()绑定本机的一个端口,用于监听该端口,其他UDP实例可以往该端口发送内容,当有数据时候udpSocket会发送readyRead()信号
void MainWindow::on_actStart_triggered()
{//"绑定端口"按钮
quint16 port=ui->spinBindPort->value(); //本机UDP端口
if (udpSocket->bind(port)) //绑定端口成功
{
ui->textEdit->appendPlainText("**已成功绑定");
ui->textEdit->appendPlainText("**绑定端口:"
+QString::number(udpSocket->localPort()));
ui->actStart->setEnabled(false);
ui->actStop->setEnabled(true);
ui->btnSend->setEnabled(true);
ui->btnBroadcast->setEnabled(true);
}
else
ui->textEdit->appendPlainText("**绑定失败");
}
解除绑定:使用abort()函数
void MainWindow::on_actStop_triggered()
{//"解除绑定"按钮
udpSocket->abort(); //解除绑定,复位socket
ui->actStart->setEnabled(true);
ui->actStop->setEnabled(false);
ui->btnSend->setEnabled(false);
ui->btnBroadcast->setEnabled(false);
ui->textEdit->appendPlainText("**已解除绑定");
}
绑定端口后,socket 的状态变为 QAbstractSocket::BoundState(已绑定),解除绑定后状态变为 QAbstractSocket::UnconnectedState(未连接)
消息发送
单播和广播都使用QUdpSocket::writeDatagram()函数发送消息。
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->textEdit->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->textEdit->appendPlainText("[broadcast] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
}
函数 writeDatagram()原型定义如下:
qint64 QUdpSocket::writeDatagram(const QByteArray &datagram, const QHostAddress &host, quint16 port)
其中,datagram 是要发出的数据报,host 是目标主机,port 是目标端口。
对于单播,指定一个IP地址即可,对于广播设置成常量QHostAddress::Broadcast(这个地址一般是 255.255.255.255)。
注意UDP数据报发送的是QByteArray 类型的字节数据数组,数据报一般不超过 512 字节。数据报的内容可以是字符串,也可以是自定义格式的二进制数据,字符串无须以换行符结束(TCP需要换行结束)。
消息接收
QUdpSocket 接收到数据报后发射 readyRead()信号,可以设置对应的槽函数使用readDatagram()函数接收消息。
void MainWindow::do_socketReadyRead()
{//读取收到的数据报
while(udpSocket->hasPendingDatagrams()) //有待读取的数据报
{
QByteArray datagram; //获取数据报的字节数
datagram.resize(udpSocket->pendingDatagramSize());
QHostAddress peerAddr;
quint16 peerPort; //IP地址和端口是可选参数,可用于读取指定主机端口的数据
udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
QString str=datagram.data();
QString peer="[From "+peerAddr.toString()+":"+QString::number(peerPort)+"] ";
ui->textEdit->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 变量是可选的,用于获取数据报来源的地址和端口。
如果接收的是纯文本字符串,可以将获取到的QByteArray数据转为QString类型。如果传输的是自定义格式的字符串或二进制数据,需要对接收到的数据进行解析。
UDP组播
UDP 组播是主机之间“一对一组”的通信模式,当多个客户端加入由一个组播地址定义的多播组之后,客户端向组播地址和端口发送的 UDP 数据报,组内成员都可以接收到,其功能类似于 QQ 群。组内的成员是动态变化的,主机可以在任何时刻加入和离开组。
使用UDP组播必须使用一个组播地址。组播地址是 D 类 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 单播的完全相同。
组播实示例程序设计
在局域网的两台计算机上(192.168.1.101和192.168.1.102)分别运行程序,将两个主机上的程序都加入地址为 239.255.43.21 的多播组,绑定端口 35320进行通信。
主窗口头文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QUdpSocket>
#include <QLabel>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QLabel *labSocketState;
QUdpSocket *udpSocket; //socket连接
QHostAddress groupAddress; //组播地址
QString getLocalIP(); //获取本机IP地址
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
//自定义槽函数
void do_socketStateChange(QAbstractSocket::SocketState socketState);
void do_socketReadyRead();//读取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
相比前面的例子,多了一个QHostAddress 类型变量 groupAddress,用于记录组播地址。
在主窗口构造函数中,使用函数setSocketOption()对 udpSocket 进行参数设置,该函数原型定义如下:
void QAbstractSocket::setSocketOption(QAbstractSocket::SocketOption option, const QVariant &value)
参数 option 是要设置的选项名称,属于枚举类型 QAbstractSocket::SocketOption;参数 value 是 要设置的选项的值。
代码中将udpSocket的QAbstractSocket::MulticastTtlOption选项的值设置为1。MulticastTtlOption 是 UDP 组播的数据报的生存期,数据报每跨一个路由该值会减 1。MulticastTtlOption 的默认值为 1, 表示组播数据报只能在同一路由下的局域网内传播。
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);
//Multicast路由层次,1表示只在同一局域网内
//组播TTL: 生存时间,每跨1个路由会减1,多播无法跨过大多数路由所以为1
//默认值是1,表示数据包只能在本地的子网中传送。
udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);
// udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,ui->spinTTL->value());
connect(udpSocket,&QUdpSocket::stateChanged,this,&MainWindow::do_socketStateChange);
do_socketStateChange(udpSocket->state()); //立即刷新一次
connect(udpSocket,SIGNAL(readyRead()),this,SLOT(do_socketReadyRead()));
}
加入组播与退出组播
加入组播通过bind()函数,原型定义如下:
bool QAbstractSocket::bind(QHostAddress::SpecialAddress addr, quint16 port = 0, QAbstractSocket::BindMode mode = DefaultForPlatform)
参数 addr 是枚举类型 QHostAddress::SpecialAddress,表示一些特殊的主机地址,程序中设置为 QHostAddress::AnyIPv4,表示任何 IPv4 地址。参数 port 是要绑定的端口,程序中设置为多播组统一的一个端口 groupPort。参数 mode 表示绑定模式,程序中设置为QUdpSocket::ShareAddress, 表示允许其他服务使用这个地址和端口,组播模式时参数 mode 必须设置为这个值。(而在UDP单播和广播中,bind函数只传递了目标IP地址)
绑定好组播地址后,需要使用 QUdpSocket:: joinMulticastGroup()函数将本机地址加入多播组中,退出组播地址使用QUdpSocket::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->textEdit->appendPlainText("**加入组播成功");
ui->textEdit->appendPlainText("**组播地址IP:"+IP);
ui->textEdit->appendPlainText("**绑定端口:"+QString::number(groupPort));
ui->actStart->setEnabled(false);
ui->actStop->setEnabled(true);
ui->comboIP->setEnabled(false);
ui->spinPort->setEnabled(false);
ui->btnMulticast->setEnabled(true);
}
else
ui->textEdit->appendPlainText("**绑定端口失败");
}
void MainWindow::on_actStop_triggered()
{//"退出组播"按钮
udpSocket->leaveMulticastGroup(groupAddress); //退出组播
udpSocket->abort(); //解除绑定
ui->actStart->setEnabled(true);
ui->actStop->setEnabled(false);
ui->comboIP->setEnabled(true);
ui->spinPort->setEnabled(true);
ui->btnMulticast->setEnabled(false);
ui->textEdit->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);
ui->textEdit->appendPlainText("[multicast] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
}
接收消息部分和UDP 单播时接收数据的代码一致
void MainWindow::do_socketReadyRead()
{//读取数据报
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->textEdit->appendPlainText(peer+str);
}
}
参考
QT6 C++ 开发指南
作者:杨德杰