1.问题描述
在Qt中使用Tcp通讯时的惯常做法是在服务端将QtcpSocket的readyRead信号与处理业务的槽函数关联,这样每当有新的通讯数据时触发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();
}
}
监听函数:文章来源:https://www.toymoban.com/news/detail-802297.html
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循环里的内容写到构造函数里,你会发现软件卡住了!所以,这里的操作是先进入第一个循环等待接入客户端,一旦成功,马上跳入第二个循环。因此,第二个循环才是线程真正接收数据,处理业务的地方。文章来源地址https://www.toymoban.com/news/detail-802297.html
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); //数据处理函数,包括解包和业务流程等操作
}
}
}
}
到了这里,关于QTcpSocket中readyRead信号不实时触发问题解决的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!