Qt—网络编程之TCP通信及常见问题
本文为作者在开发项目时对Qt的TCP通信部分的总结,主要包含TCP服务器收发数据的demo,解决TCP拆包和黏包问题的解决方案,以及对接收到的QByteArray数据的转换。
简介
TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接,也就是我们常听到的三次握手。TCP的目的是实现快速、安全的信息传递,因此在协议中针对针对数据安全做了很多处理,很适合应用在一些对安全性要求高的场合。而UDP是非连接的协议,在形式上有点类似串口,适合传输语音、视频等流量大的任务。
一、Qt中TCP通信基本用法
??TCP 通信必须先建立 TCP 连接,通信端分为客户端和服务端。服务端通过监听某个端口来监听是否有客户端连接到来,如果有连接到来,则建立新的 socket 连接;客户端通过 ip 和port 连接服务端,当成功建立连接之后,就可进行数据的收发了。 ??由于这一块网上的资料很丰富,我就不做过多介绍,我是参考的正点原子 Qt 开发教程,这里将我的TCP服务器源码贴出来,大家有需要的在此基础上进行修改。后面我主要介绍一下我在开发过程中实际遇到的问题与解决方案,仅供参考。
1. 在 .pro文件中添加 network
QT += core gui network
2. 封装好的 mytcpserver.h
#ifndef MYTCPSERVER_H
#define MYTCPSERVER_H
#include <QTcpServer>
#include <QTcpSocket>
#include <QObject>
class TcpServer : public QObject
{
Q_OBJECT
public:
explicit TcpServer(QObject *parent = nullptr);
private:
QTcpServer *tcpServer_6010;
QTcpSocket *tcpSocket_6010;
QTcpServer *tcpServer_6030;
QTcpSocket *tcpSocket_6030;
public slots:
void startListen();
void stopListen();
void clientConnected_6010();
void clientConnected_6030();
void receiveMessages_6010();
void sendMessages_6030(QByteArray);
signals:
void signal_clientConnected_6010();
void signal_clientConnected_6030();
void signal_receiveMsg_6010(QByteArray);
};
#endif
3. 封装好的 mytcpserver.cpp
#include "mytcpserver.h"
#include <QDebug>
TcpServer::TcpServer(QObject *parent) : QObject(parent)
{
tcpServer_6010 = new QTcpServer(this);
tcpSocket_6010 = new QTcpSocket(this);
tcpServer_6030 = new QTcpServer(this);
tcpSocket_6030 = new QTcpSocket(this);
connect(tcpServer_6010, SIGNAL(newConnection()), this, SLOT(clientConnected_6010()));
connect(tcpServer_6030, SIGNAL(newConnection()), this, SLOT(clientConnected_6030()));
}
void TcpServer::clientConnected_6010()
{
tcpSocket_6010 = tcpServer_6010->nextPendingConnection();
emit signal_clientConnected_6010();
connect(tcpSocket_6010, SIGNAL(readyRead()), this, SLOT(receiveMessages_6010()));
}
void TcpServer::clientConnected_6030()
{
tcpSocket_6030 = tcpServer_6030->nextPendingConnection();
emit signal_clientConnected_6030();
}
void TcpServer::startListen()
{
tcpServer_6030->listen(QHostAddress("192.168.116.250"), 6030);
tcpServer_6010->listen(QHostAddress("192.168.116.250"), 6010);
}
void TcpServer::stopListen()
{
tcpServer_6010->close();
tcpServer_6030->close();
if(tcpSocket_6010->state() == tcpSocket_6010->ConnectedState)
tcpSocket_6010->disconnectFromHost();
if(tcpSocket_6030->state() == tcpSocket_6030->ConnectedState)
tcpSocket_6030->disconnectFromHost();
}
void TcpServer::receiveMessages_6010()
{
static uint receiveLen=0;
static QByteArray receiveData;
QByteArray receiveBuf = tcpSocket_6010->readAll();
uint messageLen = receiveBuf.size();
receiveLen += messageLen;
if(receiveLen < 16006)
{
receiveData.append(receiveBuf);
}
else if(receiveLen == 16006)
{
receiveData.append(receiveBuf);
emit signal_receiveMsg_6010(receiveData);
receiveLen=0;
receiveData.clear();
}
else if(receiveLen > 16006)
{
while(receiveLen > 16006)
{
qDebug()<<receiveBuf.size()<<endl;
receiveData.append(receiveBuf);
receiveBuf = receiveData.right(16007);
receiveData.truncate(16006);
emit signal_receiveMsg_6010(receiveData);
receiveLen = receiveLen-16006;
}
}
}
void TcpServer::sendMessages_6030(QByteArray sendData)
{
if(NULL == tcpSocket_6030)
return;
if(tcpSocket_6030->state() == tcpSocket_6030->ConnectedState)
tcpSocket_6030->write(sendData);
}
这里我需要使用了两个端口,6030端口用作发送指令,6030端口用作接收数据。我的项目中传输的数据量较大,一包几万字节,所以接收数据的 receiveMessages_6010() 函数已做了对黏包问题的处理。大家可以根据自己的需求做相应的修改。
二、TCP黏包解决方法
1. 问题描述
??TCP客户端使用的是STM32开发的8通道高速数据采集卡,客户端每100ms发送一次数据,每次为16006字节的数据长度。由于TCP传输数据时,为了达到最佳传输效能,数据包的最大长度需要由MSS限定(MSS就是TCP数据包每次能够传输的最大数据分段),超过这个长度会进行自动拆包。也就是说虽然客户端一次发送16006字节数据,但是实际TCP传输时会将16006字节划分为若干小包。我使用wireshark软件抓包时可以看到,数据被拆分成长度为1440的数据包(不满1440则单独发送)。
2. TCP拆包和黏包现象
??我们来看一下数据经过TCP传输时可能出现的几种情况:
- 接收端正常收到两个数据包,即没有发生拆包和粘包的现象。
- 接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
- 这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。
3. 解决方法
??在使用Qt编写TCP服务器端程序时,Qt提供的TCP接收函数 readAll() 并非一次读取客户端全部数据,也不是读取客户端的每小包数据,而是读取TCP服务器的接收缓冲区的全部数据,这里算是Qt的一个坑,因为乍一看 readAll() 不就是读取全部数据嘛,而官方文档又没有给出具体解释。
qDebug()<<tcpSocket_6010->byteAvailable()<<endl;
QByteArray receiveBuf = tcpSocket_6010->readAll();
qDebug()<<tcpSocket_6010->byteAvailable()<<endl;
??其实仔细想一下,被拆包的每包数据都被封装成相同的格式进行传输,TCP协议并没有提供任何标识,接收端也压根无法自动判别哪些包属于完整的一包数据。 ??知道了接收函数 readAll() 的原理,再加上我们已知客户端发送的每包数据长度为 16006 字节,那么我们不就可以手动计算接收数据的长度,然后将这些数据拼接合成嘛。确实应该这么做,但是别忘了TCP还有黏包的问题,也就是TCP传输的数据包可能出现粘合在一起的现象,本次要传输的数据和下一次传输的数据被粘合在一起,那么我们按长度累加计算接收到的数据长度可能无法获取我们想要的结果。 ??我的解决方法如下:
void TcpServer::receiveMessages_6010()
{
static uint receiveLen=0;
static QByteArray receiveData;
QByteArray receiveBuf = tcpSocket_6010->readAll();
uint messageLen = receiveBuf.size();
receiveLen += messageLen;
if(receiveLen < 16006)
{
receiveData.append(receiveBuf);
}
else if(receiveLen == 16006)
{
receiveData.append(receiveBuf);
emit signal_receiveMsg_6010(receiveData);
receiveLen=0;
receiveData.clear();
}
else if(receiveLen > 16006)
{
while(receiveLen > 16006)
{
qDebug()<<receiveBuf.size()<<endl;
receiveData.append(receiveBuf);
receiveBuf = receiveData.right(16007);
receiveData.truncate(16006);
emit signal_receiveMsg_6010(receiveData);
receiveLen = receiveLen-16006;
}
}
}
??大体思路就是收满16006字节的数据就将数据发送出去,如果发生黏包,数据长度超过16006就对数据进行裁剪,多出来的部分作为下一包数据的开头。经过测试,该方法能够完美解决在传输大量数据时,TCP拆包和黏包导致的数据无法解析的问题,读者可参考此方法自行修改。
三、TCP接收到的QByteArray类型数据的转换
??上述通过 readAll() 函数接收到的数据为 QByteArray 类型,这是一个Qt 自己定义的一种类似于 String 的处理字符串的类,这个类也提供了很多成员函数,方便我们对数据进行转化。
??如果你不需要对接收到的数据进行运算,只是想打印数据,那么可以直接使用 QByteArray 类型。但是如果你需要对数据进做加减乘除,那使用 QByteArray 就不合适了,需要转换成基本数据类型。需要注意上图中 QByteArray 类提供的几个成员函数,比如 toHex() ,它的返回值依然是 QByteArray,也就是说它是将原始的 QByteArray 转换成十六进制的 QByteArray,比如 “255”->“FF”,本质上还是字符串。大家在使用官方提供的成员函数时,一定要看一下函数的返回值,不要想当然了。 ??我在客户端发送的每个数据为两个字节,如 0xFFFF,使用 readAll() 接收到的 QByteArray 类型的数据也只是按字节接收,它并不知道我们一个数据占几个字节,所以实际上 receiveData[0] = 255, receiveData[1] = 255。由于我没有找到现成的可供直接使用的处理函数,所以就手动实现了一下:
for(uint i=0; i<1000; i++)
{
cacheBuf[i] = msg[16*i+4] & 0x000000FF;
cacheBuf[i] |= ((msg[16*i+5] << 8) & 0x0000FF00);
}
总结
即使我在stm32单片机上发送的是int类型的数据,但是在Qt上通过 toInt() 函数时接收不到我想要的数据的,因为32位的单片机中int占两个字节,而Qt中的C++的int类型占4个字节,那么我使用 toInt() 函数来接收数据时,程序就会以4个字节为一个数来接收。这告诉我们,在不同平台之间传输数据的时候,要考虑同种类型的数据,它们的宽度是否一致。
|