赞
踩
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();
- }
- }
监听函数:
- 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); //数据处理函数,包括解包和业务流程等操作
- }
- }
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。