0.前言
本文主要讲解 Qt UDP 相关接口的基本应用,一些实践相关的后面会单独写。
UDP(用户数据报协议 User Datagram Protocol)是一种轻量级,不可靠,面向数据报的无连接协议。
- UDP 是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP 传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP 把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
- 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等, 因此一台服务机可同时向多个客户机传输相同的消息。
- UDP 信息包的标题很短,只有 8 个字节,相对于 TCP 的 20 个字节信息包的额外开销很小。
- 吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、 源端和终端主机性能的限制。
- UDP 使用尽最大努力交付,即不保证可靠交付, 因此主机不需要维持复杂的链接状态表(这里面有许多参数)。
- UDP 是面向报文的。发送方的 UDP 对应用程序交下来的报文, 在添加首部后就向下交付给 IP 层。既不拆分,也不合并,而是保留这些报文的边界, 因此,应用程序需要选择合适的报文大小。
UDP 知识参考:https://zhuanlan.zhihu.com/p/24860273
这篇参考的评论区也有很多可以学习的,比如:ping 命令是使用 IP 和网络控制信息协议 (ICMP),而不是 UDP。
1.准备工作
首先,要使用 Qt 的网络模块需要在 pro 中加上 network(如果是 VS IDE 就在模块选择里勾选上 network):
QT += network
引入相关类的头文件:
#include <QUdpSocket>
#include <QHostAddress>
#include <QNetworkDatagram>
Qt UDP 的操作流程:
图片参考:https://blog.csdn.net/qq_32298647/article/details/74834254
2.认识QUdpSocket的接口
QUdpSocket 是 QAbstractSocket 的子类,用于发送和接收 UDP 数据报。
可以使用 bind() 显式的绑定地址和端口。参数中的地址可以使用 QHostAddress::Any 绑定任意地址,IPv4 等效于 "0.0.0.0" , IPv6 等效于 "::";而 BindMode 一般可以设置 ShareAddress(允许其他服务绑定到相同的地址和端口) 和 DontShareAddress(不允许其他服务重新绑定),Windows 上默认等效于 ShareAddress。
bool QAbstractSocket::bind(const QHostAddress &address, quint16 port = 0, QAbstractSocket::BindMode mode = DefaultForPlatform)
bool QAbstractSocket::bind(quint16 port = 0, QAbstractSocket::BindMode mode = DefaultForPlatform)
绑定后,只要 UDP 数据报到达指定的地址和端口,就会触发 readyRead() 信号,此时可在槽函数中读取数据:
void QIODevice::readyRead()
可通过 hasPendingDatagrams() 判断是否有可读数据,通过 pendingDatagramSize() 判断数据长度。
对于数据报的读写依然是使用 read/write 相关接口:
qint64 QUdpSocket::readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr)
QNetworkDatagram QUdpSocket::receiveDatagram(qint64 maxSize = -1)
qint64 QUdpSocket::writeDatagram(const char *data, qint64 size, const QHostAddress &address, quint16 port)
qint64 QUdpSocket::writeDatagram(const QNetworkDatagram &datagram)
qint64 QUdpSocket::writeDatagram(const QByteArray &datagram, const QHostAddress &host, quint16 port)
对于单播,可以直接指定目标地址和端口发送:
const QString address_text = "127.0.0.1";
const QHostAddress address = QHostAddress(address_text);
const unsigned short port = 12345;
udpSocket->writeDatagram(QNetworkDatagram(send_data,address,port));
对于广播,需要发送到广播地址 "255.255.255.255",即使用 QHostAddress::Broadcast:
udpSocket->writeDatagram(QNetworkDatagram(send_data,QHostAddress::Broadcast,port));
对于组播,需要发送到指定的组播地址,不过要对方加入了这个组播(使用 joinMulticastGroup 和 leaveMulticastGroup 加入/退出组播):
//组播ip必须是D类ip
//D类IP段 224.0.0.0 到 239.255.255.255
//且组播地址不能是224.0.0.1
udpSocket->bind(QHostAddress::AnyIPv4,port); //根据Qt示例,组播的话IPv4和v6分开的
udpSocket->joinMulticastGroup(address); //QHostAddress("224.0.0.2")
udpSocket->writeDatagram(QNetworkDatagram(send_data,address,port)); //QHostAddress("224.0.0.2")
操作完之后,调用相关接口关闭和释放:
void QAbstractSocket::disconnectFromHost()
void QAbstractSocket::close()
void QAbstractSocket::abort()
其中, abort 调用了 close, close 调用了 disconnectFromHost。 abort 立即关闭套接字,并丢弃写缓冲区中的所有待处理数据。close 关闭套接字的 IO,以及套接字的连接。
文档:https://doc.qt.io/qt-5/qudpsocket.html
3.Qt Udp的简单示例
完整代码链接(SimpleUdpClient子项目):
https://github.com/gongjianbo/HelloQtNetwork
运行效果(可以下个网络助手调试,如果只有一台电脑,没有虚拟机,也可以用手机下个网络助手连同一个网的 Wifi 联调):
主要实现代码:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
//udp demo
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
//初始化client操作
void initClient();
//更新当前状态
void updateState();
private:
Ui::Widget *ui;
//socket对象
QUdpSocket *udpSocket;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include <QNetworkInterface>
#include <QHostAddress>
#include <QNetworkDatagram>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
setWindowTitle("Client");
initClient();
}
Widget::~Widget()
{
//关闭套接字,并丢弃写缓冲区中的所有待处理数据。
udpSocket->abort();
delete ui;
}
void Widget::initClient()
{
//创建udp socket对象
udpSocket = new QUdpSocket(this);
//获取本机ip
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
qDebug()<<"ip list:"<<ipAddressesList;
//下拉框切换
connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
[=](int index){
switch (index) {
case 0:
ui->editLocalAddress->setText(ipAddressesList.first().toString());
ui->editPeerAddress->setText(ipAddressesList.first().toString());
break;
case 1:
ui->editLocalAddress->setText("Any");
ui->editPeerAddress->setText("Broadcast");
break;
case 2:
ui->editLocalAddress->setText("224.0.0.2");
ui->editPeerAddress->setText("224.0.0.2");
break;
default:
break;
}
});
ui->editLocalAddress->setText(ipAddressesList.first().toString());
ui->editPeerAddress->setText(ipAddressesList.first().toString());
//点击绑定端口,根据ui设置进行绑定
connect(ui->btnBind,&QPushButton::clicked,[this]{
//判断当前是否已绑定,bind了就取消
if(udpSocket->state()==QAbstractSocket::BoundState){
//关闭套接字,并丢弃写缓冲区中的所有待处理数据。
udpSocket->abort();
}else if(udpSocket->state()==QAbstractSocket::UnconnectedState){
//从界面上读取ip和端口
const QHostAddress address=QHostAddress(ui->editLocalAddress->text());
const unsigned short port=ui->editLocalPort->text().toUShort();
//绑定本机地址
//combobox:单播-广播-组播
switch (ui->comboBox->currentIndex())
{
case 0:
//可以指定本地绑定的ip
udpSocket->bind(address,port);
//udpSocket->bind(port);
break;
case 1:
//udpSocket->bind(address,port);
//udpSocket->bind(port);
udpSocket->bind(QHostAddress::AnyIPv4,port);
break;
case 2:
//组播ip必须是D类ip
//D类IP段 224.0.0.0 到 239.255.255.255
//且组播地址不能是224.0.0.1
udpSocket->bind(QHostAddress::AnyIPv4,port); //根据Qt示例,组播的话IPv4和v6分开的
udpSocket->joinMulticastGroup(address); //QHostAddress("224.0.0.2")
break;
default:
break;
}
}else{
ui->textRecv->append("It is not BoundState or UnconnectedState");
}
});
//绑定状态改变
connect(udpSocket,&QUdpSocket::stateChanged,[this](QAbstractSocket::SocketState socketState){
//已绑定就设置为不可编辑
const bool is_bind=(socketState==QAbstractSocket::BoundState);
ui->btnBind->setText(is_bind?"Disbind":"Bind");
ui->editLocalAddress->setEnabled(!is_bind);
ui->editLocalPort->setEnabled(!is_bind);
ui->editPeerAddress->setEnabled(!is_bind);
ui->editPeerPort->setEnabled(!is_bind);
ui->comboBox->setEnabled(!is_bind);
updateState();
});
//发送数据
connect(ui->btnSend,&QPushButton::clicked,[this]{
//判断是可操作,isValid表示准备好读写
if(!udpSocket->isValid())
return;
//将发送区文本发送给客户端
const QByteArray send_data=ui->textSend->toPlainText().toUtf8();
//数据为空就返回
if(send_data.isEmpty())
return;
//从界面上读取ip和端口
const QString address_text=ui->editPeerAddress->text();
const QHostAddress address=QHostAddress(address_text);
const unsigned short port=ui->editPeerPort->text().toUShort();
//combobox:单播-广播-组播
switch (ui->comboBox->currentIndex())
{
case 0:
udpSocket->writeDatagram(QNetworkDatagram(send_data,address,port));
break;
case 1:
udpSocket->writeDatagram(QNetworkDatagram(send_data,QHostAddress::Broadcast,port));
break;
case 2:
udpSocket->writeDatagram(QNetworkDatagram(send_data,address,port)); //QHostAddress("224.0.0.2")
break;
default:
break;
}
});
//收到数据,触发readyRead
connect(udpSocket,&QUdpSocket::readyRead,[this]{
//没有可读的数据就返回
if(!udpSocket->hasPendingDatagrams()||
udpSocket->pendingDatagramSize()<=0)
return;
//注意收发两端文本要使用对应的编解码
QNetworkDatagram recv_datagram=udpSocket->receiveDatagram();
const QString recv_text=QString::fromUtf8(recv_datagram.data());
ui->textRecv->append(QString("[%1:%2]")
.arg(recv_datagram.senderAddress().toString())
.arg(recv_datagram.senderPort()));
ui->textRecv->append(recv_text);
});
//error信号在5.15换了名字
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
//错误信息
connect(udpSocket, static_cast<void(QAbstractSocket::*)(QAbstractSocket::SocketError)>(&QAbstractSocket::error),
[this](QAbstractSocket::SocketError){
ui->textRecv->append("Socket Error:"+udpSocket->errorString());
});
#else
//错误信息
connect(udpSocket,&QAbstractSocket::errorOccurred,[this](QAbstractSocket::SocketError){
ui->textRecv->append("Socket Error:"+udpSocket->errorString());
});
#endif
}
void Widget::updateState()
{
//将当前socket绑定的地址和端口写在标题栏
if(udpSocket->state()==QAbstractSocket::BoundState){
setWindowTitle(QString("Client[%1:%2]")
.arg(udpSocket->localAddress().toString())
.arg(udpSocket->localPort()));
}else{
setWindowTitle("Client");
}
}