最近开发了一个小软件,应项目经理强烈要求,通讯是采用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++开发程序媛~