Qt下基于QUdpSocket实现指定源组播

  1. SSM指定源组播与ASM任意源组播基础概念

ASM-任意源组播,(IGMP-V2协议)

在这种模型下,任何发送方可以发送给任何组。在路由器角度上看,只要接收方“注册”了自己属于组播,任何发送方(任何源)的数据都会分到接收方。

SSM-指定源组播,(IGMP-V3协议)

接收方在“注册”自己加入组的同时,还会告诉路由器只接受某几个发送方(指定源),包括一个组地址和一个源IP地址。在这种模型下,其实任何发送方还是可以发送给任何组的。只是路由器会根据注册信息里的只把“合法源”的数据给到接收方。

从网络配置人员的角度看SSM避免了ASM部署的复杂性,从程序员角度看,SSM要比ASM麻烦一点点就是在加入组播的“注册”过程中,要把“源”的IP信息加进去。可能是我孤陋寡闻可能是Qt真的没考虑这个SSM的应用,至少从Qt4.8.X直到用的Qt5.9.X的Qt封装的QUdpSocket感觉没有支持的特别到位。

  1. .pro文件

除了network,windows还要多包一个LIBS += -lWs2_32


QT       += core gui network

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = D2dRecv
TEMPLATE = app

# The following define makes your compiler emit warnings if you use
# any feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0
win32 {
LIBS += -lWs2_32
}
unix{
}
SOURCES += \
        main.cpp \
        dialog.cpp

HEADERS += \
        dialog.h

FORMS += \
        dialog.ui
  1. 直接上代码

核心是用QUdpSocket的socketDescriptor()函数得到,最基础的socket号,让标准Socket的setsockopt设置“注册”过程,因为linux和windows的socket函数用的不一样,各实现以下就好了。

  1. Dialog.h

#ifndef DIALOG_H
#define DIALOG_H

#include 
#include 
#include 

namespace Ui {
class Dialog;
}

class Dialog : public QDialog
{
    Q_OBJECT

public:
    explicit Dialog(QWidget *parent = 0);
    ~Dialog();
     QUdpSocket recvSock;//"QHostAddress::Any"""

     int AddSourceMembership(QUdpSocket& socket, QString groupIP, QString localIP, QStringList& groupSourceList);
     QHostAddress sendAdr;
     quint16 sendPort;
private slots:
    void on_pushButton_clicked();
    void on_pushButton_2_clicked();

public slots:
    void RecvFromServer();
private:
    Ui::Dialog *ui;
};

#endif // DIALOG_H
  1. Dialog.cpp

#include "dialog.h"
#include "ui_dialog.h"
#include 

Dialog::Dialog(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::Dialog)
{
    ui->setupUi(this);

}

Dialog::~Dialog()
{
    delete ui;
}

//不论是linux还是windows,Qt目前都没有加入SSM的函数,只能自己通过C的socket操作补全,指定源
#ifdef WIN32
// 注意不可以在之前包含"windows.h",否则会导入"winsock.h",里面宏的定义不一致
//#include "windows.h"
#include 
#include 
//如果是VC6还需要自己定义一个ip_mreq_source结构体和#define IP_ADD_SOURCE_MEMBERSHIP 15
//struct ip_mreq_source {
//  struct in_addr imr_multiaddr;
//  struct in_addr imr_sourceaddr;
//  struct in_addr imr_interface;
//};
#else
//linux下的实现基本类似,区别仅是包含的头文件不同。win10下Qt5.9没问题。银河麒麟4.0.2下Qt5.7没问题
#include 
#include 
#include 
#include 
#include 
#include 

#define closesocket close

typedef int     BOOL;
typedef unsigned int DWORD;
typedef int     SOCKET;

#define INVALID_SOCKET -1
#define FALSE   0
#define TRUE    1
#define stricmp strcasecmp
#define ERROR_SUCCESS           0x0
#endif
// 添加指定源组播地址
int Dialog::AddSourceMembership(QUdpSocket& socket, QString groupIP, QString localIP, QStringList& groupSourceList)
{
    // inet_addr将字符串形式的IP转换为整数
    //为Socket设置组播Interface //绑定指定ip来接收组播组信息 Qt的QNetworkInterface也TM不好用,还是要回到C上
    in_addr       addr;
    addr.s_addr = inet_addr(localIP.toStdString().c_str());
    int ret=0;
    ret = setsockopt( socket.socketDescriptor(), IPPROTO_IP, IP_MULTICAST_IF,(const char*)&addr, sizeof(addr));
    QString firstip=groupSourceList.at(0);
    if(firstip.length()>4)
    {
        ip_mreq_source mcast;
#ifdef WIN32
        mcast.imr_interface.S_un.S_addr = inet_addr(localIP.toStdString().c_str());
        mcast.imr_multiaddr.S_un.S_addr = inet_addr(groupIP.toStdString().c_str());
#else

        mcast.imr_interface.s_addr = inet_addr(localIP.toStdString().c_str());
        mcast.imr_multiaddr.s_addr = inet_addr(groupIP.toStdString().c_str());
#endif
        // 多个组播源依次加入
        foreach (QString source, groupSourceList)
        {
            qDebug() << source;
#ifdef WIN32
            mcast.imr_sourceaddr.S_un.S_addr = inet_addr(source.toStdString().c_str());
#else
            mcast.imr_sourceaddr.s_addr = inet_addr(source.toStdString().c_str());
#endif
            ret = setsockopt(socket.socketDescriptor(), IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP, (char*)&mcast, sizeof(mcast));
        }
    }else
    {
        struct ip_mreq mcast;
        mcast.imr_multiaddr.s_addr = inet_addr(groupIP.toStdString().c_str());
        mcast.imr_interface.s_addr = inet_addr(localIP.toStdString().c_str());
        ret = setsockopt(socket.socketDescriptor(), IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mcast, sizeof(mcast));
    }
    return ret;
}

void Dialog::RecvFromServer()
{
    char acDataBuf[65536];
    int iDataLen=65536;
    while (recvSock.hasPendingDatagrams())
    {
        memset(acDataBuf,0,65536);
        QHostAddress sendhost ;
        quint16 sendport = 0;
        int len = recvSock.readDatagram(acDataBuf, iDataLen,&sendhost,&sendport);
        if(len < 0)
        {
            continue;
        }else
        {
            QString test= acDataBuf;
            test+="\nsendIP:";
            test+=sendhost.toString();
            test+="\nsendPort:";
            test+=QString::number(sendport, 10);
            test+="\nrecvlength:";
            test+=QString::number(len, 10);
            QDateTime begin_time = QDateTime::currentDateTime();//获取系统现在的时间

            QString begin =begin_time .toString("\nyyyy.MM.dd hh:mm:ss.zzz ddd");
            test+=begin;
            ui->textEdit->setText(test);

            //模拟 接收后的处理、发送

            //            test+="\nsendTime:";
            //            QDateTime current_time = QDateTime::currentDateTime();
            //            QString currentTime = current_time.toString("yyyy-MM-dd hh:mm:ss.zzz ddd");
            //            test+=currentTime;
            //            QByteArray send_data=test.toUtf8();

            //            recvSock.writeDatagram(send_data,sendAdr,sendPort);
            //            ui->textEdit_2->setText(test);

        }
    }
}

void Dialog::on_pushButton_clicked()
{
    QHostAddress RecvAddress = QHostAddress(ui->lineEdit->text());
    quint16 RecvPort = ui->lineEdit_2->text().toInt();
    QHostAddress LocalAddress = QHostAddress(ui->lineEdit_3->text());

    QString SourceIPs = ui->lineEdit_4->text();
    QStringList SourceIPList =SourceIPs.split(";");

    //端口复用的形式绑定
    //recvSock.bind(LocalAddress,RecvPort,QUdpSocket::ShareAddress| QUdpSocket::ReuseAddressHint);
    //这个地方bind 什么跟操作系统的特性也TM有关系!AnyIPv4还是组播地址,无论如何理论上 不能是本地IP,不然就变单播了。
    bool ok  = recvSock.bind(QHostAddress::AnyIPv4,RecvPort,QUdpSocket::ShareAddress| QUdpSocket::ReuseAddressHint);

    if(!ok){
        QMessageBox::warning(NULL,"ERROR","UDP IP、端口Bind失败");
        recvSock.close();
        ui->pushButton->setEnabled(true);
        return ;
    }

    int isTrue  = AddSourceMembership(recvSock,RecvAddress.toString(),LocalAddress.toString(),SourceIPList);
    if(isTrue!=0)
    {
        QMessageBox::warning(NULL,"ERROR","UDP加入组播失败");
        recvSock.close();
        ui->pushButton->setEnabled(true);
        return ;
    }
    else
    {
        //设置ttl
        char ttl = 16;
        recvSock.setSocketOption(QAbstractSocket::MulticastTtlOption,ttl);
        //如果本地收不到,理论上还要设置一下Loop,我印象是不用,默认是开允许回环的。
        /*
假如在同一台计算机上有两个应用程序,并且加入了同一个组播。这两个程序,一个允许回环,一个阻断回环,则会有如下现象:
windows下,允许方不能发向阻断方,但阻断方可以发向允许方;
linux下,允许方可以发向阻断方,但阻断方不能发向允许方。
*/
        //设置Socket的接收缓冲区8M足够大了。
        recvSock.setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption,1024*1024*8);

        //基本上我能想的UDP组播的坑都总结写在这个程序里了。有机会再讨论。[email protected]
        connect(&recvSock, SIGNAL(readyRead()),this, SLOT(RecvFromServer()));

        //设置发送地址,同一个socket又发又收没毛病。
        sendAdr = QHostAddress(ui->lineEdit_5->text());
        sendPort=ui->lineEdit_6->text().toUShort();

        //如果一切顺利,就不要再搞一次设置了。
        ui->pushButton->setEnabled(false);
    }
}

void Dialog::on_pushButton_2_clicked()
{
    QByteArray send_data=ui->textEdit_2->toPlainText().toUtf8();
    int sendi = recvSock.writeDatagram(send_data,sendAdr,sendPort);
    qDebug() <textEdit_2->toPlainText()<<":"<
  1. main.cpp

#include "dialog.h"
#include 

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Dialog w;
    w.show();

    return a.exec();
}
  1. dialog.ui就算了

上传了资源直接下载吧,https://download.csdn.net/download/houmingyang/87610587

qt5.9能编过。

  1. 总结一下

本质上SSM这个事情不复杂,就是window下和linux下不太一样,甚至不同的linux版本里,对这协议的实现上感觉还是有细微的差距的。这个qt程序只是最简单的示意以下,很多具体问题要具体分析。其实都已经到直接些socket了我更倾向用原始的c/c++实现,有什么问题更容易发现和调整。qt封装的没问题,就是遇到稍微复杂具体问题的时候需要结合tcpdump的抓包具体分析调整。

PS:补充说下bind端口

  1. bind的时候不论如何不要用ip地址,有的操作系统支持bind组播地址,大部分保险还是用0.0.0.0吧。bind主要是绑定的端口,不是ip。除非想用udp单播。

  1. 设置组播用哪块网卡不是用bind函数,而是用选好的ip去设置socket的interface。

  1. 对于发送的socket不bind也可以,如果bind了,就不是系统随机分配发送端口了,而是用bind的端口发送了。

  1. 关于端口要有个基本概念:每个TCPIP的包,其实包含两个端口信息,一个是发送的socket的源端口(发送的时候如果不指定,系统会随机分配一个),一个是接收的socket要bind的目的端口。

你可能感兴趣的:(tcp/ip,c++,qt)