QTcpSocket中readyRead信号不实时触发问题解决

1.问题描述

在Qt中使用Tcp通讯时的惯常做法是在服务端将QtcpSocketreadyRead信号与处理业务的槽函数关联,这样每当有新的通讯数据时触发readyRead信号,进而通过槽函数处理业务流程。然而,readyRead信号与客户端的write函数并没有必然的一一对应关系。因此,对一些特别依赖数据实时性的应用场景就会出现通讯中断的情况。

关联readyRead信号的一般形式如下:

QObject::connect(mpSocket,&QTcpSocket::readyRead,this,&TcpServer::slotReadData);

例如,在一次业务流程中,当服务端根据客户端的回复指令下发指令时,由于readyRead信号没有实时触发,导致服务端没有及时收到回令而业务中断(用Wireshark抓包显示数据的确发送成功了)。当下一次重新开始时,才把上一次滞留的回令连带新的回令以粘包的形式发给服务端。此时,本意是完全从头再来,却收到上一次的滞留回令,此时业务逻辑就很有可能出错。在这种情形中,Qt的信号与槽机制参与TCP通讯就存在局限性。我们迫切需要换一种思路去实时获取TCP传输过来的数据。

2.原因分析

首先,必须明确发送端write一次,接收方就会有新数据到达,readyRead()信号就会触发一次,这种理解不对!

发送和接收没有必然一一对应关系。发送端write()函数调用一次,若这一次write了较大数据(2M),那么接收方readyRead()信号往往会触发两次以上,反过来,如果发送方write()函数被调用了两次或是以上,接收方的readyRead信号也可能只调用一次。

所谓的有新数据来,readyRead信号就会触发一次,实际上不是指从发送端有新数据来到接收端计算机,而是数据从接收计算机的Tcp/ip协议栈到达Qt应用程序,即系统io缓冲区到达Qt应用程序,数据从系统到达Qt应用程序readyRead信号就会触发一次。

综上,究其根本是QT的TCP通讯并没有丢数据,只是数据滞留在了io缓冲区!

3.解决方法

本人尝试了起一个线程不断轮询调用QtcpSocket的readAll()或者read()函数,只是偶尔能够全部及时取到TCP通讯数据,很多时候也拿不到io缓冲区的滞留数据。甚至,起两个线程,一个用于收发数据(对应readAll()write()函数),另外一个用队列里的指令驱动业务流程,这样也不行,连最基本的通讯连接都出了问题。

既然QT的接口有局限性,就尝试调用windows自带的socket接口。最终,根据这个思路,调用WinSock的的recv()接口,完美解决问题!

4.关键代码

这里省略了业务流程以及其它不相干的代码,只突出解决问题的关键代码。

先看头文件:

#include <WinSock.h>
class TcpServer : public QThread
{
	Q_OBJECT
public:
	explicit TcpServer(QObject* parent = 0);
	~TcpServer();
	void run();
	bool StartListen(const QHostAddress& address = QHostAddress::Any, quint16 port = 0);
private:
	void ProcData(char revData[], int len);
private:
	SOCKET m_listenSocket;
	SOCKET m_revSocket;  //对应所建立连接的套接字的句柄
};

再看构造函数:

TcpServer::TcpServer(QObject* parent) : QThread(parent)
{
	/*此处省略其它构造内容*/

	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;
	if (WSAStartup(sockVersion, &wsaData) != 0)
	{
		qDebug() << "WSAStartup error"; //WSAStartup返回0表示设置初始化成功
	}
	m_listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //AF_INET表示IPv4,SOCK_STREAM数据传输方式,IPPROTO_TCP传输协议;
	if (m_listenSocket == INVALID_SOCKET)
	{
		WSACleanup();
	}
}

监听函数:

bool TcpServer::StartListen(const QHostAddress& address, quint16 port)
{
	if (8899 == port)
	{
		//绑定IP和端口
		//配置监听地址和端口
		sockaddr_in addrListen;
		addrListen.sin_family = AF_INET;     //指定IP格式
		addrListen.sin_port = htons(8899);   //绑定端口号
		addrListen.sin_addr.S_un.S_addr = INADDR_ANY;  //表示任何IP
		if (bind(m_listenSocket, (SOCKADDR*)&addrListen, sizeof(addrListen)) == SOCKET_ERROR)
		{
			qDebug() << "绑定失败";
			closesocket(m_listenSocket);
		}
		//开始监听
		if (listen(m_listenSocket, 5) == SOCKET_ERROR)
		{
			qDebug() << "监听出错";
			closesocket(m_listenSocket);
		}
		return true;
	}
	else
	{
        return false;
	}
}

关键的地方来了,线程的run()函数。这里之所以要用两个while循环是因为在软件启动后,流程走到accept函数后就一直处于等待状态,如果把第一个while循环里的内容写到构造函数里,你会发现软件卡住了!所以,这里的操作是先进入第一个循环等待接入客户端,一旦成功,马上跳入第二个循环。因此,第二个循环才是线程真正接收数据,处理业务的地方。

void TcpServer::run()
{
	while (true)
	{
		sockaddr_in remoteAddr;   //接收连接到的地址信息
		int remoteAddrLen = sizeof(remoteAddr);
		m_revSocket = accept(m_listenSocket, (SOCKADDR*)&remoteAddr, &remoteAddrLen);  //等待客户端接入,直到有客户端连接上来为止
		if (m_revSocket == INVALID_SOCKET)
		{
			qDebug() << "客户端发出请求,服务器接收请求失败:" << WSAGetLastError();
			closesocket(m_listenSocket);
			WSACleanup();
		}
		else
		{
			qDebug() << "客服端与服务器建立连接成功:" << inet_ntoa(remoteAddr.sin_addr);
			goto out; //去往标识符
		}
	}
out:
	while (true)
	{
		if (m_revSocket != INVALID_SOCKET)
		{
			char revData[1024] = "";
			int res = recv(m_revSocket, revData, 1024, 0);
			if (res > 0)
			{
				qDebug() << "Bytes received:" << res;
				ProcData(revData, res); //数据处理函数,包括解包和业务流程等操作
			}
		}
	}
}

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值