摘要 如果你的应用程序从未使用过压缩,那么你很幸运。而对于另一部分使用压缩的开发人员来说,好消息是,.NET 2.0如今提供了两个类来处理压缩和解压问题。本文正是想讨论何时以及如何使用这些有用的工具。

   引言

  .NET框架2.0中的一个新名称空间是System.IO.Compression。这个新名称空间提供了两个数据压缩类:DeflateStream和GZipStream。这两个压缩类都支持无损压缩和解压,其设计目的是为了处理流式数据的压缩和解压问题。

  压缩是减少数据大小的有效办法。例如,如果你有巨大量的数据存储在你的SQL数据库中,那么如果你在把这些数据保存到一个表之前压缩一下,你就可以节省大量的磁盘空间。而且,既然现在你把更小块的数据保存到你的数据库中,花费在磁盘I/O方面的操作将会大大减少。压缩的缺点是,它要求你的机器进行另外的处理(因此需要另外的处理时间),并且,在你决定把压缩应用于你的程序之前,你需要计算这一部分时间。

  压缩在你需要在网上传送数据的情况中是极其有用的,特别是对于非常慢且代价昂贵的网络,例如GPRS连接。在这种情况中,使用压缩能够极大地缩小数据尺寸并且减少整个通讯耗费。Web服务是另一个领域-此时,使用压缩能提供巨大的优点,因为XML数据能被高度压缩。

  但是一旦你认为程序的性能代价值得使用压缩,那么你将需要深入地理解.NET 2.0的两个新的压缩类,而这正是我想在本文中所阐述的。

   创建示例应用程序

  在本文中,我将构建一个示例应用程序来展示压缩的使用。该应用程序允许你压缩文件,包括普通文本文件。然后,你能够把该示例中的代码重用于你自己的应用程序中。

  首先,使用Visual Studio 2005创建一个新的Windows应用程序并且使用下列控件来填充默认的表单(见图1):


图1.填充表单:使用所有显示的控件填充默认的Form1。

  · GroupBox控件

  · RadioButton控件

  · TextBox控件

  · Button控件

  · Label控件

  切换到Form1的code-behind并且导入下列名称空间:

Imports System.IO
Imports System.IO.Compression

  在你开始使用压缩类前,理解其工作原理是非常重要的。这些压缩类从一个字节数组中读取数据,压缩它并且把结果存储到一个流对象中。对于解压来说,解压存储到一个流对象中的数据,然后把它存储到另一个流对象中。

  首先,定义Compress()函数,它有两个参数:algo和data。第一个参数指定使用哪种算法(GZip或Deflate);第二个参数是一个包含要压缩的数据的字节数组。一个内存流对象将被用来存储压缩数据。一旦压缩完成,你需要计算压缩比,这是用压缩的数据的大小除以解压数据的大小计算的。

  然后,存储在内存流中的压缩的数据被复制到另一个字节数组中并且被返回到调用函数。另外,你还要使用一个StopWatch对象来跟踪该压缩算法使用了多少时间。Compress()函数定义如下:

Public Function Compress(ByVal algo As String, ByVal data() As Byte) As Byte()
Try
 Dim sw As New Stopwatch
 '---ms用于存储压缩的数据---
 Dim ms As New MemoryStream()
 Dim zipStream As Stream = Nothing
 '---开始秒表计时---
 sw.Start()
 If algo = "Gzip" Then
  zipStream = New GZipStream(ms, CompressionMode.Compress, True)
 ElseIf algo = "Deflate" Then
  zipStream = New DeflateStream(ms, CompressionMode.Compress, True)
 End If
 '---使用存储在数据中的信息进行压缩---
 zipStream.Write(data, 0, data.Length)
 zipStream.Close()
 '---停止秒表---
 sw.Stop()
 '---计算压缩比---
 Dim ratio As Single = Math.Round((ms.Length / data.Length) * 100, 2)
 Dim msg As String = "Original size: " & data.Length & _
", Compressed size: " & ms.Length & _
", 压缩比: " & ratio & "%" & _
", Time spent: " & sw.ElapsedMilliseconds & "ms"
 lblMessage.Text = msg
 ms.Position = 0
 '---用来存储压缩了的数据(字节数组)---
 Dim c_data(ms.Length - 1) As Byte
 '---把内存流的内容读取到字节数组---
 ms.Read(c_data, 0, ms.Length)
 Return c_data
Catch ex As Exception
 MsgBox(ex.ToString)
 Return Nothing
End Try
End Function

  这个Decompress()函数将解压由Compress()函数压缩的数据。第一个参数指定要使用的算法。包含压缩的数据的字节数组被作为第二个参数传递,然后它被复制到一个内存流对象中。然后,这些压缩类将解压存储在内存流中的数据,然后把解压的数据存储到另一个流对象中。为了获得解压的数据,你需要读取来自流对象的数据。这是通过使用RetrieveBytesFromStream()函数来实现的(将在后面解释)。

  Decompress()函数的定义如下所示:

Public Function Decompress(ByVal algo As String, ByVal data() As Byte) As Byte()
Try
 Dim sw As New Stopwatch
 '---复制数据(压缩的)到ms---
 Dim ms As New MemoryStream(data)
 Dim zipStream As Stream = Nothing
 '---开始秒表---
 sw.Start()
 '---使用存储在ms中的数据解压---
 If algo = "Gzip" Then
  zipStream = New GZipStream(ms, CompressionMode.Decompress)
 ElseIf algo = "Deflate" Then
  zipStream = New DeflateStream(ms, CompressionMode.Decompress, True)
 End If
 '---用来存储解压的数据---
 Dim dc_data() As Byte
 '---解压的数据存储于zipStream中;
 '把它们提取到一个字节数组中---
 dc_data = RetrieveBytesFromStream(zipStream, data.Length)
 '---停止秒表---
 sw.Stop()
 lblMessage.Text = "Decompression completed. Time spent: " & _
sw.ElapsedMilliseconds & "ms" & _
", Original size: " & dc_data.Length
 Return dc_data
Catch ex As Exception
 MsgBox(ex.ToString)
 Return Nothing
End Try
End Function

  这个RetrieveBytesFromStream()函数使用了两个参数:一个流对象,一个整数,并返回一个包含解压的数据的字节数组。这个整数参数用于决定每次把多少个字节从该流对象中读取到字节数组中。这是必要的,因为当数据被解压时,你不知道存在于流对象中的解压数据的大小。因此,有必要动态地把字节数组扩展成块以便存储在运行时刻期间解压缩的数据中。当你不断地扩展字节数组时,块太大会浪费内存,而块太小则会失去珍贵的时间。因此,可以由调用例程来决定要读取的最佳块大小。

  RetrieveBytesFromStream()函数的定义如下:

Public Function RetrieveBytesFromStream( _
ByVal stream As Stream, ByVal bytesblock As Integer) As Byte()
'---从一个流对象中检索字节---
 Dim data() As Byte
 Dim totalCount As Integer = 0
 Try
  While True
   '---逐渐地增加数据字节数组-的大小--
   ReDim Preserve data(totalCount + bytesblock)
   Dim bytesRead As Integer = stream.Read(data, totalCount, bytesblock)
   If bytesRead = 0 Then
    Exit While
   End If
   totalCount += bytesRead
  End While
  '---确保字节数组正确包含提取的字节数---
  ReDim Preserve data(totalCount - 1)
  Return data
 Catch ex As Exception
  MsgBox(ex.ToString)
  Return Nothing
End Try
End Function

  注意,在Decompress()函数中,你调用了RetrieveBytesFromStream()函数,如下所示:

dc_data = RetrieveBytesFromStream(zipStream, data.Length)

  块大小是指压缩的数据的大小(data.length)。在大多数情况中,解压缩的数据要比压缩的数据大几倍(由压缩比所显示),因此,在运行时刻期间你将至多动态地扩展字节数组几倍。作为一个例子,假定压缩比是百分之20而压缩的数据的大小为2MB,那么,在这种情况中,解压的数据将是10MB。因此,该字节数组将被动态地扩展5倍。理想情况下,在运行时刻期间该字节数组不应该被扩展太频繁,因为这将会严重地减慢应用程序运行速度。但是使用压缩的数据的大小作为块大小确是一种好的办法。
处理压缩事件

  现在,既然定义好了主要的压缩和解压例程,那么接下来你就可以为各种按钮进行编码了。相应于Compress按钮的事件处理器如下:

Private Sub btnCompress_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnCompress.Click
'---用来存储压缩的数据---
 Dim compressedData() As Byte
 '---压缩数据---
 If rbGZipStream.Checked Then
  compressedData = Compress("Gzip",System.Text.Encoding.ASCII.GetBytes(txtBefore.Text))
 Else
  compressedData = Compress("Deflate",System.Text.Encoding.ASCII.GetBytes(txtBefore.Text))
 End If
 '---把压缩的数据复制到一个字符串中---
 Dim i As Integer
 Dim s As New System.Text.StringBuilder()
 For i = 0 To compressedData.Length - 1
  If i <> compressedData.Length - 1 Then
   s.Append(compressedData(i) & " ")
  Else
   s.Append(compressedData(i))
  End If
 Next
 '---显示压缩的数据为一个字符串---
 txtAfter.Text = s.ToString
End Sub

  在txtBefore控件中的数据被转换成一个字节数组,然后被压缩。然后,该压缩的数据被转换成字符串以便于在txtAfter中显示。

  相应于Decompress按钮的事件处理器如下:

Private Sub btnDecompress_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnDecompress.Click
'---把压缩的字符串格式化成一个字节数组---
 Dim eachbyte() As String = txtAfter.Text.Split(" ")
 Dim data(eachbyte.Length - 1) As Byte
 For i As Integer = 0 To eachbyte.Length - 1
  data(i) = Convert.ToByte(eachbyte(i))
 Next
 '---解压数据并且显示解压的数据---
 If rbGZipStream.Checked Then
  txtBefore.Text = System.Text.Encoding.ASCII.GetString(Decompress("Gzip", data))
 Else
  txtBefore.Text = System.Text.Encoding.ASCII.GetString(Decompress("Deflate", data))
 End If
End Sub

  它把显示在控件txtAfter中的数据转换成一个字节数组,然后发送它以便进行解压。解压缩的数据被显示回txtBefore控件中。

  相应于"Select file to compress"按钮的事件处理器代码如下:

Private Sub btnSelectFile_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnSelectFile.Click
'---让用户选择一个要压缩的文件--
 Dim openFileDialog1 As New OpenFileDialog()
 'openFileDialog1.InitialDirectory = "c:"
 openFileDialog1.Filter = "All files (*.*)|*.*"
 openFileDialog1.RestoreDirectory = True
 If openFileDialog1.ShowDialog() = Windows.Forms.DialogResult.OK Then
 '---把文件的内容读入字节数组---
 Dim fileContents As Byte()
 fileContents = My.Computer.FileSystem.ReadAllBytes(openFileDialog1.FileName)
 '---创建gzip文件---
 Dim filename As String = openFileDialog1.FileName & ".gzip"
 If File.Exists(filename) Then File.Delete(filename)
  Dim fs As FileStream = New FileStream(filename, FileMode.CreateNew, FileAccess.Write)
  '---压缩文件的内容---
  Dim compressed_Data As Byte()
  If rbGZipStream.Checked Then
   compressed_Data = Compress("Gzip", fileContents)
  Else
   compressed_Data = Compress("Deflate", fileContents)
  End If
  If compressed_Data IsNot Nothing Then
   '---把压缩的内容写进压缩的文件中---
   fs.Write(compressed_Data, 0, compressed_Data.Length)
   fs.Close()
  End If
 End If
End Sub

  它读取由用户选择的文件的内容,压缩它,并且创建一个包含压缩的数据的新文件(具有一样的文件名,但是加上了一个.gzip扩展名)。

  相应于"Select file to Decompress"按钮的事件处理器代码如下:

Private Sub btnDecompressFile_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnDecompressFile.Click
 '---让用户选择一个要解压的文件---
 Dim openFileDialog1 As New OpenFileDialog()
 ' openFileDialog1.InitialDirectory = "c:"
 openFileDialog1.Filter = "All GZIP files (*.gzip)|*.gzip"
 openFileDialog1.RestoreDirectory = True
 If openFileDialog1.ShowDialog() = Windows.Forms.DialogResult.OK Then
 '---把压缩的文件的内容读入到字节数组---
 Dim fileContents As Byte()
 fileContents = My.Computer.FileSystem.ReadAllBytes(openFileDialog1.FileName)
 '---解压文件的内容---
 Dim uncompressed_Data As Byte()
 If rbGZipStream.Checked Then
  uncompressed_Data = Decompress("Gzip", fileContents)
 Else
  uncompressed_Data = Decompress("Deflat", fileContents)
 End If
 '---创建解压的文件---
 Dim filename As String = openFileDialog1.FileName.Substring(0, openFileDialog1.FileName.Length - 5)
 If File.Exists(filename) Then File.Delete(filename)
  Dim fs As FileStream = New FileStream(filename,FileMode.CreateNew, FileAccess.Write)
  If uncompressed_Data IsNot Nothing Then
   '---把解压内容写入到文件中---
   fs.Write(uncompressed_Data, 0, uncompressed_Data.Length)
   fs.Close()
  End If
 End If
End Sub

  它读取用户选择的文件的内容,解压之,并且创建一个包含解压的数据的新文件(通过去掉它的.gzip扩展名)。
测试应用程序

  按F5测试应用程序(见图2)。


图2.测试应用程序:选择使用的压缩算法,然后你可以压缩一个文本串或一个文件内容。

  你应该注意下列事实:

  · 压缩小数量的文本实际上将会导致一种较大的压缩文本。

  · 不同的文本将产生不同的压缩比,尽管字符数是固定的。

  · 文本文件压缩效果最好;它们能够带来最好的压缩比。

  · 其它二进制的文件,例如.exe,jpg,通常压缩效果并不很好并且可能会导致大于百分比之100的压缩比,这是没有价值的。

  需要注意的是,.NET中的GZIP和Deflate算法的实现要比市场上的其它第三方GZIP工具具有较低的效率。尽管你能够使用.NET类把10MB的数据压缩到4MB,但是你发现使用一种第三方工具可能会达到一种更小的压缩大小。另外,这个压缩类无法操作大于4GB的数据。然而,在.NET中的实现将允许你解压使用市场中的其它GZIP工具压缩的所有的文件。

   小结

  在本文中,你已经看到了如何在.NET 2.0中使用压缩类。尽管这种实现还不如市场上的那些非MS方案有效,但是它的确为你提供了一种容易(免费)的方式来在你的.NET应用程序中加入压缩功能。