C++|UDP通讯使用总结

最近开发了一个小软件,应项目经理强烈要求,通讯是采用UDP,下面作为总结为大家分享一下~

开发环境:Qt 6.8.2

本来我是打算直接用QUdpSocker开发的,想着纯Qt项目,直接使用Qt自带的功能,开发起来肯定会快,果真开发起来很快,但是我发现,使用QUdpSocket自带的消息,当连接两台设备的时候,接收消息就会不及时

cpp 复制代码
connect(m_pUdpSocket, &QUdpSocket::readyRead, this, &UdpSocketManger::OnReadyUdpData);

void UdpSocketManger::OnReadyUdpData()
{
    while(m_pUdpSocket->hasPendingDatagrams())
    {
        QNetworkDatagram datagram = m_pUdpSocket->receiveDatagram(); //接收到一条完整的数据包
         if(!datagram.isValid()) continue;
         QString sOutIP = "";
         if(isLocalAddress(datagram.senderAddress(), sOutIP))
         {
             continue; //本机IP地址消息,不处理
         }

         {
             //使用锁机制添加处理数据
             std::lock_guard<std::mutex> lock(m_mutexQueue);
             m_dequeOriginalData.push(UdpPacket{sOutIP, datagram.data()}); //尾部追加数据
         }

     }
}

我还在这里使用开线程的方式接收处理原始数据,如果我要是在OnReadyUdpData这个函数中直接处理,那肯定是会卡死界面的。这个方法果真不行,后来我有尝试将OnReadyUpData这个函数中逻辑直接放到线程中使用,会比使用connect消息的方式快一些,也仅仅是快一些而已。

果断放弃了QUDP,转而使用C++原生的UDP通讯,虽然写起来比较麻烦,但是效率高呀!

C++原生的UDP通讯直接系统调用,延迟更低,吞吐量更高,即使我将QUdp优化到极致,也无法高性能的带动50台设备,毕竟Qt UDP被封装了一层。

下面我为大家分享C++原生UDP,简单版本,仅限于IPV4(IPV6兼容这里就不讲解了,否则太乱),单线程接收数据,单线程处理数据,吞吐量<100肯定是没问题的。

C++ UDP实现逻辑

1:创建UDP套接字

cpp 复制代码
m_serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
if (m_serverSocket < 0)
{
    safeCloseSocket("创建UDP 套接字 失败!");
    return false;
}

2:设置非阻塞模式

cpp 复制代码
u_long mode = 1; // 1=非阻塞,0=阻塞
if(ioctlsocket(m_serverSocket, FIONBIO, &mode) != 0)
{
    safeCloseSocket("设置非阻塞模式失败!");
    return false;
}

3:禁止广播回环

cpp 复制代码
int loop = 0;
if (setsockopt(m_serverSocket, IPPROTO_IP, IP_MULTICAST_LOOP, reinterpret_cast<const char*>(&loop), sizeof(loop)) < 0)
{
        qDebug() << "Failed to disable loopback";
}

4:启动UDP广播

cpp 复制代码
int broadcastEnable = 1;
if (setsockopt(m_serverSocket, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<const char*>(&broadcastEnable), sizeof(broadcastEnable)))
{
    safeCloseSocket("UDP通讯,启动广播策略,失败!");
    return false;
}

5:绑定端口

cpp 复制代码
sockaddr_in serverAddr{};
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
//serverAddr.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡
std::string sIP = m_sLocalIP.toStdString();
inet_pton(AF_INET, sIP.c_str(), &serverAddr.sin_addr);
serverAddr.sin_port = htons(m_nPort);
if (bind(m_serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
     safeCloseSocket("UDP通讯,绑定端口失败!");
     return false;
}

注意:在我的这段代码中屏蔽了代码:serverAddr.sin_addr.s_addr = INADDR_ANY; 那是因为当我的PC机上存在虚拟网卡时,使用UDP服务端广播的消息无法被发送出去

6:开启数据监听线程

cpp 复制代码
m_bDataProcessing = true; //开启数据监听处理
m_threadReceived = std::thread(&UdpSocketManger::ThreadReceiveData, this, m_serverSocket);

m_threadReceived定义:std::thread m_threadReceived

7:开启数据工作线程

因为数据量过大,如果直接在recvfrom中处理数据,可能会影响数据接收的效率,那么在启动监听线程时,同步启动工作线程

cpp 复制代码
m_threadWorker = std::thread(&UdpSocketManger::ThreadWorker, this);

8:监听线程实现

cpp 复制代码
void UdpSocketManger::ThreadReceiveData(int sockfd)
{
    char buffer[BUFFER_SIZE] = { '\0' }; //接收数据缓冲区
    while(m_bDataProcessing)
    {
        sockaddr_storage  clientAddr;
#ifdef _WIN32
    int clientAddrLen = sizeof(clientAddr);
#else
    socklen_t clientAddrLen = sizeof(clientAddr);
#endif
        ssize_t recvLen = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, reinterpret_cast<sockaddr*>(&clientAddr), &clientAddrLen);
        if(recvLen <= 0)
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            continue; //无效数据不处理
        }
        // 解析IP
        char ipStr[INET6_ADDRSTRLEN] = {0};
        auto* addr4 = reinterpret_cast<sockaddr_in*>(&clientAddr);
        inet_ntop(AF_INET, &addr4->sin_addr, ipStr, sizeof(ipStr));
        QString clientIP = QString::fromLatin1(ipStr);
        //过滤自身IP数据
        if(m_setLocalIPString.contains(clientIP))
        {
            continue;
        }
        // 关键转换:char* → QByteArray
        QByteArray data(buffer, recvLen); // 直接构造,避免额外拷贝
        {
            std::lock_guard<std::mutex> lock(m_mutexQueue);
            m_dequeOriginalData.push(UdpPacket{clientIP,data}); //尾部追加数据
        }
    }
    qDebug() << "线程《ThreadReceiveData》,安全结束!";
}

在我的线程实现中,添加了过滤自身IP数据,保证每次处理的有效数据都是非本机传入的。

有人会问:不是已经设置了禁止回环怎么还会接收到本机的消息呢?

当我们在发送一个UDP广播包时,操作系统的网络协议栈会做两件事:

第一件事:将数据包通过物理网卡发送到网络中

第二件事:内向回环,同时协议栈会将这个数据包复制一份,直接"饶回"给本机上所有正在监听目标端口的套接字。

而且,项目经理设计的不合理,服务端和客户端都绑定了一个端口号,能不接收到自己的消息才怪!

8:获取本机IP地址

那么在我的UdpSocketManager构造函数中就需要首先获取本地的地址,用于线程进行对比

为了简便使用的是Qt方式:

cpp 复制代码
QSet<QString> m_setLocalIPString; //缓存本地IP地址字符串
for (const QHostAddress &addr : QNetworkInterface::allAddresses()) {
        m_setLocalIPString.insert(addr.toString());
}

9:工作线程实现

cpp 复制代码
void UdpSocketManger::ThreadWorker()
{
    while(m_bDataProcessing)
    {
        // 从队列获取数据
        UdpPacket packet;
        {
            std::unique_lock<std::mutex> lock(m_mutexQueue);
            if (!m_bDataProcessing) break;
            if(!m_dequeOriginalData.empty())
            {
                packet= std::move(m_dequeOriginalData.front()); //移动而非拷贝数据
                m_dequeOriginalData.pop(); //剔除第一条数据
            }
        }
        // 处理数据(示例:打印客户端信息)
        if(!packet.dataArray.isEmpty())
        {
            //检查当前接收的数据是否有效
            if(this->JudgeValidData(packet.dataArray))
            {
                //数据有效,此时处理有效数据
                this->ProcessingValidData(packet.sSenderIP, packet.dataArray);
            }
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    qDebug() << "线程《ThreadWorker》,安全结束!";
}

10:停止线程

cpp 复制代码
//1: 关闭线程标识
m_bDataProcessing = false;
//2: 等待线程结束
if (m_threadReceived.joinable()) 
{
    m_threadReceived.join();
}
if (m_threadWorker.joinable()) 
{
    m_threadWorker.join();
}
//3: 关闭socket
safeCloseSocket();
//4: 清理所有线程数据
clearTotalThreadData();

11:安全关闭线程

cpp 复制代码
void UdpSocketManger::safeCloseSocket(QString sLogTips)
{
    if (!sLogTips.isEmpty())
    {
#ifdef _WIN32
        qDebug() << sLogTips << "| 错误码:" << WSAGetLastError();
#else
        qDebug() << sLogTips << "| 错误:" <<strerror(errno);
#endif
    }
    if(m_serverSocket != -1)
    {
#ifdef _WIN32
        closesocket(m_serverSocket);
#else
        close(m_serverSocket);
#endif
        m_serverSocket = -1;  // 标记为已关闭
    }
}

以上就是使用C++的UDP进行通讯,相比较QUDP来说,性能更高。我最开始使用的是QUDP可能是因为数据量大,导致连接设备过多UDP处理不过来,就换成了C++的原生UDP,效率果然提升了。

我是糯诺诺米团,一名C++开发程序媛~