在现代软件开发中,数据压缩是一个常见的需求。无论是为了节省存储空间还是为了加快网络传输速度,压缩算法都扮演着重要的角色。在.NET环境下,虽然提供了如GZipStream和DeflateStream等压缩工具,但它们主要用于单个文件的压缩和解压缩。如果要将多个文件压缩成一个归档文件,就需要更多的工作,或者利用第三方库。本文将介绍如何在没有第三方库的情况下,完全在.NET中实现一个自己的归档文件系统。
最初的想法是创建一个包含ArchiveFile对象列表的类。每个ArchiveFile对象包含文件的详细信息和一个填充了文件内容的字节数组。通过序列化这个类并通过压缩流来存储和压缩数据。尽管实现了这个解决方案(在库中作为TinyArchive类保留),但后来发现它有一个致命的缺陷:整个对象和所有未压缩的数据都需要加载到内存中。这限制了归档文件的最大大小。
回到设计板上,意识到不能使用之前编写的压缩二进制序列化代码来处理文件结构,因为它不能选择性地加载归档文件的部分。需要创建自己的。想要能够只读取归档中文件的详细信息,然后使用这些信息来选择性地读取特定部分的文件。也意识到,不能把所有的索引都放在文件的前面,因为这将使得向归档中添加更多文件变得非常困难。
最终,决定从包含下一部分归档长度的两个字节开始。下一部分将是压缩文件的详细信息:它的名称和在归档中的长度,紧接着是文件的压缩内容。下一个文件将以相同的方式添加。代码将读取前两个字节,将其转换为ushort,并使用该值指定从归档中读取的下一个字节块的长度,这些字节块是索引详细信息。从索引中,它得到压缩数据块的长度,它使用这个长度跳到下一个索引。
通过这种方式,读者可以非常快速地对整个归档进行目录化。它构建了一个可搜索的索引,指定了归档中每个压缩文件的起始字节索引号和长度。提取压缩文件只是打开归档文件的文件流,定位到要提取的文件的索引位置,然后读取正确数量的字节。然后通过DeflateStream类将压缩字节扩展到原始大小。
删除归档中的文件并不是最初应用程序的要求,但觉得这是使库完整的必要条件。在这方面遇到了很多麻烦,而且对解决方案并不满意。删除方法基本上会在临时文件位置创建当前归档的副本,跳过它被告知要删除的文件。然后删除原始文件并将副本移动到其位置。
在附带的Visual Studio项目中提供了一个演示归档表单应用程序。它是一个快速、基本的多文件归档实现。可以创建归档,向其中添加文件,提取它们,删除它们。可以使用树形视图控件浏览归档中的文件。
处理归档的主要类是StreamingArchiveFile。下面的代码展示了如何创建一个新的归档,并将一个文件夹中的文件添加到其中:
C#
//
创建一个新的或访问一个现有的流归档文件:
StreamingArchiveFile arc =
new
StreamingArchiveFile(
@"
C:\Temp\streamingArchive.arc"
);
//
现在遍历特定目录中的文件,并将它们添加到归档中:
foreach
(
string
fileName
in
Directory.GetFiles(
@"
C:\Temp\Test\"
,
"
*.*"
))
{
//
添加文件
arc.AddFile(
new
ArchiveFile(fileName));
}
提取文件:这段代码展示了如何枚举归档中的文件,将它们提取到一个临时文件夹,并打开它们。
C#
//
打开现有的归档文件:
StreamingArchiveFile arc =
new
StreamingArchiveFile(
@"
C:\Temp\streamingArchive.arc"
);
//
遍历归档中的文件:
foreach
(
string
fileName
in
arc.FileIndex.IndexedFileNames)
{
//
写入文件的名称
Debug.Print(
"
文件: "
+ fileName);
//
提取文件:
ArchiveFile file = arc.GetFile(fileName);
//
将其保存到磁盘:
String
tempFileName = Path.GetTempPath() +
"
\\"
+ file.Name;
file.SaveAs(tempFileName);
//
打开文件:
Process.Start(tempFileName);
}
还有一个使用正则表达式和LINQ在归档中查找文件的搜索方法:
C#
///
<summary>
///
</summary>
///
<param
name="fileNameExpression"
>
</param
>
///
<returns
>
</returns
>
public
IEnumerable<ArchiveFileIndex> Search(
String
fileNameExpression)
{
return
(
from
file
in
_fileIndex
where
Regex.IsMatch(file.FileName, fileNameExpression)
select
file);
}
要真正了解归档的使用方式,请查看FrmStreamingArchiveUI表单中的代码。
.NET提供了两个压缩类:GZipStream和DeflateStream。它们都使用deflate算法,GZipStream类实际上是在DeflateStream之上构建的,并添加了一些额外的头部信息和CRC检查。使用了DeflateStream类,因为它更快,占用的空间更小。
C#
///
<summary>
///
</summary>
///
<typeparam
name="T"
>
要反序列化的对象类型
</typeparam
>
///
<param
name="compressedInputStream"
>
包含要反序列化的对象的压缩数据的流
</param
>
///
<returns
>
</returns
>
public
static
T DeSerializeCompressed<T>(Stream compressedInputStream,
bool
useCustomBinder =
false
)
{
//
构建二进制格式化程序并分配自定义绑定器:
BinaryFormatter formatter =
new
BinaryFormatter();
if
(useCustomBinder)
formatter.Binder =
new
TinySerializer(
typeof
(T));
//
通过GZip解压缩流读取流。
using
(DeflateStream decompressionStream =
new
DeflateStream(compressedInputStream, CompressionMode.Decompress,
true
))
{
//
反序列化为对象:
object
graph = formatter.Deserialize(decompressionStream);
//
检查类型是否正确并返回。
if
(graph
is
T)
return
(T)graph;
else
throw
new
ArgumentException(
"
无效类型!
");
}
}