本文假设您熟悉 C# 和 Windows 窗体
下载本文的代码: ZipCompression.exe (150KB)
摘要
在存储文件或者通过网络发送文件时,使用 Zip 压缩可以节省空间和网络带宽。此外,还不会丢失经过 Zip 的文件夹的目录结构,这使其成为非常有用的压缩方案。C# 语言不具有任何使您可以操纵 Zip 文件的类,但是由于面向 .NET 的语言可以共享类实现,并且 J# 在 java.util.zip 命名空间中公开了类,因此您可以在 C# 代码中使用这些类。本文将解释如何使用 Microsoft J# 类库创建能够压缩和解压缩 Zip 文件的 C# 应用程序。它还将介绍 J# 运行库的其他一些可以从任何 .NET 兼容语言中使用以节省某些编码工作的独特部分。
本页内容
Zip 是一种受人欢迎的数据传输和存储标准,因为它可以节省磁盘空间和网络带宽。典型的文本和数据库文件可以被压缩至它们原始大小的 10%。即使二进制文件不能进行同样的压缩,通常也可以获得 50% 的压缩比。
Zip 文件的一个附加优点是单个文件可以包含多个文件,同时可以保留目录结构。这使您可以发送附加到电子邮件消息中的完整目录树,并且让收件人恢复原始文件结构。
Zip 数据格式是开放的,并且不会涉及专利权或其他法律问题。开发人员可以自由地创建操纵 Zip 文件的应用程序,以及使用低级别 Zip 压缩算法来暂时减小他们自己的自定义数据的大小。Zip 数据规范的作者在名为 zlib 的库 (http://www.gzip.org/zlib) 中向开发人员提供压缩和解压缩算法。Java 平台在 Java 开发工具包 (JDK) 的版本 1.1 中采用了该库,以构成 Java 存档 (JAR) 文件格式的基础,因此从 JDK 版本 1.1 开始,标准 Java 语言 API 就包含了操纵 Zip 文件所需的类。可以在 java.util.zip 命名空间下找到这些类。
Zip 文件和 C#
我希望在用 C# 编写的应用程序中使用 Zip 压缩。遗憾的是,Microsoft.NET Framework 当前不包含任何用于操纵 Zip 文件的类。但是,我的确找到了几个与 Zip 压缩有关的产品。例如,#ziplib(以前称为 NZipLib,http://www.icsharpcode.net/OpenSource/SharpZipLib/default.asp)是 zlib 库到 C# 的移植产品。它的许可证允许开发人员在封闭源代码的商业应用程序中包含该库。但是,在 MSDN Magazine 付印之时,#ziplib 尚处于预发布状态(版本 0.31)。
另外一个解决方案是使用非托管 zlib 作为 Windows DLL 并且为其编写必要的 Interop 包装,但是由于压缩涉及到在每个函数调用期间到处传递大量数据,因此编写 Interop 包装以获得最佳性能将是一个困难的过程。尽管可以使用其他库,但它们不是免费的。
解决方案
.NET Framework 的设计考虑了语言互操作性。可以从任何实现了必要功能的 .NET 兼容编程语言中正确地使用所有遵循某些特定规则的托管组件。互操作性所需的规则和语言功能集称为公共语言规范 (CLS)。
Microsoft 实现的所有 .NET 语言编译器都是符合 CLS 的,其中包括 Microsoft Visual J# .NET — 一种供希望在 Microsoft .NET Framework 上生成应用程序和服务的 Java 语言开发人员使用的开发工具。(Visual J# .NET 是由 Microsoft 独立开发的。它没有经过 Sun Microsystems, Inc. 的认可和批准)这就是为什么可以在用 J# 编写的 Windows 窗体和 ASP.NET 应用程序中使用 .NET Framework 类的原因。
正像您将在本文稍后看到的那样,J# 运行库公开的某些类实际上并不符合 CLS,但是您仍然可以从其他语言中访问大多数 J# 类,以便使用 .NET Framework 未实现的特定功能。由于 J# 实现了 JDK 版本 1.1.4,因此丝毫不会令人感到意外的是,开发人员可以通过 J# 运行库访问 java.util.zip 命名空间。在本文的下一部分中,我将介绍一个用 C# 编写的应用程序,它使用 java.util.zip 类压缩和解压缩 Zip 文件,以便在本地节省空间以及在网络中节省带宽。
本文中的所有示例代码都是用 Microsoft Visual Studio 2002 和 J# 运行库版本 1.0(参见位于本文顶部的链接)开发的。
SharpZip
我用 C# 编写了本文随附的示例应用程序之一 SharpZip。它是一个用于处理 Zip 文件的简化实用工具,通过它可以创建 Zip 文件,或者打开现有的 Zip 文件以解压缩、附加和删除文件(参见图 1)。
图 1 SharpZip 应用程序
在查看代码之前,您需要确保在系统中正确安装了 J# 运行库。无需安装完整的 Visual J# .NET 产品。您可以只下载并安装 J# 1.0 Redistributable Package,它可以从 http://msdn.microsoft.com/vjsharp/downloads/howtoget.asp 获得。
Java.util.zip 命名空间在 vjslib.dll 程序集中实现。该程序集位于 C:\WINNT\Microsoft Visual JSharp .NET\Framework\v1.0.4205\ 目录中(您需要将 WINNT 替换为实际的 Windows 目录)。
在项目中包含对 vjslib.dll 的引用时,可以开始从代码中使用 J# 命名空间并且用对象浏览器浏览 JDK 命名空间(参见图 2)。重要的类包括 java.util.zip.ZipFile、java.util.zip.ZipEntry 和 java.util.zip.ZipOutputStream。这些类显示在图 3 中,通过它们可以在文件级别操纵 Zip 文件。
图 2 对象浏览器中的命名空间
在使用本文中概述的方法时,方法名称在您看来可能是陌生的,这是因为 Java 用于标识符(除类和接口外)的命名约定与在 C# 中使用的命名约定有所不同。在 Java 中,命名空间和方法名称是使用低级大小写混合编写的,其中第一个字母小写,其余单词为首字母大写,如“nextElement”所示。但是,我肯定您会掌握这种方法的。
枚举 Zip 条目
Java.util.zip.ZipFile 类的 entries 方法返回一个实现 java.util.Enumeration 接口的对象。然后,应用程序遍历枚举,以检索表示 Zip 文件中的各个条目的 ZipEntry 实例。ZipEntry 类将公开所有需要的信息,例如,文件名、压缩方法、时间戳、原始大小和压缩大小等等(参见图 4)。
请注意,尽管 java.util.Enumeration 接口类似于 System.Collections.IEnumerator 接口,但 Java 枚举器在您通过调用 nextElement 检索当前对象时前进至下一个元素,而 .NET 枚举器当您在 MoveNext 调用中检查更多元素的可用性时前进。另一个重要差异是 Enumeration 接口不提供用于重新启动遍历的方法。
.NET 枚举器的一个优点是您可以多次访问当前元素。另一方面,Java 枚举器使您可以多次检查完成情况,但是这在大多数情况下不是非常有用。Java 和 .NET 枚举器都经过了良好的设计,能够防止您在枚举循环内部忘记前进至下一个元素。
我决定编写一个用于包装 Java 枚举器的类,以便我可以将 C# foreach 语句与它们一起使用。我将该类命名为 EnumerationAdapter。我通过再次调用能够返回 Java 枚举器的方法来模拟 Reset 方法。为此,包装类构造函数采用 java.util.Enumeration 接口的委托作为参数,而不是 java.util.Enumeration 接口本身作为参数。
解压缩 Zip 文件
SharpZip 应用程序在解压缩文件时所做的第一件事情,是提示用户指定应当在其中创建文件的目录。您可能已经注意到,应用程序显示了“Browse for Folder”对话框。我倾向于使用 System.Windows.Forms.Design.FolderNameEditor.FolderBrowser 类,但是文档声称该类型支持 .NET Framework 基础结构,并且不适合直接使用,因此我通过导入 Microsoft Shell Controls and Automation 类型库,借助于 COM Interop 来使用 Shell32 对象。
从 Zip 文件中提取原始文件(解压缩)的操作非常简单:只需调用 ZipFile 对象上的 getInputStream,并传递您要为其获得压缩文件的条目即可。GetInputStream 方法将产生一个 InputStream,以便您从中读取存档条目的内容。
ExtractZipFile Helper 函数为您完成该工作。通过使用单独的条目将目录存储在 Zip 文件中,但每个条目中的文件名也包含目录信息,因此 ExtractZipFile 忽略了目录条目,并且从文件名中提取必要的路径信息。
要将单个文件保存到磁盘,只需将与感兴趣的条目相对应的 InputStream 的内容写入文件。这一次我决定不将自定义 System.IO.Stream 类包装为 Java 流,因为 java.io 命名空间对于流具有相当好的支持。特别地,java.io.FileOutputStream 使您可以创建文件以便向其复制所需的条目。
图 5 中的 CopyStream Helper 函数将 java.io.InputStream 对象的内容复制到 java.io.OutputStream 对象。该 Helper 函数还被 SharpZip 应用程序的其他部分使用。可是,您应当注意,该示例在改写输出文件之前不会检查它们是否已经存在。您可能希望通过询问是否应当改写该文件来提示用户。
还要注意,没有针对密码保护文件的支持。您可以使用 System.Security.Cryptography 命名空间中的类创建自己的加密机制。如果您这样做,则请注意,产生的文件将不与标准 Zip 实用工具(例如,WinZip)兼容。
创建和修改 Zip 文件
Java.util.zip.ZipOutputStream 类使您可以压缩数据并且将结果写入基础 java.io.OutputStream 对象。SharpZip 应用程序适合于处理文件,因此它将压缩数据写入一个新的 java.io.FileOutputStream 对象,但是您可以容易地从 java.io.OutputStream 派生自己的类,或者使用标准类之一将压缩数据直接写入网络或其他存储介质。
CreateEmptyZipFile Helper 函数创建一个 Zip 文件并且立即关闭它。结果得到一个不含任何条目的空 Zip 文件。追加或删除项就没有那么简单了,因为 java.util.zip 包不提供对 Zip 文件的随机访问。对于删除文件,应当将想要保留的条目复制到新的 Zip 文件。对于添加文件,应当将所有条目复制到新的 Zip 文件,然后追加新条目。复制条目涉及到按照我已经描述的方式从源文件中解压缩条目,然后将其重新压缩到目标文件。
为想要添加的每个文件创建一个新的 ZipEntry 实例,并且对该条目调用 setMethod 以设置要使用的压缩方法。受支持的方法是 ZipEntry.DEFLATED(它使用压缩算法压缩数据)和 ZipEntry.STORED(它存储数据但不应用任何压缩)。然后调用 ZipOutputStream.putNextEntry,同时传入新条目,然后通过调用 ZipOutputStream 对象上的写入方法写入它的数据。在完成当前条目的处理时,调用 ZipOutputStream.closeEntry 并继续处理下一个条目。
图 5 中的 UpdateZipFile 函数通过为每个条目调用委托实现了更新和删除,以便您可以选择应当将哪些条目复制到临时文件。最后,新条目被添加到 Zip 文件。
低级别 Zip 压缩
使用 java.util.zip 类,不仅可以压缩文件,还可以压缩应用程序数据。为了说明这一点,我创建了一对函数,以便使用 java.util.zip.Deflater 和 java.util.zip.Inflater 类压缩和解压缩字符串。
压缩函数将创建一个 java.util.zip.Deflater 类的实例。构造函数中的一个参数定义所需的压缩级别。接下来,我调用 Deflater.setInput 类,同时将要压缩的数据作为带符号的字节 (sbyte) 数组进行传递,然后调用 Deflater.finish。
请注意,与 C# 相反,Java 中的 byte 数据类型是带符号的 — Java 中没有无符号 byte 数据类型。这就是 J# 运行库的所有处理缓冲区的方法都采用 sbyte 数组作为参数的原因。
幸运的是,com.ms.vjsharp.struct 命名空间包含 JavaStructMarshalHelper 类,该类除了具有其他功能以外,还能够帮助您执行数组转换。CompressString 函数调用 convertToByteArray 方法,以便将字符串转换为带符号的字节数组。为了获得实际的压缩位,我只是不停地调用 Deflater.deflate,直到 Deflater.finished 返回真以表示已经消耗尽所有输入数据。我在压缩循环内部使用 java.io.ByteArrayOutputStream 的实例收集产生的数据。作为一般规则,在 C# 中处理 Java 类型时,最好使用 JDK 类。它是避免在 sbyte 和 byte 之间反复转换数组的最佳方式。
用于解压缩字符串的代码看起来非常类似于用于压缩的代码。这一次,创建一个 java.util.zip.Inflater 类的实例并调用 setInput 方法,同时传入压缩数据。解压缩循环不断地调用 Inflater.inflate,直到 Inflate.finished 变为真,表示所有输入数据都已经被解压缩。最后,调用 JavaStructMarshalHelper.convertToString 以便将无符号字节数组转换为要由该函数返回的字符串。
CsZipLL 示例应用程序(LL 代表低级别)创建一个长字符串并且将其压缩至大约一半大小。您可以使用这些函数完成某些工作,例如,编写 SOAP 扩展以减少 Web 服务所需的网络带宽。
J# 的其他吸引人的功能
尽管本文重点介绍如何处理 Zip 文件,但该原则也可以应用于 J# 运行库提供了无法从 .NET Framework 标准程序集中获得的功能的其他领域。
由于 J# 为开发人员提供了将他们的 Visual J++ 项目迁移到 .NET Framework 的途径,因此 J# 还实现了很多特定于 Visual J++ 的功能,例如 J/Direct?。J/Direct 技术使 Java 语言程序可以调用本机 Windows 代码。像 Visual J++ 中一样,J# 中的 com.ms.win32 命名空间提供了对大多数 Windows API 函数、数据类型和常量的访问。
User32、Kernel32 和 Gdi32 类包含 Win32?API 函数的核心。这些常量在一些名为 winx(其中,x 是常量的首字母)的接口中被定义为静态字段。例如,ShowWindow API 的 SW_SHOW 标志可以在 com.ms.win32.wins 接口中找到。
为了使接口符合 CLS,它不得包含字段,而 com.ms.win32.winx 接口无法通过该测试。因为 C# 不允许在接口中使用字段,所以 IntelliSense 和 C# 编译器都看不到这些常量,但是您仍然可以使用反射访问这些字段,如下所示:
private int GetWin32IntConstant(string name)
{
System.Reflection.Assembly asm =
System.Reflection.Assembly.GetAssembly(typeof(com.ms.win32.wina));
Type t = asm.GetType("com.ms.win32.win" + char.ToLower(name[0]),
true);
System.Reflection.FieldInfo info = t.GetField(name);
return int.Parse(info.GetValue(null).ToString());
}
使用该技术检索 Windows API 常量速度会很慢,因此您在使用该方法时应当小心。另外一个问题是,由于常量在编译时得不到解析,因此每当您拼错它们时,都会得到运行时错误。在任何情况下,在 .NET 程序集中声明大多数 Windows API 都可以节省大量工作。例如,SharpZip 示例程序显示了与每个文件的扩展名相关联的系统图标。为此,代码调用 com.ms.win32.Shell32 接口中定义的 SHGetFileInfo API 以获得图标的句柄(参见图 6)。
请注意,当您从句柄创建 System.Drawing.Icon 对象时,新 Icon 将不拥有该句柄。这意味着,您必须通过调用 DestroyIcon API 释放关联的资源。由于我不希望在 Icon 对象的整个生存期内存储图标句柄,因此我选择通过使用其句柄上的复制构造函数创建生成 object.Icon 的副本。
尽管 com.ms.win32 命名空间非常巨大,但您应当知道它并未包含每个 Windows API 函数和数据结构。例如,com.ms.win32.Shell32 接口的一个显著疏忽是 SHBrowseForFolder API,它允许我们显示“Browse for Folder”对话框,而无需使用 Microsoft Shell Controls and Automation COM 库。
还请注意,处理回调有点复杂,这是由于 Java 语言不支持委托。对于每个回调类型,都提供了定义函数原型的抽象类。您必须从该类派生以实现处理回调的代码,然后向 API 调用传递该类的一个实例(参见图 7)。另外一个与 Java 语言有关的较小困难是,按引用传递的参数被声明为数组,但是这只影响调用这些函数的代码,而不影响基础功能。
最后,某些 API 调用的转换非常低劣。一个示例是 waveOutOpen(定义在 Winmm 类中)。DwCallback 参数在 C++ 中用于传递事件句柄、窗口句柄、线程 ID 或回调函数,具体取决于 fdwOpen 参数的值。由于 J/Direct 包装将 dwCallback 参数声明为 Int32,并且没有将回调(委托)typecast 到 Int32 的方式,所以必须使用其他通知机制,例如,事件句柄、窗口句柄或线程 ID。
在核心 J# 包中,还有其他一些有趣的东西。例如,java.math.BigDecimal 和 java.math.BigIntegers 类使您可以操纵任意大的数字,这在您编写应用程序以处理加密算法或科学计算时可能非常有用。
CsMath 示例项目显示了如何使用 java.math.BigDecimal,通过 Machin 的公式来计算在小数点后带有任意个数字的 Pi。为了使代码更易读,我在自己的 BigDecimal 类中包装了 java.math.BigDecimal,并且定义了最常用的运算符。
应用程序部署
使用该技术的应用程序要求在目标计算机上安装 J# 运行库和 .NET Framework。就像 .NET Framework 一样,Microsoft 提供了一个可以与应用程序安装程序一起部署的可重新分发的包。
Microsoft 已经表示将继续为桌面操作系统支持 J#。但是,当前 J# 中没有对 .NET Compact Framework 的支持,因此您无法将本文介绍的技术应用于面向智能设备的应用程序。将程序集复制到本地项目目录的操作将无效,因为 J# 运行库程序集极度依赖于本机调用。但是,您可以为使用移动 Web 控件的 Web 应用程序充分利用 J# 运行库。
小结
J# 运行库包含很多可以从 .NET Framework 中的其他语言使用的有用的类。其中一些类使您可以处理 Zip 文件、执行高精度数学计算或者调用 Windows API。尽管可以通过使用第三方库获得该功能的大部分,但 J# 运行库受到 Microsoft 的充分支持,并且是免费的!
相关文章,请参阅:
Java 911: Parlez-vous J/Direct?
有关背景信息,请参阅:
http://msdn.microsoft.com/vjsharp/What is the Common Language Specification?
Ianier Munoz 是 Dokumenta 的一名软件架构师和分析师,该公司是一家总部位于卢森堡的咨询公司。他还创作了 Chronotron 和其他一些流行软件。您可以通过 http://www.chronotron.com 与他联系。