C++|UDP通讯使用总结
最近开发了一个小软件,应项目经理强烈要求,通讯是采用UDP,下面作为总结为大家分享一下~
开发环境:Qt 6.8.2
本来我是打算直接用QUdpSocker开发的,想着纯Qt项目,直接使用Qt自带的功能,开发起来肯定会快,果真开发起来很快,但是我发现,使用QUdpSocket自带的消息,当连接两台设备的时候,接收消息就会不及时
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套接字
m_serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
if (m_serverSocket < 0)
{safeCloseSocket("创建UDP 套接字 失败!");return false;
}
2:设置非阻塞模式
u_long mode = 1; // 1=非阻塞,0=阻塞
if(ioctlsocket(m_serverSocket, FIONBIO, &mode) != 0)
{safeCloseSocket("设置非阻塞模式失败!");return false;
}
3:禁止广播回环
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广播
int broadcastEnable = 1;
if (setsockopt(m_serverSocket, SOL_SOCKET, SO_BROADCAST, reinterpret_cast<const char*>(&broadcastEnable), sizeof(broadcastEnable)))
{safeCloseSocket("UDP通讯,启动广播策略,失败!");return false;
}
5:绑定端口
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:开启数据监听线程
m_bDataProcessing = true; //开启数据监听处理
m_threadReceived = std::thread(&UdpSocketManger::ThreadReceiveData, this, m_serverSocket);
m_threadReceived定义:std::thread m_threadReceived
7:开启数据工作线程
因为数据量过大,如果直接在recvfrom中处理数据,可能会影响数据接收的效率,那么在启动监听线程时,同步启动工作线程
m_threadWorker = std::thread(&UdpSocketManger::ThreadWorker, this);
8:监听线程实现
void UdpSocketManger::ThreadReceiveData(int sockfd)
{char buffer[BUFFER_SIZE] = { '\0' }; //接收数据缓冲区while(m_bDataProcessing){sockaddr_storage clientAddr;
#ifdef _WIN32int clientAddrLen = sizeof(clientAddr);
#elsesocklen_t clientAddrLen = sizeof(clientAddr);
#endifssize_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; //无效数据不处理}// 解析IPchar 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* → QByteArrayQByteArray 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方式:
QSet<QString> m_setLocalIPString; //缓存本地IP地址字符串
for (const QHostAddress &addr : QNetworkInterface::allAddresses()) {m_setLocalIPString.insert(addr.toString());
}
9:工作线程实现
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:停止线程
//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:安全关闭线程
void UdpSocketManger::safeCloseSocket(QString sLogTips)
{if (!sLogTips.isEmpty()){
#ifdef _WIN32qDebug() << sLogTips << "| 错误码:" << WSAGetLastError();
#elseqDebug() << sLogTips << "| 错误:" <<strerror(errno);
#endif}if(m_serverSocket != -1){
#ifdef _WIN32closesocket(m_serverSocket);
#elseclose(m_serverSocket);
#endifm_serverSocket = -1; // 标记为已关闭}
}
以上就是使用C++的UDP进行通讯,相比较QUDP来说,性能更高。我最开始使用的是QUDP可能是因为数据量大,导致连接设备过多UDP处理不过来,就换成了C++的原生UDP,效率果然提升了。
我是糯诺诺米团,一名C++开发程序媛~