本文討論:
XmlLite 與其他可用 XML 分析器的比較
XMLLite 的優勢和限制
讀取和寫入 XML
XML 安全注意事項
本文使用了以下技術:
XML、C++
目錄
為什麼推出新的 XML 分析器?
COM“Lite”
讀取 XML
寫入 XML
使用流
讀取時的文本編碼
寫入時的文本編碼
處理大數據值
安全注意事項
總結
盡管 .Net Framework 不斷取得成功,Microsoft 仍然認真對待本機 C++ 開發。通過引入 XmlLite(適合於用本機 C++ 編寫的應用程序的高性能、低開銷 XML 讀取器和寫入器)說明了這一點。
托管代碼通過 System.Xml 命名空間廣泛支持 XML,而依賴於 COM 的傳統 Visual Basic® 和 C++ 應用程序可以訪問 Microsoft® XML 核心服務 (MSXML) 中的類似功能。但是,這些並沒有為需要快速精簡的 XML 分析器的本機 C++ 開發人員提供富有吸引力的選項。開始使用 XMLLite 吧。
本文將探討您可以對 XmlLite 執行的操作。但是,首先,為設定預期,我希望快速回顧一下 XmlLite 未提供的內容,至少是此初始版本中未提供的內容。對於初學者,它既未提供文檔對象模型 (DOM) 實現,也未提供 XML 架構或文檔類型定義 (DTD) 驗證。它還缺少對高級工具的支持,例如基於光標的導航(如 XPath)、樣式表和序列化。但是,通過建立在 XMLLite 之上的功能,可以根據需要填補任何空白,Microsoft .Net Framework 中的幾乎所有 XML 功能同樣都建立在 XmlReader 和 XMLWriter 類之上。
那麼,XmlLite 提供了哪些內容?簡單地說,它提供了非緩存的只進分析器(提供接收式編程模型)和非緩存的只進 XML 生成器。已證明這兩者是非常有價值的功能。
為什麼推出新的 XML 分析器?
開發人員日益熟悉他們每天使用的庫,通過廣泛使用 XML,他們肯定會詢問有關新推出的 XML 分析器的一些疑難問題。要了解這一新分析器的價值,讓我們首先考慮一下 XML 分析器當今的情形。
很自然地,如果應用程序已經利用 .NET Framework,則決定通常是很簡單的:只需使用 System.Xml 即可。為證明這一點,XmlLite 的設計基於 .NET Framework 中 XmlReader 和 XmlWriter 類的設計。從以 C++ 編寫的托管應用程序使用 XmlLite 通常沒有優勢。XmlLite 的功能畢竟比 XmlReader 和 XmlWriter 類提供的功能要少得多。(圖 1 中的表略述 XMLLite 中的主要類型如何映射到 .Net Framework 中的主要類型。)另一方面,如果應用程序僅使用本機代碼,那麼就 Microsoft 技術而言,MSXML 在傳統上是所選的解決方案。
Figure1XMLLite 類映射到 .Net Framework
CreateXMLReaderInputWithEncodingName
CreateXMLWriterOutputWithEncodingCodePage
CreateXMLWriterOutputWithEncodingName
編碼類 XMLNodeType 枚舉 XMLNodeType 枚舉MSXML 提供了兩個差異很大的 XML 分析器。第一個分析器是在各種情形下可用的 DOM 實現。如果使用較小的 XML 文檔且需要隨機訪問 XML 文檔進行內存中讀取和寫入,則 DOM 實現是一種合理的選擇。MSXML 的更高版本引入了“用於 XML 的簡單 API (SAX2)”的實現。它實際上是否簡單是有爭議的。使用 SAX2 時(甚至在開始之前),您需要實現至少兩個 COM 接口:一個用於接收 XML 文檔中各個節點的通知,另一個用於接收分析錯誤的通知。
將 SAX2 實現添加到 MSXML 的原因如下:與 DOM 實現不同,SAX2 分析器以數據流形式讀取 XML 文檔,並通知您何時到達各個節點。這意味著,您的應用程序的內存使用量並不隨所分析文檔的大小而增加。
SAX2 存在的問題以及 .NET Framework 不提供其實現的原因在於 SAX2 模型的內在復雜性。它要求實現接口或事件,並強制開發人員使用更為間接的編程模型,要求開發人員管理注定會使應用程序變得復雜的其他狀態。相反,.Net Framework 中的 XmlReader 和 XmlWriter 類以及 XmlLite 的 IXmlReader 和 IXMLWriter 接口提供了簡單易懂的分析器,可以直接在函數中使用,而不必管理任何外部狀態或通知。
由於其設計的簡明性,XmlLite 能夠提供相當好的性能,即使與 MSXML SAX2 實現相比也是如此。雖然 SAX2 分析器可以比 DOM 實現更好地處理大型文檔,但是與 XMLLite 相比就遜色了。
簡單地說,XmlLite 優於 MSXML,且它更易於從本機 C++ 使用。MSXML 仍將是 Visual Basic 和基於 COM 的腳本語言的最可行解決方案,但是現在本機 Visual C++® 最終具有了專門為它設計的 XML 分析器。雖然 Windows Vista™ 和更高版本中附帶有 XmlLite,但是一個更新對於 Windows® XP 和 Windows Server® 2003 的 32 位和 64 位版本也是可用的。因為未涉及 COM 注冊,所以此更新包應該不會導致 MSXML 通常造成的有關安裝和版本控制的難題。
COM“Lite”
XmlLite 不僅是易記的名稱;事實上,它是一個輕型 XML 分析器。XMLLite 利用了 COM 的精華,即編程規范和約定,並拋棄了復雜的和可能不必要的部分,如 COM 注冊、運行時服務、代理、線程模型、封送處理等。
從 XmlLite.dll 導出的函數創建 XML 讀取器和寫入器。通過鏈接到 XmlLite.lib 並包括 Windows SDK 中的 XmlLite.h 頭文件,可以訪問它們。生成的 COM 樣式接口使用熟悉的 IUnknown 接口方法進行生存期管理。COM IStream 接口也起到一定作用並表示存儲器。除此之外,沒有 COM 的依賴項;無需注冊任何 COM 類或甚至調用強制性的 CoInitialize 函數。活動模板庫 (ATL) CComPtr 類處理剩余的一小部分 COM。但是,您確實需要關注線程安全,因為出於單線程方案中的性能,XMLLite 不是線程安全的。
我在以下示例中使用 COM_VERIFY 宏,以便清晰地識別方法在何處返回需要檢查的 HRESULT。可以將此替換為相應的錯誤處理 - 不管該操作引發異常還是您自己返回 HRESULT。
讀取 XML
XmlLite 提供了返回 IXmlReader 接口實現的 CreateXMLReader 函數:
CComPtr<IXMLReader> reader;
COM_VERIFY(::CreateXmlReader(__uuidof(IXMLReader),
reinterpret_cast<void**>(&reader),
0));
雖然是可選的,但是 CComPtr 類模板確保迅速釋放接口指針。
CreateXmlReader 接受接口標識符 (IID) 以及指向 void 指針的指針。這是 COM 編程中的常見模式,允許調用方指定要返回的接口指針的類型。我的示例使用 __uuidof 運算符,該運算符是 Microsoft 特定的關鍵字,用於提取與類型關聯的 GUID。在這種情況下,它用於檢索接口的 IID。CreateXMLReader 的最後一個參數接受可選的 IMalloc 實現以允許調用方控制內存分配。
創建讀取器後,需要指示讀取器將用作輸入的存儲器。IStream 接口表示存儲器,這樣就可以將 XMLLite 與可能設計的任何流實現一起使用:
CComPtr<IStream> stream;
// Create stream object here...
COM_VERIFY(reader->SetInput(stream));
(我將在本文的後面部分中討論流。)
設置 XML 讀取器的輸入後,可以通過重復調用 Read 方法進行讀取。Read 方法接受一個可選參數,該參數在每次成功調用時返回節點類型。Read 方法返回 S_OK 以指示已從流中成功讀取下一個節點,返回 S_FALSE 以指示已到達流的結尾處。以下是如何依次枚舉節點的一個示例:
HRESULT result = S_OK;
XmlNodeType nodeType = XMLNodeType_None;
while (S_OK == (result = reader->Read(&nodeType)))
{
// Get node-specific info
}
要枚舉當前節點的屬性,請使用 MoveToFirstAttribute 和 MoveToNextAttribute 方法。如果已成功地重新定位讀取器,則這兩種方法都返回 S_OK;如果不存在更多的屬性,則返回 S_FALSE。以下示例說明如何依次枚舉給定節點的屬性:
for (HRESULT result = reader->MoveToFirstAttribute();
S_OK == result;
result = reader->MoveToNextAttribute())
{
// Get attribute-specific info
}
調用 IXMLReader 的 Read 方法時,它會將任何節點屬性自動存儲在內部集合中。這樣,您就可以使用 MoveToAttributeByName 方法,按名稱將讀取器移動到特定的屬性。但是,枚舉屬性並將其存儲在應用程序特定的數據結構中,效率通常更高。請注意,您還可以使用 GetAttributeCount 方法確定當前節點中的屬性數。
確定節點或屬性後,獲取其信息就很簡單了。以下示例演示如何獲取給定節點的命名空間 URI 和本地名稱:
PCWSTR namespaceUri = 0;
UINT namespaceUriLength = 0;
COM_VERIFY(reader->GetNamespaceUri(&namespaceUri,
&namespaceUriLength));
PCWSTR localName = 0;
UINT localNameLength = 0;
COM_VERIFY(reader->GetLocalName(&localName,
&localNameLength));
返回字符串值的所有 IXMLReader 方法都遵循此模式。第一個參數接受指向寬字符指針常量的指針。第二個參數是可選的;如果它不為零,則它將返回以字符度量的字符串長度(不包括空結束符)。
以下是強調性能的另一個示例。僅在將讀取器移動到其他節點或以某種其他方式(如通過設置新的輸入流或釋放 IXmlReader 接口)使當前節點無效之前,從 IXmlReader 方法返回的字符串指針才是有效的。換句話說,IXMLReader 不會將流的副本返回給調用方。
與其在 .NET Framework 中的對應方不同,IXMLReader 未提供讀取鍵入內容的任何方法。例如,如果特定的元素或屬性包含數字或日期,則您需要首先獲取其字符串表示形式,然後根據需要自己進行轉換。.Net Framework 的 XmlReader 類中存在的許多其他 helper 方法也不存在於 IXmlReader 中,但是可以作為 helper 函數編寫。XMLLite 確實符合最小接口設計的 C++ 理論。
圖 2 顯示使用 IXmlReader 讀取 XML 文檔時涉及的對象和抽象。但是,請牢記,IStream 可以抽取任何存儲,此處顯示的文件僅僅是一個常見示例。
圖 2讀取器
寫入 XML
XmlLite 提供了返回 IXmlWriter 接口實現的 CreateXMLWriter 函數:
CComPtr<IXMLWriter> writer;
COM_VERIFY(::CreateXmlWriter(__uuidof(IXMLWriter),
reinterpret_cast<void**>(&writer),
0));
創建寫入器後,需要指示寫入器將用作輸出的存儲器:
CComPtr<IStream> stream;
// Create stream object here
COM_VERIFY(writer->SetOutput(stream));
開始寫入之前,可以修改寫入器屬性。XmlWriterProperty 枚舉定義可用的屬性。例如,您可能希望指定是否縮進 XML 輸出以便於讀者閱讀(使用 SetProperty 方法可以做到這一點):
COM_VERIFY(writer->SetProperty(XMLWriterProperty_Indent, TRUE));
然後可以開始使用 IXmlWriter 方法寫入基礎流。XmlLite 支持 XML 片段。如果計劃寫入完整的 XML 文檔,則應該從調用 WriteStartDocument 方法(它負責寫入 XML 聲明)開始。聲明取決於所用的編碼,但是默認編碼為 UTF-8,它在大多數情況下都應該是合適的。(稍後將介紹文本編碼。)提供了許多 WriteXxx 方法,用於寫入各種節點類型、屬性和值。
請考慮以下示例:
COM_VERIFY(writer->WriteStartDocument(XMLStandalone_Omit));
COM_VERIFY(writer->WriteStartElement(0, L"Html",
L"http://www.w3.org/1999/xHtml"));
COM_VERIFY(writer->WriteStartElement(0, L"head", 0));
COM_VERIFY(writer->WriteElementString(0, L"title", 0, L"My Web Page"));
COM_VERIFY(writer->WriteEndElement()); // </head>
COM_VERIFY(writer->WriteStartElement(0, L"body", 0));
COM_VERIFY(writer->WriteElementString(0, L"p", 0, L"Hello world!"));
COM_VERIFY(writer->WriteEndDocument());
WriteStartDocument 方法處理將 XML 聲明寫入流的操作。它只有一個參數,該參數接受來自 XmlStandalone 枚舉的值,指示是否出現獨立的文檔聲明,如果是這樣,則指示它保存的值。寫入 XML 片段時,通常省略對 WriteStartDocument 的調用。
WriteStartElement 方法接受以下三個參數:第一個參數指定元素的可選命名空間前綴,第二個參數指定元素的本地名稱,第三個參數指定可選的命名空間 URI。WriteElementString 是 XMLLite 提供的非常方便的方法之一。用於寫入 XHtml 文檔標題的以下代碼等效於上一示例中使用的 WriteElementString:
COM_VERIFY(writer->WriteStartElement(0, L"title", 0));
COM_VERIFY(writer->WriteString(L"My Web Page"));
COM_VERIFY(writer->WriteEndElement());
顯然,WriteElementString 方法不是絕對必要的,但它確實很有用。
最後,WriteEndDocument 方法用於關閉文檔。您可能已注意到,未顯式關閉 body 和 Html 元素。WriteEndDocument 會自動關閉任何打開的元素。就此而言,釋放寫入器也會關閉任何剩余的元素。但是,如果您不小心,則未顯式關閉此類元素的做法可能會導致錯誤,因為流的生存期和寫入器的生存期通常可以不同。要說的是,如果需要確保已將所有要寫入的內容寫入基礎流,則只需調用 IXMLWriter 的 Flush 方法即可。
圖 3 顯示使用 IXmlWriter 寫入 XML 文檔時涉及的對象和抽象流。請牢記,IStream 可以抽取任何存儲,此處的文件僅僅是一個常見示例。
圖 3寫入器
使用流
到此為止,我對流進行的介紹並不多。與一些功能更全面的 XML 庫不同,XMLLite 未提供任何支持從公共存儲位置(如文件或通過網絡協議)讀取和向其寫入的功能。正因為這一點,對於希望從其讀取或向其寫入的任何存儲器,您都需要提供 IStream 實現。實現 IStream 接口並不復雜,但是在許多情況下,您不需要執行此操作,因為實現可能已存在。
CreateStreamOnHGlobal 函數提供由虛擬內存支持的 IStream 實現。第一個參數是使用 GlobalAlloc 函數創建的可選內存句柄。但是,只需傳遞零,CreateStreamOnHGlobal 即可為您創建內存對象。以下示例創建一個由系統內存支持且將根據需要動態增長的 IStream 實現:
CComPtr<IStream> stream;
COM_VERIFY(::CreateStreamOnHGlobal(0, TRUE, &stream));
釋放流將釋放內存。
SHCreateStreamOnFile 函數提供了另一個有用的 IStream 實現。它創建由文件支持的 IStream:
CComPtr<IStream> stream;
COM_VERIFY(::SHCreateStreamOnFile(L"D:Sample.XML",
STGM_WRITE | STGM_SHARE_DENY_WRITE,
&stream));
讀取時的文本編碼
雖然默認情況下 XmlLite 使用 UTF-8 進行寫入,但是如果在讀取時嘗試檢測文本編碼,則可以覆蓋此行為。首先,讓我們看一下您將自動獲取的信息。對於給定的流,IXmlReader 將通過作為 XML 前同步碼的字節順序標記來檢測編碼提示。IXmlReader 還將允許在 XML 聲明中指定的任何編碼。期望任何 XML 分析器都具有這兩個特征。如果具有可能未定義任何編碼信息的輸入流,而且 XmlLite 無法試探性地確定正使用的編碼,則可以將 IXMLReader 定向到特定的編碼(如果給定了代碼頁或編碼名稱)。
可以假借 IXmlReaderInput 接口創建 XML 讀取器輸入對象,而不是將流直接傳遞到 IXmlReader。提供了兩個用於創建包裝輸入流的輸入對象的函數。CreateXmlReaderInputWithEncodingCodePage 函數接受代碼頁編號形式的代碼。CreateXmlReaderInputWithEncodingName 函數接受使用其規范名稱的編碼。除此之外,這兩個函數具有完全相同的簽名。概括一下,通常可以對 XML 讀取器的輸入流進行如下設置:
CComPtr<IStream> stream;
// Create stream object here
COM_VERIFY(reader->SetInput(stream));
要覆蓋編碼,請將代碼更改為:
CComPtr<IStream> stream;
// Create stream object here
CComPtr<IXMLReaderInput> input;
COM_VERIFY(::CreateXMLReaderInputWithEncodingName(stream,
0, // default allocator
L"ISO-8859-8",
TRUE, // hint
0, // base URI
&input));
COM_VERIFY(reader->SetInput(input));
第一個參數指示 XML 讀取器將從其讀取的流。第二個參數接受可選的 IMalloc 實現。如果提供的話,則它將覆蓋 XML 讀取器自己的實現。第三個參數指定編碼名稱。msdn2.microsoft.com/ms752827.ASPx 上的文檔列出了本機支持的編碼;要支持其他編碼,可以提供 IMultiLanguage2 接口實現。下一個參數指示是否必須使用指定的編碼或者它是否僅僅是一個提示。如果指定 TRUE,則指示分析器嘗試使用建議的編碼,但是如果它失敗,則可以隨意嘗試試探性地確定實際的編碼。如果指定 FALSE,則指示分析器嘗試建議的編碼;如果它與輸入流不匹配,則返回錯誤。下一個參數接受可能用於解析外部實體的可選基本 URI。最後一個參數返回表示要傳遞到 SetInput 方法的輸入對象的接口指針。
寫入時的文本編碼
XML 寫入器將基於傳遞到 SetOutput 方法的對象確定要使用的編碼。如果該對象實現 IStream 接口或者甚至實現有限的 ISequentialStream 接口,則 XML 寫入器將使用 UTF-8 編碼。可以創建 XML 寫入器輸出對象來覆蓋此行為。提供了兩個用於創建包裝輸出流的輸出對象的函數。CreateXmlWriterOutputWithEncodingCodePage 函數接受代碼頁編號形式的編碼,而 CreateXmlWriterOutputWithEncodingName 函數接受使用其規范名稱的編碼。除此之外,這兩個函數具有完全相同的簽名。通常,可以對 XML 寫入器的輸出流進行如下設置:
CComPtr<IStream> stream;
// Create stream object here
COM_VERIFY(writer->SetOutput(stream));
要覆蓋默認編碼,請編寫以下代碼:
CComPtr<IStream> stream;
// Create stream object here
CComPtr<IXMLWriterOutput> output;
COM_VERIFY(::CreateXMLWriterOutputWithEncodingName(stream,
0,
L"ISO-8859-8",
&output));
COM_VERIFY(writer->SetOutput(output));
第一個參數指示 XML 寫入器將寫入的流。第二個參數接受可選的 IMalloc 實現。如果提供的話,則它將覆蓋 XML 寫入器自己的實現。第三個參數指定編碼名稱。最後一個參數返回表示要傳遞到 SetOutput 方法的輸出對象的接口指針。
處理大數據值
為了在讀取大數據值時限制內存使用,XML 讀取器提供了按數據塊讀取值的機制。IXMLReader ReadValueChunk 方法讀取的字符數不超過規定的最大字符數,在預料到後續調用時向前移動讀取器。以下示例說明如何重復調用 ReadValueChunk 以讀取大數據值:
CString value;
WCHAR chunk[256] = { 0 };
HRESULT result = S_OK;
UINT charsRead = 0;
while (S_OK == (result = reader->ReadValueChunk(chunk,
countof(chunk),
&charsRead)))
{
value.Append(chunk, charsRead);
}
當不再有數據可用時,ReadValueChunk 返回 S_FALSE。在此示例中,我要將數據塊寫入 CString 對象。這僅僅是為了說明如何管理數據塊的長度,顯然這在實際中會抵消數據分塊的優勢。
安全注意事項
以 XML 為中心的應用程序必須總是處理來自不可信源的 XML。XMLLite 提供了許多工具以保護應用程序免受已知漏洞和將來漏洞的攻擊。
XML 文檔可以包含對外部實體的引用。一些 XML 分析器自動解析這些實體。雖然此方法可能很有用,但是,如果未仔細編寫 XML 解析程序以緩解各種威脅,則此方法可能會造成安全漏洞的攻擊。XmlLite 既不自動解析外部實體,也不提供 XML 解析程序。要提供自己的實現(如有必要),請實現 IXmlResolver 接口並將 XmlReaderProperty_XmlResolver 屬性與 IXMLReader SetProperty 方法一起使用,以指示讀取器使用您的解析程序。
XML 文檔可能還包含 DTD 處理說明。雖然 XmlLite 不支持文檔驗證(使用 XML 架構或 DTD),但是它支持 DTD 實體擴展和默認屬性。由於這些 DTD 可以包含對外部實體的引用,因此它們可能會使您的應用程序受到各種攻擊。默認情況下,XmlLite 禁用 DTD 處理。通過將 XmlReaderProperty_DtdProcessing 屬性設置為 DtdProcessing_Parse 值,可以允許 DTD 處理。此外,還存在由 XMLReaderProperty_MaxEntityExpansion 控制的 DTD 實體擴展攻擊(也稱為 billion laughs 攻擊)的內置緩解措施。此屬性的默認值為 100,000。
攻擊者可以利用使用 XML 的應用程序的另一種方法是,創建名稱非常長的文檔。如果未能阻止,則這可能用盡巨大的內存並允許拒絕服務攻擊。我已經提示了可以執行的方法。緩解此類威脅的一種明顯方法是,按數據塊讀取大數據值,如上一部分所述。另一種有用的方法是,提供限制內存分配的自定義 IMalloc 實現。如果輸入流支持隨機訪問,則還可以指示 XML 讀取器使用 XMLReaderProperty_RandomAccess 屬性來避免緩存屬性。這將減少用於讀取開始元素標記的內存量,但是也可能降低分析速度,因為分析器必須來回查找以便在請求時檢索各個屬性值。
如果 XML 層次結構過深,則也可能快速用盡系統資源。要阻止攻擊者提供層次結構過深的 XML 文檔,可以使用 XMLReaderProperty_MaxElementDepth 屬性限制分析器將允許的深度。此屬性默認為 256。
總結
XmlLite 為本機 C++ 應用程序提供了功能強大的 XML 分析器。它著重於性能,知道它所使用的系統資源,為控制這些特征提供了很大的靈活性。XmlLite 支持所有的常見文本編碼,是一種非常有用的實用工具,可以簡化本機 C++ 應用程序中的 XML 使用。有關詳細信息,請參閱 msdn2.microsoft.com/ms752872.ASPx 上的 XMLLite 文檔。
Kenny Kerr是一名專門從事 Windows 軟件開發的軟件專家。他熱衷於撰寫有關編程和軟件設計的文章和向開發人員傳授與此有關的知識。可通過 http://weblogs.ASP.Net/kennykerr 與 Reach Kenny 取得聯系。