共计 2836 个字符,预计需要花费 8 分钟才能阅读完成。
假如有一个 TCP 服务端,会向连贯到它的 TCP 客户端周期(或随机)发送一个报文。报文由定长的报文头和不定长的报文体(数据局部)组成,报文体是一张图片,每个字节示意图片中一个像素的灰度值。咱们的工作就是读取报文,解析图片内容,保留或显示图片。
报文头的格局如下:
#define FLAG 0x12131415
struct Header
{
quint32 flag; // 报文标识
quint32 length; // 报文长度
quint32 width; // 图片宽度
quint32 height; // 图片高度
}
flag
是报文标识,用来辨认报文的开始;length
是报文的总长度,通过它能够晓得报文何时完结;width
和 height
示意图片的高度和宽度,用来将报文体数据解析为图片。
首先结构一个 QTcpScoket 用于和服务端建设 TCP 连贯,期待接收数据。
QTcpSocket *socket = new QTcpSocket(this);
socket->setReadBufferSize(BUF_SIZE);
connect(socket, SIGNAL(readyRead()), this, SLOT(slotReadData()));
测试发现,如果服务端一次发送的报文长度很长(例如 10086 字节),会被宰割成多个包发送。上面是通过 tcpdump 命令抓包失去的:
14:47:27.438893 IP ...... length 1448
14:47:27.438916 IP ...... length 1448
14:47:27.438920 IP ...... length 1448
14:47:27.438922 IP ...... length 1448
14:47:27.438980 IP ...... length 1448
14:47:27.438983 IP ...... length 1448
14:47:27.438985 IP ...... length 1398
此例中,10068 个字节被宰割成 7 个包。在 QTcpSocket
接管到数据后,每个包会对应地发射一次 readyRead()
信号。也就是说,在槽函数中,只能读取整个报文的一部分。因而须要定义一些成员变量,来保留数据读取过程中的状态。
private:
QTcpSocket *mSocket;
char mBuf[BUF_SIZE]; // 数据读取缓冲区
qint64 mSize = 0; // 已读取数据的长度
Header mHeader; // 报文头
bool mHeadValid = false; // 报文头是否无效
接下来咱们开始读取数据,有两种思路。
形式一
先读取报文头,再读取报文体,读完一个报文,再尝试读下一个。
void Client::slotReadData()
{while (true)
{qint64 readSize = mSocket->read(mBuf + mSize, getMaxDataSize());
if (readSize == 0) break;
mSize += readSize;
// 读取头
if (!mHeadValid && mSize >= sizeof(Header))
{Header *header = (Header*)mBuf;
if (header->flag == FLAG)
{
mHeadValid = true;
mHeader = *header;
}
}
// 解决残缺数据
if (mHeadValid && mSize == mHeader.length)
{dealData();
mHeadValid = false;
mSize = 0;
}
}
}
代码的基本思路是,先尝试读取报文头,依据报文标识定位报文头。读到报文头后,即可失去报文总长度和其余信息,此时将报文头无效标识设为 true
。接下来持续读数据,当已读取的数据长度等于报文头中告知的报文总长度时,实现以后报文的读取,此时须要重置mHeadValid
和mSize
的值,为读取下一个报文做筹备。其中 dealData()
函数用于解决报文数据,例如保留图片。
另外,代码中还有一个 getMaxDataSize()
函数,用来获取以后冀望读取的数据的最大长度,定义如下:
qint64 Client::getMaxDataSize()
{if (mHeadValid) return mHeader.length - mSize;
else return sizeof(Header) - mSize;
}
形式二
先读取报文头,再读取报文体,每次读取尽量多的数据。代码如下:
void Client::slotReadData()
{while (true)
{qint64 readSize = mSocket->read(mBuf + mSize, getMaxDataSize());
if (readSize == 0) break;
mSize += readSize;
// 读取头
if (!mHeadValid && mSize >= sizeof(Header))
{Header *header = (Header*)mBuf;
if (header->flag == FLAG)
{
mHeadValid = true;
mHeader = *header;
}
}
// 解决残缺数据
if (mHeadValid && mSize == mHeader.length)
{dealData();
qint64 left = mSize - mHeader.length;
if (left > 0) memmove(mBuf, mBuf + mHeader.length, left);
mHeadValid = false;
mSize = left;
}
}
}
在这种形式下,getMaxDataSize()
函数的定义如下:
qint64 Client::getMaxDataSize()
{return BUF_SIZE - mSize;}
与第一种形式的区别在于,在读完一个报文时,以后报文后会存在下一个报文的开始局部。因而须要将这部分数据移到缓冲区的开始地位。也就是说,咱们假设呈现了两个报文的内容交叠在一个包中的状况。在两个报文间隔时间较长的状况下,是不应该呈现这种状况的。那么什么状况下会呈现呢?试验发现,在数据发送过快(TODO)或者网络断开一段时间后又连贯导致服务端挤压的大量数据在段时间内发送进来时,会呈现这种状况。实际上,形式一也可能解决这种状况。因而,这两种接管 TCP 报文的形式,都是能够的。
另外,通过测试发现:TCP 服务端收回的报文数据,总是被拆分为大小为 1448 的包。然而——尤其在数据发送数据过快时——readyRead()
信号的发射次数,以及每次发射时能够读取到的数据大小,与此并不统一。大抵状况是,每次读到的数据大小偏向于是 1448 的整数倍,看起来是 Qt 底层把 N 个包合并在了一起。另外,槽函数的执行耗时也影响后续每次读取的数据大小。以后的槽函数耗时越长,下一个槽函数读到的数据越多。总之,你不能预测 readyRead()
什么时候发射,以及每次发射时能读取的数据有多少。
对于为什么时 1448 字节和对于 TCP 分段,能够参考这篇文章:TCP 分段 & IP 分片