在linux系统,我们经常会见到.tar.gz格式的压缩文件,这是一种经过tar协议归档,再进行gz压缩得到的文件。
比如我们有一个文件夹dir,里面包含两个文件a.txt、b.txt,两个文件夹dir1、dir2,dir1文件夹内包含c.txt,dir2内包含d.txt和dir3,dir3内包含e.txt。
- tar打包
在内存中,文件的位置其实是散乱的排列。
通过tar协议,我们可以将数据在内存中进行连续排列。
排列出来大概就是这样的。
不论是文件还是文件夹,我们都需要用一个结构体充当数据头,对数据进行描述,在结构体后放入文件的具体内容(文件夹不需要放具体内容),这样就把所有的文件和文件夹全部按照规律顺序放在一块连续的内存了。
让我们来分析下tar的协议。
数据头是一个占用512字节内存的结构体,我提取了下最重要的6个部分,用红色框标记出来。
1)name:用来标记数据的相对路径,这个路径是整个数据包内的相对路径,比如我们打包的这个dir文件夹,最外层这个dir描述就是“dir/”,而内部的a.txt就是“dir/a.txt”
2)mode:文件读写执行权限,linux有用,windows下没什么卵用,这玩意吧我们写个固定值就行。
3)size:这个当然就特别重要了,需要填入实际数据的大小(8进制),文件夹就是0。
4)chksum:这玩意是个校验位,挺烦人的,就是除去checksum字段(8字节)其他所有的 504个字节的ascii码相加的值再加上256的数值(8进制)
5)typeflag:这个是用来标记数据类型的,多的我就不扯了,'0'表示文件,'5'文件夹就够了。
6)magic:这个纯粹只是标记了。固定ustar就行。
数据头后面紧跟着的就是数据实际内容了,而数据内容也必须是512字节分块,最后不足512的也得填内存凑够512字节。
我们用c#代码简单来制造下tar文件:
(我这里造数据头就直接搞512内存然后指定位置简单填充下)
class tarfileinfo
{
public string fullpathname;
public string relationpathname;
}
class taropt
{
byte[] gentarhead(string name, int size)
{
name = name.replace("\\", "/");
if (0 == size) if (name[name.length - 1] != '/') name = "/";
byte[] buf = new byte[512];
set(buf, name, 0, 100);
set(buf, "0000777", 100, 8);
string si = convert.tostring(size, 8);
set(buf, si, 124, 12);
set(buf, "ustar ", 257, 100);
if (size == 0) buf[156] = 53;// '5';
int chksum = 256;
for (int i = 0; i < 512; i)
{
chksum = buf[i];
}
string chk = convert.tostring(chksum, 8);
set(buf, chk, 148, 8);
return buf;
}
void set(byte[] buf, string str, int pos, int maxlen)
{
byte[] by = encoding.utf8.getbytes(str);
int len = by.length > maxlen ? maxlen : by.length;
array.copy(by, 0, buf, pos, len);
}
void adddir(filestream fw, string dirname)
{
byte[] buf = gentarhead(dirname, 0);
fw.write(buf,0,buf.length);
}
void addfile(filestream fw, tarfileinfo info)
{
filestream fs = new filestream(info.fullpathname, filemode.open);
int len = (int)fs.length;
byte[] buf = gentarhead(info.relationpathname, len);
fw.write(buf, 0, buf.length);
for (int i = 0; i < len; i = 512)
{
byte[] by = new byte[512];
fs.read(by, 0, 512);
fw.write(by, 0, by.length);
}
}
public void gentar(string dir,string dstpathname)
{
list fi = new list();
int pos = dir.lastindexof("\\");
if (pos == dir.length - 1) dir = dir.substring(0, pos);
string basedir = dir;
pos = basedir.lastindexof("\\");
basedir = basedir.substring(0, pos 1);
addpath(basedir, dir, fi);
filestream filestream = new filestream(dstpathname, filemode.create, fileaccess.write);
foreach (var item in fi)
{
if (item.fullpathname == "") adddir(filestream, item.relationpathname);
else addfile(filestream, item);
}
filestream.close();
}
void addpath(string basedir, string path, list pathnamelist)
{
tarfileinfo tfi = new tarfileinfo();
tfi.fullpathname = "";
tfi.relationpathname = path.substring(basedir.length);
pathnamelist.add(tfi);
directoryinfo thefolder = new directoryinfo(path);
foreach (fileinfo nextfile in thefolder.getfiles("*.*"))
{
tfi = new tarfileinfo();
tfi.fullpathname = nextfile.fullname;
tfi.relationpathname = tfi.fullpathname.substring(basedir.length);
pathnamelist.add(tfi);
}
foreach (directoryinfo nextfolder in thefolder.getdirectories())
{
addpath(basedir, nextfolder.fullname, pathnamelist);
}
}
}
taropt opt = new taropt();
opt.gentar("d:\\dir","d:\\dir.tar");
调用后,生成了dir.tar文件,我们可以随便用压缩工具打开查看文件内容。
也可以用16进制编辑器看看内存情况,和tar协议对比下:
- gz压缩
tar就只是单纯地数据排列打包,而且因为有数据头,还有512一个数据包的规则,所以出来的文件比原来占用的内存还要大。
通过gzip压缩协议可以对文件进行压缩,这个是web上一个主流的协议,具体算法我没那个能力和时间捣鼓。
我们用c#的gzipstream压缩下:
fileinfo fi = new fileinfo("d:\\dir.tar");
filestream fs = fi.openread();
byte[] buf = new byte[fi.length];
fs.read(buf, 0, buf.length);
filestream dst = new filestream("d:\\dir.tar.gz", filemode.create);
gzipstream compresssionstream = new gzipstream(dst, compressionmode.compress);
binarywriter bw = new binarywriter(compresssionstream);
bw.write(buf, 0, buf.length);
bw.close();
compresssionstream.close();
dst.close();
这样就有了dir.tar.gz文件。
比较下压缩前后大小: