QUdpSocket 丢包测试与解决

Qt的信号与槽是很好的去耦合机质,但在实际使用中,要特别注意它的性能问题。信号与槽不适合非常密集的触发,切记越靠近动态语言的东西(典型的是Qt的元对象系统 meta-object)性能肯定越差。

本次记录的测试,即在生产环境遇到的典型案例。含有煤层断面、地下水的回波是勘探中重要的传感器数据。一般由多个传感器汇聚到计算终端(上位机)上,进行模型解算。以前一直在上位机上采用TCP Server, 板卡连接tcp 端口发东东,没有遇到丢包的现象。近期,由于传感器升级,其接口为UDP格式,不得不采用UDP Server收集数据,遇到了丢包问题。

现象

声波卡发来数据包,包头含有计数器,因此,可以通过比较收到的包个数和计数器的数值,确定有木有丢包。发包程序是SoC的Linux,网络连接是4G。

参照以前TcpSocket的套路,使用信号与槽,响应readyRead直接读取。由于UDP包与包是天然分割开的,不会类似TCPSocket那样连接为一个没头没尾的整体,读取起来简单多了。但是,上位机处理程序出来的结果总是不对,检查发现,数据相当不连续,每10000个包,能丢掉1000~3000个!

这显然是不行的,联系厂家,改回Tcp。可是厂家会拒绝修改,理由是他们出场测试好了,没有问题。用厂家的windows控制台测试程序测试,真的一个不丢。

分析

由于厂家给的程序没问题,那问题出在上位机的程序上。难道是QUDPSocket不能使用信号与槽?不对啊,Tcp信号与槽,以前百兆视频都Ok的,因为它有Buffer的——查一查文档:

[virtual] void QAbstractSocket::setReadBufferSize(qint64 size)
Sets the size of QAbstractSocket's internal read buffer to be size bytes.
If the buffer size is limited to a certain size, QAbstractSocket won't buffer more than this size of data. Exceptionally, a buffer size of 0 means that the read buffer is unlimited and all incoming data is buffered. This is the default.
This option is useful if you only read the data at certain points in time (e.g., in a real-time streaming application) or if you want to protect your socket against receiving too much data, which may eventually cause your application to run out of memory.
Only QTcpSocket uses QAbstractSocket's internal buffer; QUdpSocket does not use any buffering at all, but rather relies on the implicit buffering provided by the operating system. Because of this, calling this function on QUdpSocket has no effect.
See also readBufferSize() and read().

看到了最后一句“ QUdpSocket does not use any buffering at all, but rather relies on the implicit buffering provided by the operating system. Because of this, calling this function on QUdpSocket has no effect.”
原来,Qt的QUDPSocket是不用Buffer的。这就不奇怪了,信号与槽的延迟大,肯定会丢包。

测试

为了抛开网络等因素,比较信号与槽、简单阻塞循环、windows本地API三者的UDP接收性能,我们做了一个本地localhost下的极限测试程序。

测试方法:
每20毫秒发一波,一波100包,每包在4KB以内。测试收到10000个包时,计数的差值(发送了多少包),以体现丢包情况。

1 笔记本计算机环境

QUdpSocket LOOP:
Send 10277, Recv 10000, Lost 277.
QUdpSocket Signal and Slots:
Send 12482, Recv 10000, Lost 2482.
Local Socket :
Send 10003, Recv 10000, Lost 3.

2 高性能计算机环境

QUdpSocket LOOP:
Send 10028, Recv 10000, Lost 28.
QUdpSocket Signal and Slots:
Send 10247, Recv 10000, Lost 247.
Local Socket :
Send 10000, Recv 10000, Lost 0.

改进

  1. 避免动态内存分配。不要使用 QByteArray等动态结构,而是老实的用C的指针接口。
  2. 避免额外的参数获取(如源地址、端口)。
    可显著改善性能,结果(高性能计算机):

QUdpSocket LOOP:
Send 10000, Recv 10000, Lost 0.
QUdpSocket Signal and Slots:
Send 10000, Recv 10000, Lost 0.
Local Socket :
Send 10000, Recv 10000, Lost 0.

3 结论(2022补充)

  1. 丢包与计算机CPU的性能关系很大。像便携式笔记本电脑,丢包非常严重。高性能计算机,就好的多。
  2. 使用信号与槽的时效性最差,其次为 QUdpSocket的Loop模式。
  3. 使用本地Socket API,性能最好,在i7上一个不丢。

尽管UDP协议不保证数据完整性,但各个框架应该尽可能地保证高性能。QUdpSocket应该提供缓存,以便大幅度减少丢包。

2022年,测试了Qt6,发现在性能上有所加强。另外,注意Qt readDatagram 使用分配好的内存,会显著提高性能。在我的 i7 十代计算机上,Loop模式已经和本地没有差别了。

4 UDP之外的选择

在2022年,尝试使用以太网协议直接进行吞吐,效果很好。这种情况适用于网线直连的情况。请参考这里:
https://goldenhawking.blog.csdn.net/article/details/126292692

附件-测试程序

test.pro

QT -= gui
QT += network

CONFIG += c++11 console
CONFIG -= app_bundle


QMAKE_LIBS += -lws2_32
DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += \
	main.cpp

HEADERS += \
	qtest.h

qtest.h

#ifndef ALLINONE_H
#define ALLINONE_H
#include <QCoreApplication>
#include <QThread>
#include <QUdpSocket>
#include <QHostAddress>
#include <QTextStream>
#include <QNetworkDatagram>
#include <windows.h>
static const int tests = 10000;
static const int port = 23456;
static const int period = 20;
static const int batch = 100;
static const int packsize = 512;//pack size in 8 bytes
/*!
 * \brief test_Loop Test QUDPSocket using simple loop.
 * \return total send packages.
 */
inline int test_Loop()
{
	qint64 startID = -1, endID = -1;
	QUdpSocket * p = new QUdpSocket;
	char revdata[packsize*8];
	const long long * d = reinterpret_cast<const long long *>(revdata);
	if (p->bind(QHostAddress::LocalHost,port))
	{
		for (int i=0;i<tests;++i)
		{
			while (!(p->hasPendingDatagrams()));
				p->waitForReadyRead();
			int sz = p->readDatagram(revdata,packsize*8);
			if (sz<8)
				continue;
			if (d)
			{
				if (startID==-1)
					startID = *d;
				endID = *d;
			}
		}
		p->close();
	}
	p->deleteLater();
	return endID - startID + 1;
}

/*!
 * \brief The TestObj class test QUDPSocket using signal-slots.
 */
class TestObj:public QObject
{
	Q_OBJECT
public:
	explicit TestObj(QObject * pa = nullptr):QObject(pa){
		connect (this,&TestObj::_cmd_start,this,&TestObj::slot_start,Qt::QueuedConnection);
	}
	bool isFinished() const {return m_finished;}
	int startID() const {return m_startID;}
	int endID() const {return m_endID;}
protected slots:
	void readPdDt(){
		static char revdata[packsize*8];
		static const long long * d = reinterpret_cast<const long long *>(revdata);
		while (m_sock->hasPendingDatagrams())
		{
			int sz = m_sock->readDatagram(revdata,packsize*8);
			if (sz<8)
				continue;
			if (++m_total>tests)
			{
				m_sock->close();
				m_finished = true;
				QThread::currentThread()->quit();
				break;
			}
			if (d)
			{
				if (m_startID==-1)
					m_startID = *d;
				m_endID = *d;
			}
		}
	}
public:
	void start(){
		emit _cmd_start();
	}
private slots:
	void slot_start(){
		m_sock = new QUdpSocket(this);
		connect (m_sock,&QUdpSocket::readyRead,this,&TestObj::readPdDt);
		if (!m_sock->bind(QHostAddress::LocalHost,port))
		{
			m_sock->deleteLater();
			return;
		}
	}
signals:
	void _cmd_start();
private:
	int m_startID = -1;
	int m_endID = -1;
	int m_total = 0;
	bool m_finished = false;
	QUdpSocket * m_sock = nullptr;
};


/*!
 * \brief local_test test udp recv using local socket API
 * \return total send
 */
inline int local_test()
{
	//init wsa
	const WORD sockVision = MAKEWORD(2,2);
	WSADATA wsadata;
	//sockets
	SOCKET sock;
	if( WSAStartup(sockVision,&wsadata) != 0 )
	{
		printf("WSA initial failed.\n");
		return 0;
	}

	//Create sock
	sock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
	if(sock == INVALID_SOCKET)
	{
		printf("socket creating failed.\n");
		return 0;
	}

	struct sockaddr_in sin;
	//Bind to Localhost:port
	sin.sin_family = AF_INET;
	sin.sin_port = htons(port);
	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//local host
	if( bind(sock,(LPSOCKADDR)&sin,sizeof(sin)) == SOCKET_ERROR )
	{
		printf("Can not bind to Address.\n");
		return 0;
	}
	struct sockaddr_in remoteAddr;
	int nAddrlen = sizeof(remoteAddr);
	int startID = -1, endID = -1;
	int total = 0;
	char revdata[packsize*8];
	const long long * pL = reinterpret_cast<const long long *>(revdata);
	while (total<tests)
	{
		int rev  = recvfrom(sock,revdata,packsize*8,0,(SOCKADDR*)&remoteAddr,&nAddrlen);
		if(rev >=8)
		{
			if (startID==-1)
				startID = *pL;
			endID = *pL;
			++total;
		}
	}
	closesocket(sock);
	WSACleanup();
	return endID - startID + 1;
}


/*!
 * \brief The sendingThread class send testing packages.
 */
class sendingThread: public QThread{
	Q_OBJECT
public:
	explicit sendingThread(QObject * pa = nullptr):QThread(pa){}
	void stop(){term = true;}
protected:
	void run() override{
		qint64 SendBuf[packsize] = {0};
		const char * dtp = reinterpret_cast<const char *>(SendBuf);

		int iResult;
		WSADATA wsaData;

		SOCKET SendSocket = INVALID_SOCKET;
		sockaddr_in RecvAddr;
		//----------------------
		// Initialize Winsock
		iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
		if (iResult != NO_ERROR) {
			wprintf(L"WSAStartup failed with error: %d\n", iResult);
			return;
		}

		//---------------------------------------------
		// Create a socket for sending data
		SendSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
		if (SendSocket == INVALID_SOCKET) {
			wprintf(L"socket failed with error: %ld\n", WSAGetLastError());
			WSACleanup();
			return;
		}
		//---------------------------------------------
		RecvAddr.sin_family = AF_INET;
		RecvAddr.sin_port = htons(port);
		RecvAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

		//---------------------------------------------
		// Send a datagram to the receiver
		while(!term)
		{
			for (int i=0;i<batch;++i)
			{
				const int BufLen = sizeof(SendBuf) - rand()%(sizeof(SendBuf)-100);
				++SendBuf[0];
				iResult = sendto(SendSocket,
								 dtp, BufLen, 0, (SOCKADDR *) & RecvAddr, sizeof (RecvAddr));
				if (iResult == SOCKET_ERROR) {
					wprintf(L"sendto failed with error: %d\n", WSAGetLastError());
					closesocket(SendSocket);
					WSACleanup();
					return ;
				}
			}
			if (period>0)
				msleep(period);
		}
		//---------------------------------------------
		// When the application is finished sending, close the socket.
		wprintf(L"Finished sending. Closing socket.\n");
		iResult = closesocket(SendSocket);
		if (iResult == SOCKET_ERROR) {
			wprintf(L"closesocket failed with error: %d\n", WSAGetLastError());
			WSACleanup();
			return;
		}
		//---------------------------------------------
		// Clean up and quit.
		wprintf(L"Exiting.\n");
		WSACleanup();

	}
private:
	bool term = false;
};
#endif // ALLINONE_H

main.cpp

#include "qtest.h"

int main(int argc, char *argv[])
{
	QCoreApplication a(argc, argv);
	//Start Sending
	sendingThread * tsend = new sendingThread;
	tsend->start(QThread::HighestPriority);
	QThread::msleep(3000);
	printf("Start...\n");

	//test UDP loop
	int total_loop = test_Loop();
	printf("QUdpSocket LOOP:\n\tSend %d, Recv %d, Lost %d.\n",
							   total_loop,tests,total_loop-tests );

	QThread::msleep(3000);
	printf("Start...\n");

	//test Class
	QThread * trecv = new QThread;
	TestObj * sobj = new TestObj;
	sobj->moveToThread(trecv);
	trecv->start();
	sobj->start();
	trecv->wait();
	sobj->deleteLater();
	trecv->deleteLater();
	const int totaltest_ss = sobj->endID()-sobj->startID()+1;
	printf("QUdpSocket Signal and Slots:\n\tSend %d, Recv %d, Lost %d.\n",
							   totaltest_ss,
							   tests,
							   totaltest_ss-tests );

	QThread::msleep(3000);
	printf("Start...\n");

	//Test Local
	const int local_total = local_test();
	printf("Local Socket :\n\tSend %d, Recv %d, Lost %d.\n",
							   local_total,
							   tests,
							   local_total-tests );
	//end
	tsend->stop();
	tsend->wait();
	tsend->deleteLater();
	QCoreApplication::processEvents();
	getchar();
	return 0;
}

  • 15
    点赞
  • 108
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁劲犇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值