C/C++ 实现 lz4 格式的压缩和解压过程浅析

2020/06/12 C++ 共 3330 字,约 10 分钟

公司的游戏会将一些资源文件压缩后放在服务器,客户端在需要的时候拉取,然后解压使用。用的是 C# 的 lz4 进行的压缩和解压缩,导致申请的内存没办法及时释放(mono 虚拟机申请的内存是不会归还给系统的),所以如果手机上如果经历了边玩边下载的话,峰值内存会有大概100M+的上浮,这对于一些内存比较受限的机器来说是不友好的。

问题说明

  1. C# mono 虚拟机申请的内存是不会归还给系统的。
  2. 原生语言(C/C++)可以自由控制内存的释放。
  3. 如果想内存峰值降低,则需要将原来由 C# 开发的 lz4 模块,改用 C/C++ 实现,然后由 C# 调用,从而达到降低内存峰值的目的。

实现方案

Github 上有 C 语言实现的 lz4 可以直接用:https://github.com/lz4/lz4。参考根目录下 examples 里的例子即可实现对一个文件进行 lz4 压缩和解压缩,里面的文档和例子都很清楚,这里就不做赘述了。

Github 上这个 lz4 的包只能针对单个二进制文件进行压缩,无法处理文件夹。如果需要对文件夹进行压缩时,就需要先将文件夹合并成一个二进制文件,解压后再分割还原成文件夹。在 Linux 中将文件夹合并成单个文件通常使用 tar 命令,在 iOS 平台尝试了下,是无法调用 Shell 命令的,所以需要用代码实现一下 tar 文件解析。

整个过程简述如下:

  1. 在 Mac/Linux 上用 tar 命令打包一个文件夹为一个文件。
  2. 用 lz4 库对第一步打包后的文件压缩,然后放到服务器。
  3. 客户端拉取到压缩的文件后,调用 lz4 库的 Api 解压缩。
  4. 调用自己实现的 tar 解析包,将解压缩后的单个文件分割还原成文件夹。

第四步中需要自己实现一个 tar 的解析包,GNU Linux tar 包的源码有近 30w 行的代码,我们的业务场景并不需要如此完善的实现,只要能正常分割还原文件就可以了,下面看下如何实现一个具备基础功能的 tar 解析包。

Tar 格式

tar 是 Unix 和类 Unix 系统上的归档打包工具,可以将多个文件合并为一个文件,打包后的文件名亦为 “tar”。目前,tar 文件格式已经成为 POSIX 标准,最初是 POSIX.1-1988,目前是 POSIX.1-2001。本程序最初的设计目的是将文件备份到磁带上(tape archive),因而得名 tar。

tar 会为每个文件生成一个 512 字节的 header,记录它的名称、权限、大小等信息,然后将文件内容按 512 个字节分割成多个块,按顺序放在 header 后面。最后将所有的这些块写进一个二进制文件中。

C++ 实现解析

根据 tar 标准 定义这个 tar header 的结构体:

typedef struct tar_header
{                                     /* byte offset */
	char name[100];               /*   0 */
	char mode[8];                 /* 100 */
	char uid[8];                  /* 108 */
	char gid[8];                  /* 116 */
	char size[12];                /* 124 */
	char mtime[12];               /* 136 */
	char chksum[8];               /* 148 */
	char typeflag;                /* 156 */
	char linkname[100];           /* 157 */
	char magic[6];                /* 257 */
	char version[2];              /* 263 */
	char uname[32];               /* 265 */
	char gname[32];               /* 297 */
	char devmajor[8];             /* 329 */
	char devminor[8];             /* 337 */
	char prefix[155];             /* 345 */
                                      /* 500 */
} tar_header;

再定义下文件的类型:

/* Values used in typeflag field.  */
#define REGTYPE  '0'            /* regular file */
#define AREGTYPE '\0'           /* regular file */
#define LNKTYPE  '1'            /* link */
#define SYMTYPE  '2'            /* reserved */
#define CHRTYPE  '3'            /* character special */
#define BLKTYPE  '4'            /* block special */
#define DIRTYPE  '5'            /* directory */
#define FIFOTYPE '6'            /* FIFO special */
#define CONTTYPE '7'            /* reserved */

获取各个文件的位置、名称和大小:

std::vector<std::string> file_names;
std::vector<size_t> file_sizes;
std::vector<size_t> file_data_start_addrs;
const int block_size{ 512 };

file = fopen("tar_name", "rb");
if (!file) return false;

unsigned char buf[block_size];
tar_header* header = (tar_header*)buf;
memset(buf, 0, block_size);

size_t pos{ 0 };

while (1) {
	size_t read_size = fread(buf, block_size, 1, file);
	if (read_size != 1) break;

	pos += block_size;
	size_t file_size{0};
	sscanf(header->size, "%lo", &file_size);
	size_t file_block_count = (file_size + block_size - 1) / block_size;

	switch (header->typeflag) {
		case '0': 
		case '\0':
			// normal file
			file_sizes.push_back(file_size);
			file_names.push_back(std::string(header->name));
			file_data_start_addrs.push_back(pos);
			break;
		case '1':
			// hard link
			break;
		case '2':
			// symbolic link
			break;
		case '3':
			// device file/special file
			break;
		case '4':
			// block device
			break;
		case '5':
			// directory
			break;
		case '6':
			// named pipe
			break;
		default:
			break;
	}

	pos += file_block_count * block_size;
	fseek(file, pos, SEEK_SET);
}

fseek(file, 0, SEEK_SET);

return true;

通过file_namesfile_sizesfile_data_start_addrs 就可以去取各个文件的内容了:

bool GetFileContents(const char* file_name, char* contents)
{
	bool flag = false;
	for (int i = 0; i < file_names.size(); i++) {
		std::string name_(file_name);

		if (file_names[i].compare(name_) == 0) {
			int file_size = file_sizes[i];
			flag = true;
			fseek(file, file_data_start_addrs[i], SEEK_SET);
			fread(contents, file_size, 1, file);
			fseek(file, 0, SEEK_SET);

			break;
		}
	}

	return flag;
}

更完善的实现可以参考 GNU Linux Tar 包的源码:https://www.gnu.org/software/tar/

文档信息

Search

    Table of Contents