本文假設您熟悉 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 與他聯系。