简介
UDP(User Datagram Protocol,用户数据报协议)是轻量的,不可靠的、面向数据报(datagram)的、无连接的协议,它可以用于对可靠性要求不高的场景。与TCP通信不同,两个程序之间进行UDP通信不需要预先建立持久的socket连接,UDP每次发送数据报都需要指定目标地址和端口。
QUdpSocket以数据报传输数据,而不是连续的数据流。发送数据报使用函数QUdpSocket::writeDatagram(),数据报的长度一般小于512字节,每个数据报包含发送者和接收者的IP地址和端口等信息。
要进行UDP数据接收,要用QUdpSocket::bind()函数先绑定一个端口,用于接收传入的数据报,当有数据报传入时会发射readyRead()信号,使用readDatagram()函数来读取接收到的数据报。
UDP消息传送有单播、广播、组播三种模式,示意图如下:
- 单播(unicast)模式:一个UDP客户端发出的数据报只发送到另一个指定地址和端口的UDP客户端,时一对一的数据传输。
- 广播(broadcast)模式:一个UDP客户端发出的数据报,在同一网络范围内其他所有的UDP客户端都可以收到。QUdpSocket支持IPv4广播。广播经常用于实现网络发现的协议。要获取广播数据只需在数据报中指定接收端地址为QHostAddress::Broadcast,一般的广播地址为255.255.255.255。
- 组播模式(multicast)模式:也称多播。UDP客户端加入到另一个组播IP地址指定的多播组,成员向组播地址发送的数据报组内成员都可以收到,类似于QQ群功能。QUdpSocket::joinMulticastGroup()函数实现加入多播组的功能,加入多播组后,UDP数据的收发与正常的UDP数据的收发方式一样。
使用广播和多播模式,UDP可以实现一些比较灵活的通信功能,而TCP通信只有单播模式,没有广播和多播模式。所以,UDP通信虽然不能确保数据传输的准确性,但是具有灵活性,一般的即时通信软件都是基于UDP通信的。
在单播,广播,多播模式下,UDP程序都是对等的,不像TCP通信那样分为客户端和服务器端。多播和广播的实现方式基本相同,只是数据报的目标IP地址设置不同,多播模式需要加入多播组,实现方式有较大差异。
UDP单播和广播
示例程序QUDPUnicast_Broadcast的主窗口继承自QMainWindow类,程序可以进行UDP数据报的接收和发送。
可以运行在不同的计算机上,也可以运行在不同的计算机上。在同一台计算机上运行时,两个运行实例需要绑定不同的端口,如果两个实例运行在不同的计算机上,则可以使用相同的端口,因为IP地址不同了,不会导致绑定时发生冲突。一般的UDP通信程序都是在不同的计算机上运行的,约定一个固定的端口作为通信端。
ui界面
ui界面如下:
程序
QUDPUnicast_Broadcast.h如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QLabel>
#include <QUdpSocket>
#include <QHostInfo>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui;
QLabel *LabSocketState;
QUdpSocket *udpSocket;
QStringList getLocalIP();
private slots:
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onSocketReadyRead(); //读取socket传入数据
void on_act_bindPotr_triggered();
void on_act_unbind_triggered();
void on_pb_send_clicked();
void on_pb_broadmsg_clicked();
void on_act_clear_triggered();
void on_act_exit_triggered();
};
#endif // MAINWINDOW_H
QUDPUnicast_Broadcast.cpp如下:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
LabSocketState = new QLabel("Socket状态:");
LabSocketState->setMinimumWidth(200);
ui->statusBar->addWidget(LabSocketState);
QStringList localeIp = getLocalIP();
ui->cb_tarAddress->addItems(localeIp);
udpSocket = new QUdpSocket(this);
connect(udpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
onSocketStateChange(udpSocket->state());
connect(udpSocket,SIGNAL(readyRead()),this,SLOT(onSocketReadyRead()));
}
MainWindow::~MainWindow()
{
delete ui;
}
QStringList MainWindow::getLocalIP()
{
QString hostName = QHostInfo::localHostName();
QHostInfo hostInfo = QHostInfo::fromName(hostName);
QStringList ipList;
QList<QHostAddress> aHost = hostInfo.addresses();
if(!aHost.isEmpty())
{
for(int i = 0; i < aHost.count(); i++)
{
QHostAddress aAddress = aHost.at(i);
if(QAbstractSocket::IPv4Protocol == aAddress.protocol())
{
ipList.append(aAddress.toString());
}
}
}
return ipList;
}
void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{
//socket状态变化
switch (socketState)
{
case QAbstractSocket::UnconnectedState:
LabSocketState->setText("socket状态:UnconnectedState");
break;
case QAbstractSocket::HostLookupState:
LabSocketState->setText("socket状态:HostLookupState");
break;
case QAbstractSocket::ConnectingState:
LabSocketState->setText("socket状态:ConnectingState");
break;
case QAbstractSocket::ConnectedState:
LabSocketState->setText("socket状态:ConnectedState");
break;
case QAbstractSocket::BoundState:
LabSocketState->setText("socket状态:BoundState");
break;
case QAbstractSocket::ClosingState:
LabSocketState->setText("socket状态:ClosingState");
break;
case QAbstractSocket::ListeningState:
LabSocketState->setText("socket状态:ListeningState");
break;
}
}
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);
}
}
void MainWindow::on_act_bindPotr_triggered()
{
quint16 port = ui->sb_bindPort->value(); //本机UDP端口
if(udpSocket->bind(port)) //绑定端口成功
{
ui->plainTextEdit->appendPlainText("**已成功绑定");
ui->plainTextEdit->appendPlainText("**绑定端口:"+QString::number(udpSocket->localPort()));
ui->act_bindPotr->setEnabled(false);
ui->act_unbind->setEnabled(true);
}
else
{
ui->plainTextEdit->appendPlainText("**绑定失败");
}
}
void MainWindow::on_act_unbind_triggered()
{
udpSocket->abort(); //结束绑定
ui->act_bindPotr->setEnabled(true);
ui->act_unbind->setEnabled(false);
ui->plainTextEdit->appendPlainText("**已解除绑定");
}
void MainWindow::on_pb_send_clicked()
{
QString targetIP = ui->cb_tarAddress->currentText(); //目标IP
QHostAddress targetAddr(targetIP);
quint16 targetPort = ui->sb_tarPort->value();
QString msg = ui->le_msg->text(); //发送的消息内容
QByteArray str = msg.toUtf8();
udpSocket->writeDatagram(str,targetAddr,targetPort); //发出数据报
ui->plainTextEdit->appendPlainText("[Out]:"+msg);
ui->le_msg->clear();
ui->le_msg->setFocus();
}
void MainWindow::on_pb_broadmsg_clicked()
{
quint16 targetPort = ui->sb_tarPort->value(); //目标端口
QString msg = ui->le_msg->text(); //发送的消息内容
QByteArray str = msg.toUtf8();
udpSocket->writeDatagram(str,QHostAddress::Broadcast,targetPort); //发出数据报
ui->plainTextEdit->appendPlainText("[broadcast]:"+msg);
ui->le_msg->clear();
ui->le_msg->setFocus();
}
void MainWindow::on_act_clear_triggered()
{
ui->plainTextEdit->clear();
}
void MainWindow::on_act_exit_triggered()
{
this->close();
}
QUdpSocket类型的私有变量udpSocket是用于UDP通信的socket。
定义的槽函数,onSocketStateChange()与udpSocket的stateChange()信号相关,用于显示udpSocket当前的状态;onSocketReadyRead()槽函数与udpSocket()的readyRead()信号相关联,用于读取缓冲区的数据报。
要实现UDP数据的接收,必须先用QUdpSocket::bind()函数绑定一个端口,用于监听传入的数据报,解除绑定则使用abort()函数。
绑定端口后,socket的状态转变为已绑定状态"BoundState",解除绑定后状态变为未连接状态“UnconnectedState”。
发送点对点消息和广播消息都是用QUdpSocket::writeDatagram()函数。使用writeDatagram()函数向一个目标用户发送消息时,需要指定目标地址和端口。
在广播消息时,只需要将目标地址更换为一个特殊的地址,即广播地址QHostAddress::Broadcast,一般是255.255.255.255。
QUdpSocket发送的数据报时QByteArray类型的字节数组,数据报的长度一般不超过512字节。数据报的内容可以是文本字符串,也可以是自定义格式的二进制数据,文本字符串无需以换行符结束。
QUdpSocket接收到数据报后发射readyRead()信号,之后在关联的槽函数onSocketReadyRead()中读取缓冲区中的数据报。
其中:
hasPendingDatagrams()表示是否有待读取的传入数据报。
pendingDatagramSize()返回待读取的数据报的字节数。
readDatagram()函数用于读取数据报内容。
三、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完全相同。
ui界面
程序
示例程序为QUDPMulticast.h与QUDPMulticast.cpp
QUDPMulticast.h如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QLabel>
#include <QUdpSocket>
#include <QHostInfo>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui;
QLabel *LabSocketState;
QUdpSocket *udpSocket;
QHostAddress groupAddress; //组播地址
QStringList getLocalIP(); //获取本机ip
private slots:
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onSocketReadyRead();
void on_act_join_triggered();
void on_act_unjoin_triggered();
void on_pb_send_clicked();
};
#endif // MAINWINDOW_H
QUDPMulticast.cpp如下:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
LabSocketState = new QLabel("Socket状态:");
LabSocketState->setMinimumWidth(200);
ui->statusBar->addWidget(LabSocketState);
QStringList ipList = getLocalIP();
ui->le_ip->setText(ipList.at(2));
udpSocket = new QUdpSocket(this);
udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);
connect(udpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
onSocketStateChange(udpSocket->state());
connect(udpSocket,SIGNAL(readyRead()),this,SLOT(onSocketReadyRead()));
}
MainWindow::~MainWindow()
{
delete ui;
}
QStringList MainWindow::getLocalIP()
{
QString localName = QHostInfo::localHostName();
QHostInfo aHost = QHostInfo::fromName(localName);
QStringList ipList;
QList<QHostAddress> ipAddress = aHost.addresses();
if(!ipAddress.isEmpty())
{
for(int i = 0; i < ipAddress.count(); i++)
{
if(QAbstractSocket::IPv4Protocol == ipAddress.at(i).protocol())
{
ipList.append(ipAddress.at(i).toString());
}
}
}
return ipList;
}
void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{
//socket状态变化
switch (socketState)
{
case QAbstractSocket::UnconnectedState:
LabSocketState->setText("socket状态:UnconnectedState");
break;
case QAbstractSocket::HostLookupState:
LabSocketState->setText("socket状态:HostLookupState");
break;
case QAbstractSocket::ConnectingState:
LabSocketState->setText("socket状态:ConnectingState");
break;
case QAbstractSocket::ConnectedState:
LabSocketState->setText("socket状态:ConnectedState");
break;
case QAbstractSocket::BoundState:
LabSocketState->setText("socket状态:BoundState");
break;
case QAbstractSocket::ClosingState:
LabSocketState->setText("socket状态:ClosingState");
break;
case QAbstractSocket::ListeningState:
LabSocketState->setText("socket状态:ListeningState");
break;
}
}
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);
}
}
void MainWindow::on_act_join_triggered()
{
//加入组播
QString IP = ui->le_ip->text();
groupAddress = QHostAddress(IP); //多播组地址
quint16 groupPort = ui->sb_port->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->act_join->setEnabled(false);
ui->act_unjoin->setEnabled(true);
ui->le_ip->setEnabled(false);
}
else
{
ui->plainTextEdit->appendPlainText("**加入组播组失败");
}
}
void MainWindow::on_act_unjoin_triggered()
{
//退出组播
udpSocket->leaveMulticastGroup(groupAddress); //退出组播
udpSocket->abort(); //解除绑定
ui->act_join->setEnabled(true);
ui->act_unjoin->setEnabled(false);
ui->le_ip->setEnabled(true);
ui->plainTextEdit->appendPlainText("**已退出组播,解除端口绑定");
}
void MainWindow::on_pb_send_clicked()
{
//发送组播消息
quint16 groupPort = ui->sb_port->value();
QString msg = ui->le_msg->text();
QByteArray datagram = msg.toUtf8();
udpSocket->writeDatagram(datagram,groupAddress,groupPort);
ui->plainTextEdit->appendPlainText("[multicast]"+msg);
ui->le_msg->clear();
ui->le_msg->setFocus();
}
在.h文件中定义了一个QHostAddress类型的变量groupAddress,用于记录组播地址。
在.cpp文件中,使用QUdpSocket::setSocketQption()函数,对socket进行参数设置。
udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);
将socket的QAbstractSocket::MulticastTtlOption的值设置为1,MulticastTtlOption是UDP组播的数据报的生存周期,数据报每跨过一个路由会减1。缺省为1,表示多播数据报只能在同一路由下的局域网内传播。
加入组播前,必须先绑定端口,绑定端口的语句是:
udpSocket->bind(QHostAddress::AnyIPv4,groupPort,QUdpSocket::ShareAddress)
这里指定的地址为:QHostAddress::AnyIPv4,端口为多播组统一的一个端口。
使用QUdpSocket::joinMulticastGroup();函数加入组播
多播组地址groupAddress由界面上的组合框里输入,注意,局域网内的组播地址的范围是239.0.0.0~239.255.255.255,绝不能使用本机地址作为组播地址。
退出组播使用leaveMulticastGroup()函数
加入多播组后,发送组播数据报也是使用writeDatagram()函数,只是目标地址使用的是组播地址,在readyRead()信号的槽函数里使用readDatagram()槽函数读取数据报。
参考
本文参考 Qt 5.9 C++开发指南 (王维波 栗宝鹃 侯春望) 网络编程篇,详细信息请阅读该书籍。