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

 

游戏会将一些资源文件压缩后放在服务器,客户端在需要的时候拉取,然后解压使用。用的是 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 file_names;

std::vector file_sizes;

std::vector 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;

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

/ C/C++中文件操作总结

/

使用 C/C++ 来做一些压缩、加密等操作,其中经常要用到一些文件操作的函数

打开文件

fopen C 标准库用来操作文件的函数,拥有良好的移植性,而 open UNIX 的系统调用,移植性有限,我基本都使用 fopen 函数:

FILE *fopen(char *filename, char *mode);

在这里,filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:

模式

描述

r

打开一个已有的文本文件,允许读取文件。

w

打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则会被截断为零长度,重新写入。

a

打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。

r+

打开一个文本文件,允许读写文件。

w+

打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。

a+

打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

返回值FILE *是指向这个文件的指针,后续用它来做文件读写等操作。

读写文件

下面两个函数用于二进制文件输入和输出:

size_t fread(void *ptr, size_t size_of_elements,

             size_t number_of_elements, FILE *a_file);

             

size_t fwrite(const void *ptr, size_t size_of_elements,

             size_t number_of_elements, FILE *a_file);

需要注意的是返回值是成功读取的对象个数,即number_of_elements,而不是读取内容的size。若出现错误或到达文件末尾,则可能小于number_of_elements

关闭文件

文件操作完成后,请使用fclose()函数关闭文件。函数的原型如下:

int fclose(FILE *fp);

如果成功关闭文件,fclose()函数返回零,如果关闭文件时发生错误,函数返回 -1

文件权限

文件权限标志可以使用加权数字表示,这组数字被称为 umask 变量,它的类型是 mode_t,是一个无符号八进制数。umask 变量的定义方法如下表所示。umask 变量由 3 位数字组成,数字的每一位代表一类权限。用户所获得的权限是加权数值的总和。例如 764 表示所有者拥有读、写和执行权限,群组拥有读和写权限,其他用户拥有读权限。

加权数值

1

2

3

4

所有者拥有读权限

群组拥有读权限

其他用户拥有读权限

2

所有者拥有写权限

群组拥有写权限

其他用户拥有写权限

1

所有者拥有执行权限

群组拥有执行权限

其他用户拥有执行权限

文件类型

Linux 中一切皆文件,有以下 7 种文件类型。

普通文件类型Linux 中最多的一种文件类型, 包括 纯文本文件(ASCII)、二进制文件 (binary)、数据格式的文件 (data) 和各种压缩文件。使用ls -l命令查看时第一个属性为 [-]

目录文件:就是目录, 能用 cd 命令进入的。第一个属性为 [d],例如 [drwxrwxrwx]

块设备文件 就是存储数据以供系统存取的接口设备,简单而言就是硬盘。例如一号硬盘的代码是 /dev/hda1 等文件。第一个属性为 [b]

字符设备文件:即串行端口的接口设备,例如键盘、鼠标等等。第一个属性为 [c]

套接字文件:这类文件通常用在网络数据连接。可以启动一个程序来监听客户端的要求,客户端就可以通过套接字来进行数据通信。第一个属性为 [s],最常在 /var/run 目录中看到这种文件类型。

管道文件FIFO也是一种特殊的文件类型,它主要的目的是,解决多个程序同时存取一个文件所造成的错误。FIFO first-in-first-out (先进先出) 的缩写。第一个属性为 [p]

链接文件:类似 Windows 下面的快捷方式。第一个属性为 [l],例如 [lrwxrwxrwx]

文件信息

access()函数用于检查文件是否存在和访问权限,原型如下:

// mode 可选值

// R_OK      测试读许可权

// W_OK      测试写许可权

// X_OK      测试执行许可权

// F_OK      测试文件是否存在

int access(const char * pathname, int mode)

成功执行时,返回0,失败返回-1

stat函数用于获取文件的详细信息,保存在标准库的struct stat,原型如下:

int stat(const char * pathname, stat *buf);

struct stat 定义如下:

struct stat {

    dev_t         st_dev;       //文件的设备编号

    ino_t         st_ino;       //节点

    mode_t        st_mode;      //文件的类型和存取的权限

    nlink_t       st_nlink;     //连到该文件的硬连接数目,刚建立的文件值为1

    uid_t         st_uid;       //用户ID

    gid_t         st_gid;       //ID

    dev_t         st_rdev;      //(设备类型)若此文件为设备文件,则为其设备编号

    off_t         st_size;      //文件字节数(文件大小)

    unsigned long st_blksize;   //块大小(文件系统的I/O 缓冲区大小)

    unsigned long st_blocks;    //块数

    time_t        st_atime;     //最后一次访问时间

    time_t        st_mtime;     //最后一次修改时间

    time_t        st_ctime;     //最后一次改变时间(指属性)

};

便利函数

获取文件内容和大小

static FILE *OpenFileWithPath(const char *path)

{

    const char *fileMode = "rb";

    return fopen (path, fileMode);

}

static char *ReadStringFromFile(const char *pathName, int *size)

{

       FILE *file = OpenFileWithPath (pathName);

       if (file == NULL)

       {

              return 0;

       }

       fseek (file, 0, SEEK_END);

       int length = ftell(file);

       fseek (file, 0, SEEK_SET);

       if (length < 0)

       {

              fclose (file);

              return 0;

       }

       *size = length;

       char *outData = malloc (length);

       int readLength = fread (outData, 1, length, file); fclose(file);

       if (readLength != length)

       {

        free (outData);

              return 0;

       }

       return outData;

}

从绝对路径中提取文件名

char* FileName(char *absolutePath)

{

    if (absolutePath[0] != '/') {

        return 0;

    }

   

    char *s = strrchr(absolutePath, '/');

    size_t size = strlen(s);

    char *des = malloc(size);

    memcpy(des, s + 1, size);

    return des;

}

获取绝对路径的文件所在的目录

char* DirName(char *absolutePath)

{

    if (absolutePath[0] != '/') {

        return 0;

    }

   

    const size_t len = strlen(absolutePath);

    char *path = malloc(len + 1);

    strncpy(path, absolutePath, len);

   

    if (path[len - 1] ==  '/') {

        path[len - 1] = 0;

    }

   

    for(char * p = path + strlen(path) - 1; *p; p--){

        if (*p == '/'){

            *p = '\0';

            break;

        }

    }

    return path;

}

递归创建一个目录

 

#define DEFAULT_DIR_MODE S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH // 0755

int RecursiveMkdir(const char * dir, const unsigned int mode){

    const size_t len = strlen(dir);

 

    if (!len){

        return 0;

    }

 

    char * path = calloc(len + 1, sizeof(char));

    strncpy(path, dir, len);

 

    if (path[len - 1] ==  '/'){

       path[len - 1] = 0;

    }

 

    for(char * p = path + 1; *p; p++){

        if (*p == '/'){

            *p = '\0';

           

            if (access(path, 0) < 0) {

                mkdir(path, mode?mode:DEFAULT_DIR_MODE);

            }

           

            *p = '/';

        }

    }

   

    free(path);

    return 0;

}

/

 

你可能感兴趣的:(C++和C#)