一、udp单播、广播和组播的说明
udp是不可靠、无连接的,所以划分为发送方和接收方更好理解
1、单播
udp是无连接的,进行单播通信时,必须要绑定接收方端口,发送方直接通过接收方的ip和绑定的端口进行通信。发送方可以绑定端口也可以不用绑定端口,不绑定端口的话,系统会随机分配端口。
对于多网卡来说,需要指定网卡,绑定一个网卡的ip。如果不进行显式的绑定操作,qudpsocket
对象将会使用默认的绑定方式,自动选择一个可用的 ip 地址进行绑定。
2、广播
对于只有1个网卡的主机来说,可以不用显示绑定ip,发送方直接发送广播就行,接收方绑定广播端口就行,这样才能看到收到的消息。
对于多网卡来说,要指定唯一的网卡ip并且在广播前要绑定广播端口。ip可以不用绑定,这样系统会随机分配一个网卡ip。
3、组播
对于只有1个网卡的主机来说,可以不用绑定ip,直接绑定端口后加入组播就行。系统分配任意一个ip ,相当于 qhostaddress::anyipv4。
对于多网卡来说,要指定唯一的网卡并且在加入组播前要绑定组播端口,ip可以不用绑定,系统分配任意一个ip。
注意:指定网卡不等于指定ip!!!
1、使用 setmulticastinterface
方法可以指定一个明确的网卡,但并不意味着只有一个 ip 地址。一个网卡可以绑定多个 ip 地址,例如在同一台主机上同时存在有线网卡和无线网卡,它们可能都连接到同一局域网,并分别配置了不同的 ip 地址。此时,通过 setmulticastinterface
方法指定了一个明确的网卡后,并不确定使用哪个 ip 地址来进行组播通信。
如果需要确保使用特定的 ip 地址进行组播通信,则需要使用 bind
方法来将 qudpsocket
对象绑定到具体的 ip 地址和端口上,这样每次进行组播通信时,都会使用该 ip 地址来发送和接收数据报文。
2、使用 qhostaddress::anyipv4
参数可以将 qudpsocket
对象绑定到本机的所有 ipv4 地址。这意味着,该 qudpsocket
对象可以接收通过本机的任意一个 ipv4 地址发送到指定端口的数据包。
然而,需要注意的是,绑定到多个 ipv4 地址并不意味着可以同时从多个地址接收数据包。在任何给定的时刻,qudpsocket
对象只能通过一个 ip 地址接收数据包。
当有多个 ipv4 地址可用时,qudpsocket
对象会选择其中一个地址来接收数据包。这个选择通常由操作系统或网络栈决定,并且可能会受到各种因素的影响,例如网络接口的优先级、路由表等。
因此,使用 qhostaddress::anyipv4
参数可以让 qudpsocket
对象绑定到本机的所有 ipv4 地址,但实际上它只能通过其中一个地址接收数据包。具体使用哪个地址取决于操作系统和网络环境。
二、遇到的udp通信的问题参考
关于qt udp组播的几个问题https://blog.csdn.net/tom06/article/details/52163665?spm=1001.2014.3001.5506
qt读取网卡列表多网卡绑定组播网卡https://blog.csdn.net/qq_30727593/article/details/127441711?spm=1001.2014.3001.5506
三、效果与代码
1、在.pro文件中添加如下内容:
qt += network
2、在.h文件中添加串口所用的头文件
#include <qudpsocket>
#include <qnetworkinterface>
3、添加一个qudpsocket* socket的类成员,并在.cpp中实例化对象:
socket = new qudpsocket;
4、扫描可用网口
qlist<qnetworkinterface> interfacelist = qnetworkinterface::allinterfaces();
foreach (qnetworkinterface nif, interfacelist) {
// 检查网卡是否有效并已经启用
if (nif.isvalid() && nif.flags().testflag(qnetworkinterface::isup)) {
// 将已经启用的网卡名称添加到列表中
enabledinterfacelist.append(nif);
qlist<qnetworkaddressentry> entries = nif.addressentries();
foreach (qnetworkaddressentry entry, entries) {
if (entry.ip().protocol() == qabstractsocket::ipv4protocol) {
ui->nif_config->additem(entry.ip().tostring());
}
}
}
}
5、对组播进行设置(可以忽略)
//组播的数据的生存期,数据报没跨1个路由就会减1.表示多播数据报只能在同一路由下的局域网内传播
socket->setsocketoption(qabstractsocket::multicastttloption,1);
//1是允许loopback模式(自发自收),0是阻止。
socket->setsocketoption(qabstractsocket::multicastloopbackoption, true);
6、初始化组播设置
int mainwindow::init_group(qudpsocket *socket, qnetworkinterface ¤tinterface, qhostaddress &localaddress, qhostaddress &targetaddress, int localport)
{
if (targetaddress.ismulticast()) {//ismulticast()判断是否是组播地址
if (localport == 0) {//判断是否指定本地端口
//bind(localaddress, qudpsocket::shareaddress | qudpsocket::reuseaddresshint)可以换成bind(qhostaddress::anyipv4, qudpsocket::shareaddress | qudpsocket::reuseaddresshint)
if (socket->bind(localaddress, qudpsocket::shareaddress | qudpsocket::reuseaddresshint)) {
socket->setmulticastinterface(currentinterface);
if (socket->joinmulticastgroup(targetaddress, currentinterface)) {
qdebug() << "未指定本地端口,加入组播成功";
return 1;
} else {
qdebug() << "未指定本地端口,加入组播失败";
return -1;
}
} else {
qdebug() << "未指定本地端口,绑定失败";
return -1;
}
} else {
if (socket->bind(localaddress, localport, qudpsocket::shareaddress | qudpsocket::reuseaddresshint)) {
socket->setmulticastinterface(currentinterface);
if (socket->joinmulticastgroup(targetaddress, currentinterface)) {
qdebug() << "指定本地端口,加入组播成功";
return 1;
} else {
qdebug() << "指定本地端口,加入组播失败";
return -1;
}
} else {
qdebug() << "指定本地端口,绑定失败";
return -1;
}
}
} else {
qdebug() << "目标地址不是组播地址";
return -1;
}
// 如果执行到这里,说明没有通过任何返回语句,应该是一个错误
qdebug() << "init_group 函数执行路径错误";
return -1; // 或者抛出异常,取决于您希望如何处理这种情况
}
1、qudpsocket::shareaddress:
- 这个选项告诉操作系统允许多个
qudpsocket
对象绑定到同一个地址和端口上。在默认情况下,操作系统可能会阻止多个 socket 绑定到相同的地址和端口,但是如果设置了shareaddress
选项,qt 会尝试在可能的情况下共享这个地址和端口。 - 在实际应用中,如果需要多个
qudpsocket
对象同时监听相同的地址和端口,可以使用shareaddress
选项来避免绑定失败的问题。 - 想象一下你和朋友们想在同一个电话号码上收发短信。默认情况下,操作系统可能会阻止多个程序或者多个
qudpsocket
对象同时使用同一个网络地址(ip 地址)和端口号。但是如果你打开了shareaddress
选项,就像是你和朋友们一起共享一个电话号码,大家可以同时收发信息。 - 这个选项让多个
qudpsocket
对象可以在同一个网络地址和端口上工作,而不会相互干扰或者造成绑定失败的问题。
2、qudpsocket::reuseaddresshint:
- 这个选项告诉操作系统允许在 udp socket 关闭之后,立即重新使用相同的地址和端口。如果没有设置这个选项,操作系统会在
qudpsocket
关闭后一段时间内保持端口的占用状态,这可能会导致稍后尝试重新绑定失败。 - 设置
reuseaddresshint
可以避免在关闭一个qudpsocket
后立即重新绑定时遇到address already in use
的错误。 - 想象一下你用一个电话号码打电话,然后挂了电话。在一段时间内,电话号码可能会被暂时保留,不允许其他人再用。这就好比默认情况下,当一个
qudpsocket
关闭后,操作系统可能会暂时保留使用的网络地址和端口号,不让其他程序立即使用。 - 如果你设置了
reuseaddresshint
,就像是告诉操作系统,“我关掉电话后,如果其他人想用这个电话号码,可以立刻用,不用等。” 这个选项允许在一个qudpsocket
关闭后,立即重新使用相同的网络地址和端口号,而不会遇到“地址已经被占用”的错误。
7、发送消息
socket->writedatagram(str,targetaddress,targetport);
8、接收消息
//接收消息
connect(udpsocket,&qudpsocket::readyread,this,[&](){
qbytearray datagram;
datagram.resize(udpsocket->pendingdatagramsize());
udpsocket->readdatagram(datagram.data(), datagram.size());
ui->recvtextedit->append(datagram);
});
9、退出组播
int mainwindow::exit_group(qudpsocket *socket ,qnetworkinterface ¤tinterface, qhostaddress &targetaddress)
{
if(socket->leavemulticastgroup(targetaddress,currentinterface)){
socket->abort();
qdebug() << "退出组播成功";
return 1;
}else{
qdebug() << "退出组播失败";
return -1;
}
}
四、自定义udp工具类
myudptools.h
#ifndef myudptools_h
#define myudptools_h
#include <qobject>
#include <qthread>
#include <qcombobox>
#include <qudpsocket>
#include <qmessagebox>
#include <qapplication>
#include <qnetworkdatagram>
#include <qnetworkinterface>
class myudptools : public qobject
{
q_object
public:
explicit myudptools(qobject *parent = nullptr);
~myudptools();
// 单播相关方法
int setupunicast(qstring currentnetinterfacraddress, quint16 port = 0);
void sendunicastdata(qstring targetaddress,quint16 port,const qbytearray &data);
int exitunicast();
// 组播相关方法
int setupmulticast(qnetworkinterface *netinterface,qstring currentnetinterfacraddress,qstring groupaddress, quint16 port = 0);
void sendmulticastdata(qstring targetaddress,quint16 port,const qbytearray &data);
int exitmulticast();
// 广播相关方法
int setupbroadcast(qstring currentnetinterfacraddress,qstring targetaddress,quint16 port = 0);
void sendbroadcastdata(qstring targetaddress,quint16 port,const qbytearray &data);
int exitbroadcast();
//添加或刷新combox的网卡信息
void init_or_flash_netinterfaceinfotocombox(qlist<qnetworkinterface> &list,qcombobox *combox);
signals:
void sig_recvdata(qbytearray data);
private slots:
void processpendingdatagrams();//接收数据槽函数
void handlesocketerror(qabstractsocket::socketerror socketerror);//udp错误日志
private:
qthread *thread;
qhostaddress localaddress;//本机当前网卡ip
qudpsocket *unicastsocket;//单播
quint16 unicastport_local;//单播本地端口
quint16 unicastport_target;//单播目标端口
qhostaddress targetaddress_unicast;//单播目标地址
qudpsocket *multicastsocket;//组播
qhostaddress multicastgroupaddress;//组播地址
quint16 multicastport_local;//组播本地端口
quint16 multicastport_target;//组播目标端口
qudpsocket *broadcastsocket;//广播
qhostaddress broadcastgroupaddress;//广播地址
quint16 broadcastport_local;//广播本地端口
quint16 broadcastport_target;//广播目标端口
};
#endif // myudptools_h
myudptools.cpp
#include "myudptools.h"
myudptools::myudptools(qobject *parent)
: qobject(parent),unicastsocket(nullptr),multicastsocket(nullptr),broadcastsocket(nullptr)
{
thread = new qthread();
movetothread(thread);
//应用程序关闭后触发
connect(qapp,&qapplication::abouttoquit,thread, &qthread::quit);
//线程退出后触发
connect(thread, &qthread::finished, thread, &qthread::deletelater);
connect(thread,&qthread::finished,this,&myudptools::deletelater);
// 在子线程中直接调用函数
qmetaobject::invokemethod(this, "init_or_flash_netinterfaceinfotocombox", qt::queuedconnection);
//启动线程
thread->start();
}
myudptools::~myudptools()
{
if(unicastsocket){
delete unicastsocket;
}
if(multicastsocket){
delete multicastsocket;
}
if(broadcastsocket){
delete broadcastsocket;
}
}
//===========================单播相关方法实现===========================
int myudptools::setupunicast(qstring currentnetinterfacraddress, quint16 port)
{
if (!unicastsocket) {
unicastsocket = new qudpsocket(this);//将父对象设置为当前对象
connect(unicastsocket, &qudpsocket::readyread, this, &myudptools::processpendingdatagrams);
connect(unicastsocket, signal(error(qabstractsocket::socketerror)),this, slot(handlesocketerror(qabstractsocket::socketerror)));
qinfo() << "创建单播socket";
}
localaddress = qhostaddress(currentnetinterfacraddress);
unicastport_local = port;
if(port == 0){
if (!unicastsocket->bind(localaddress,qudpsocket::reuseaddresshint|qudpsocket::shareaddress)) {
qcritical() << "单播绑定失败:" << unicastsocket->errorstring();
return -1;
}
}else{
if (!unicastsocket->bind(localaddress, unicastport_local,qudpsocket::reuseaddresshint|qudpsocket::shareaddress)) {
qcritical() << "单播绑定失败:" << unicastsocket->errorstring();
return -1;
}
}
qinfo() << "单播连接成功";
return 1;
}
void myudptools::sendunicastdata(qstring targetaddress, quint16 port, const qbytearray &data)
{
if(unicastsocket){
targetaddress_unicast = qhostaddress(targetaddress);
unicastport_target = port;
if(unicastsocket->writedatagram(data,targetaddress_unicast,unicastport_target) == -1){
qcritical() << "发送单播数据失败:" << unicastsocket->errorstring();
}
}
}
int myudptools::exitunicast()
{
if (unicastsocket) {
unicastsocket->close();
qcritical() << "退出成功";
return 1;
}else{
qcritical() << "已退出单播,无需再退出";
return -1;
}
}
//===========================组播相关方法实现===========================
int myudptools::setupmulticast(qnetworkinterface *netinterface,qstring currentnetinterfacraddress, qstring groupaddress, quint16 port)
{
if(!multicastsocket){
multicastsocket = new qudpsocket(this);
connect(multicastsocket, &qudpsocket::readyread, this, &myudptools::processpendingdatagrams);
connect(multicastsocket, signal(error(qabstractsocket::socketerror)),this, slot(handlesocketerror(qabstractsocket::socketerror)));
qinfo() << "创建组播socket";
}
localaddress = qhostaddress(currentnetinterfacraddress);
multicastgroupaddress = qhostaddress(groupaddress);
multicastport_local = port;
if(!multicastgroupaddress.ismulticast()){
qwarning() << "不是组播地址";
return -1;
}
if(port == 0){
if(!multicastsocket->bind(localaddress,qudpsocket::reuseaddresshint|qudpsocket::shareaddress)){
qcritical() << "组播绑定失败:" << multicastsocket->errorstring();
return -1;
}
}else{
if(!multicastsocket->bind(localaddress,multicastport_local,qudpsocket::reuseaddresshint|qudpsocket::shareaddress)){
qcritical() << "组播绑定失败:" << multicastsocket->errorstring();
return -1;
}
}
multicastsocket->setmulticastinterface(*netinterface);
if(!multicastsocket->joinmulticastgroup(multicastgroupaddress,*netinterface)){
qcritical() << "加入组播失败:" << multicastsocket->errorstring();
return -1;
}
qinfo() << "组播连接成功";
return 1;
}
void myudptools::sendmulticastdata(qstring targetaddress, quint16 port, const qbytearray &data)
{
if(multicastsocket){
multicastgroupaddress = qhostaddress(targetaddress);
multicastport_target = port;
if(multicastsocket->writedatagram(data,multicastgroupaddress,multicastport_target) == -1){
qcritical() << "发送组播数据失败:" << multicastsocket->errorstring();
}
}
}
int myudptools::exitmulticast()
{
if (multicastsocket) {
multicastsocket->leavemulticastgroup(multicastgroupaddress);
multicastsocket->close();
qinfo() << "退出组播成功";
return 1;
}else{
qcritical() << "已退出组播,无需再退出";
return -1;
}
}
//===========================广播相关方法实现===========================
int myudptools::setupbroadcast(qstring currentnetinterfaceaddress,qstring targetaddress, quint16 port)
{
if (!broadcastsocket) {
broadcastsocket = new qudpsocket(this);//将父对象设置为当前对象
connect(broadcastsocket, &qudpsocket::readyread, this, &myudptools::processpendingdatagrams);
connect(broadcastsocket, signal(error(qabstractsocket::socketerror)),this, slot(handlesocketerror(qabstractsocket::socketerror)));
qinfo() << "创建广播socket";
}
localaddress = qhostaddress(currentnetinterfaceaddress);
broadcastport_local = port;
broadcastgroupaddress = qhostaddress(targetaddress);
if(!broadcastgroupaddress.isbroadcast()){
qcritical() << "不是广播地址";
return -1;
}
if(port == 0){
if (!broadcastsocket->bind(localaddress,qudpsocket::reuseaddresshint|qudpsocket::shareaddress)) {
qcritical() << "广播绑定失败:" << broadcastsocket->errorstring();
return -1;
}
}else{
if (!broadcastsocket->bind(localaddress, broadcastport_local,qudpsocket::reuseaddresshint|qudpsocket::shareaddress)) {
qcritical() << "广播绑定失败:" << broadcastsocket->errorstring();
return -1;
}
}
qinfo() << "连接成功";
return 1;
}
void myudptools::sendbroadcastdata(qstring targetaddress, quint16 port, const qbytearray &data)
{
if(broadcastsocket){
broadcastgroupaddress = qhostaddress(targetaddress);
broadcastport_target = port;
if(broadcastsocket->writedatagram(data,broadcastgroupaddress,broadcastport_target) == -1){
qcritical() << "发送广播数据失败:" << broadcastsocket->errorstring();
}
}
}
int myudptools::exitbroadcast()
{
if (broadcastsocket) {
broadcastsocket->close();
qinfo() << "退出成功";
return 1;
}else{
qdebug() << "已退出广播,无需再退出";
return -1;
}
}
//===========================添加或刷新combox的网卡信息函数实现===========================
void myudptools::init_or_flash_netinterfaceinfotocombox(qlist<qnetworkinterface> &list, qcombobox *combox)
{
list.clear();
combox->clear();
qlist<qnetworkinterface> interfacelist = qnetworkinterface::allinterfaces();
foreach (qnetworkinterface nif, interfacelist) {
// 检查网卡是否有效并已经启用
if (nif.isvalid() && nif.flags().testflag(qnetworkinterface::isup)) {
// 将已经启用的网卡名称添加到列表中
list.append(nif);
qlist<qnetworkaddressentry> entries = nif.addressentries();
foreach (qnetworkaddressentry entry, entries) {
if (entry.ip().protocol() == qabstractsocket::ipv4protocol) {
combox->additem(entry.ip().tostring());
}
}
}
}
}
//===========================接收数据槽函数实现===========================
void myudptools::processpendingdatagrams()
{
qudpsocket *socket = qobject_cast<qudpsocket *>(sender());//sender()发送信号的对象的指针
if (!socket) return;
while (socket->haspendingdatagrams()) {
#if 1
qbytearray datagram;
datagram.resize(socket->pendingdatagramsize());
socket->readdatagram(datagram.data(), datagram.size());
qdebug() << "received data:" << datagram;
emit sig_recvdata(datagram);
#else
qnetworkdatagram datagram = socket->receivedatagram();
qbytearray data = datagram.data();
emit sig_recvdata(data);
#endif
}
}
//===========================udp错误日志信息===========================
void myudptools::handlesocketerror(qabstractsocket::socketerror socketerror)
{
qudpsocket *socket = qobject_cast<qudpsocket *>(sender());
if (!socket) return;
switch (socketerror) {
case qabstractsocket::hostnotfounderror:
qcritical() << "host not found error:" << socket->errorstring();
break;
case qabstractsocket::connectionrefusederror:
qcritical() << "connection refused error:" << socket->errorstring();
break;
case qabstractsocket::datagramtoolargeerror:
qcritical() << "datagram too large error:" << socket->errorstring();
break;
default:
qcritical() << "socket error:" << socket->errorstring();
break;
}
}
注意:
发表评论