本文已参与「新人创作礼」活动,一起开启掘金创作之路。
这篇文章主要的目的是讲UDP传输的字节限制以及用分包和组包去解决这个限制,本文直接将桌面截屏后传输是低效的做法,使用H.264或H.265对图像进行处理能减少传输的数据量。
本文不涉及重传机制,当数据块丢失时,组包无法完成,所以不推荐用在完整性要求高的场景。如果希望数据能够安全完整的传输,推荐使用TCP协议。
分包与组包
数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的,所以该域主要被用来计算可变长度的数据部分(又称为数据负载)。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为65535字节。不过,一些实际应用往往会限制数据报的大小,有时会降低到8192字节。
为了避免发送文件时udp报文超出最大长度限制,我们会把文件切割成更小的块,每个块都会用对应的序号和偏移地址(在组包时防止顺序混乱),并且使用批号作为键,将同一批号的块根据序号进行组装,接收时判断序号与总块数判断当前文件是否接收完成。
包头定义
#ifndef PACKAGE_H
#define PACKAGE_H
#define MAX_PACK_SIZE 1024
#pragma pack(1)
typedef struct _PACKAGE_HEAD {
char magicNumber[2]; // 魔术字
unsigned int BN; // 批号
unsigned int packageHeadSize; // 包头大小
unsigned int packageSize; // 总大小
unsigned int packageTotal; // 分包总数量
unsigned int packageCurIdx; // 当前分包索引
unsigned int fileSize; // 文件总大小
} PACKAGE_HEAD;
#pragma pack()
#endif // PACKAGE_H
GZIP压缩
HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术。大流量的WEB站点常常使用GZIP压缩技术来让用户感受更快的速度。这一般是指WWW服务器中安装的一个功能,当有人来访问这个服务器中的网站时,服务器中的这个功能就将网页内容压缩后传输到来访的电脑浏览器中显示出来.一般对纯文本内容可压缩到原大小的40%.这样传输就快了,效果就是你点击网址后会很快的显示出来.当然这也会增加服务器的负载. 一般服务器中都安装有这个功能模块的。
为了提高传输速度,减少带宽,使用GZIP压缩后进行传输是个不错的选择。
安装zlib
-
去官网下载最新版的zlib: www.zlib.net/
-
解压并用命令行工具进入该目录
-
执行脚本配置工具:
./configure
-
编译并安装:
make && make install
Qt引入zlib
在.pro文件中新增:LIBS += -L/usr/local/lib -lz
,路径请使用实际安装路径。
压缩与解压类
#ifndef ZIP_H
#define ZIP_H
#include <zlib.h>
#include <zconf.h>
#include <QObject>
class Zip : public QObject
{
Q_OBJECT
public:
// 压缩
static QByteArray GzipCompress(const QByteArray& postBody) {
QByteArray outBuf;
z_stream c_stream;
int err = 0;
int windowBits = 15;
int GZIP_ENCODING = 16;
if (!postBody.isEmpty()) {
c_stream.zalloc = (alloc_func)0;
c_stream.zfree = (free_func)0;
c_stream.opaque = (voidpf)0;
c_stream.next_in = (Bytef *)postBody.data();
c_stream.avail_in = postBody.size();
if (deflateInit2(&c_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
MAX_WBITS + GZIP_ENCODING, 8, Z_DEFAULT_STRATEGY) != Z_OK) return QByteArray();
for (;;) {
char destBuf[4096] = { 0 };
c_stream.next_out = (Bytef *)destBuf;
c_stream.avail_out = 4096;
int err = deflate(&c_stream, Z_FINISH);
outBuf.append(destBuf, 4096 - c_stream.avail_out);
if (err == Z_STREAM_END || err != Z_OK) {
break;
}
}
auto total = c_stream.total_out;
deflateEnd(&c_stream);
total = c_stream.total_out;
}
return outBuf;
}
// 解压
static QByteArray GZipUnCompress(const QByteArray& src) {
QByteArray outBuffer;
z_stream strm;
strm.zalloc = NULL;
strm.zfree = NULL;
strm.opaque = NULL;
strm.avail_in = src.size();
strm.next_in = (Bytef *)src.data();
int err = -1, ret = -1;
err = inflateInit2(&strm, MAX_WBITS + 16);
if (err == Z_OK) {
while (true) {
char buffer[4096] = { 0 };
strm.avail_out = 4096;
strm.next_out = (Bytef *)buffer;
int code = inflate(&strm, Z_FINISH);
outBuffer.append(buffer, 4096 - strm.avail_out);
if (Z_STREAM_END == code ) {
break;
}
}
}
inflateEnd(&strm);
return outBuffer;
}
private:
explicit Zip(QObject *parent = nullptr);
signals:
};
#endif // ZIP_H
分包
unsigned int BN = 0; // 初始化批号
QScreen *screen = QGuiApplication::primaryScreen();
QRect mm = screen->availableGeometry() ;
while (true) {
QByteArray ba;
QBuffer bf(&ba);
screen->grabWindow(0, 0, 0, mm.width(), mm.height()).save(&bf, "jpg", 50);
// 压缩数据
ba = Zip::GzipCompress(ba);
PACKAGE_HEAD head;
// 定义魔术字
head.magicNumber[0] = 'G';
head.magicNumber[1] = 'K';
// 获取并自增批号
head.BN = BN++;
head.packageHeadSize = sizeof(PACKAGE_HEAD);
head.fileSize = ba.size();
// 计算总分包数量
head.packageTotal = head.fileSize / MAX_PACK_SIZE;
head.packageTotal += head.fileSize % MAX_PACK_SIZE > 0 ? 1 : 0;
for (int i=0; i<head.packageTotal; i++) {
QByteArray b;
head.packageCurIdx = i;
if (head.fileSize - i * MAX_PACK_SIZE >= MAX_PACK_SIZE) {
head.packageSize = MAX_PACK_SIZE + sizeof(PACKAGE_HEAD);
b.append((char*)&head, sizeof(head));
b.append(ba.mid(i * MAX_PACK_SIZE, MAX_PACK_SIZE));
} else {
head.packageSize = head.fileSize - i * MAX_PACK_SIZE + sizeof(PACKAGE_HEAD);
b.append((char*)&head, sizeof(head));
b.append(ba.mid(i * MAX_PACK_SIZE, head.fileSize - i * MAX_PACK_SIZE));
}
QtConcurrent::run([=](){
// 使用udp发送分包数据
QUdpSocket cli;
cli.writeDatagram(b, QHostAddress("127.0.0.1"), 8080);
});
}
}
组包
// 定义缓冲区结构体用于管理封包
typedef struct _PACKAGE {
unsigned int idxCount;
QByteArray buf;
} PACKAGE;
QUdpSocket* m_socket; // UDP套接字
QMutex m_mutex; // 锁
QMap<unsigned int, PACKAGE> m_buf; // 接收缓冲区
// 收包函数
receive() {
QMutexLocker _(&m_mutex);
QByteArray ba;
while(m_socket->hasPendingDatagrams()) {
ba.resize(m_socket->pendingDatagramSize());
m_socket->readDatagram(ba.data(), ba.size());
if (ba.size() <= sizeof(PACKAGE_HEAD)) {
qDebug() << "ba.size() <= sizeof(PACKAGE_HEAD)";
m_buf.clear();
return;
}
PACKAGE_HEAD head;
memcpy((char*)&head, ba.constData(), sizeof(PACKAGE_HEAD));
if (head.magicNumber[0] != 'G' || head.magicNumber[1] != 'K') {
qDebug() << "magicNumber error";
return;
}
if (!m_buf.contains(head.BN)) {
m_buf[head.BN] = PACKAGE{0, QByteArray()};
m_buf[head.BN].buf.fill(0, head.fileSize);
}
m_buf[head.BN].idxCount++;
// 通过当前索引 * 分包大小计算出当前块所在文件的偏移值,并将数据填充进去
memcpy(m_buf[head.BN].buf.data() + head.packageCurIdx * MAX_PACK_SIZE, ba.constData() + sizeof(PACKAGE_HEAD), head.packageSize - head.packageHeadSize);
// 如果当前批号的索引计数值等于总包数,证明该批号的数据传输并组装完成
if (m_buf[head.BN].idxCount == head.packageTotal) {
// 解压数据
ba = Zip::GZipUnCompress(m_buf[head.BN].buf);
QPixmap pixmap;
pixmap.loadFromData(ba);
QPalette palette;
palette.setBrush(backgroundRole(), QBrush(pixmap.scaled(this->size(), Qt::KeepAspectRatioByExpanding)));
setPalette(palette);
m_buf.remove(head.BN);
// 检查缓冲中还有多少批号未完成
qDebug() << m_buf.size();
}
}
}