什么是序列化
程序員在編寫應用程序的時候往往需要將程序的某些數據存儲在內存中,然后將其寫入某個文件或是將它傳輸到網絡中的另一臺計算機上以實現通訊。這個將程序數據轉化成能被存儲并傳輸的格式的過程被稱為“序列化”(Serialization),而它的逆過程則可被稱為“反序列化”(Deserialization)。
簡單來說,序列化就是將對象實例的狀態(tài)轉換為可保持或傳輸的格式的過程。與序列化相對的是反序列化,它根據流重構對象。這兩個過程結合起來,可以輕松地存儲和傳輸數據。例如,可以序列化一個對象,然后使用 HTTP 通過 Internet 在客戶端和服務器之間傳輸該對象。
2 為什么使用序列化2.1 哪些情況需要使用序列化2.1.1 以某種存儲形式使自定義對象持久化通過序列化,可以將對象的狀態(tài)保持在存儲媒體中,在以后能夠重新創(chuàng)建精確的副本。我們經常需要將對象的字段值保存到磁盤中,并在以后檢索此數據。盡管不使用序列化也能完成這項工作,但這種方法通常很繁瑣而且容易出錯,并且在需要跟蹤對象的層次結構時,會變得越來越復雜??梢韵胂笠幌戮帉懓罅繉ο蟮拇笮蜆I(yè)務應用程序的情形,程序員不得不為每一個對象編寫代碼,以便將字段和屬性保存至磁盤以及從磁盤還原這些字段和屬性。序列化提供了輕松實現這個目標的快捷方法。
2.1.2 將對象從一個地方傳遞到另一個地方通常來說,對象僅在創(chuàng)建對象的應用程序域中有效。但是,序列化可以通過值將對象從一個應用程序域發(fā)送到另一個應用程序域中。例如,序列化可用于在ASP.NET中保存會話狀態(tài)并將對象復制到Windows窗體的剪貼板中。序列化最重要的目的之一就是在網絡上傳輸對象。
2.2 序列化的優(yōu)勢在系統(tǒng)化的序列化方法出現之前,程序員如果想要將自定義的一個類的對象持久化地保存下來,并進行傳輸,可以采用以下這些方法:
l 由程序員自己實現保存對象數據的功能,針對每一個對象編寫代碼,將其數據存儲下來。
l 將對象強制轉換為char*或者void*類型的數據,然后進行數據的傳輸。
下面將從通用性、便捷性、靈活性和可移植性的角度來比較序列化相對于上述兩種方法的優(yōu)勢。
2.2.1 通用性如果由程序員自己實現保存對象數據的功能,那么對于每一個類的對象,程序員都要編寫不同的代碼,工作量很大,通用性不高。而序列化提供了一套流程化的方法,對于每一種類,都是大體一致的流程,提高了代碼的通用性。
如果將對象強制轉換為char*或void*類型的數據進行傳輸,那么必須預先得知該對象的大小以提前分配數組的空間。但是,如果該對象中存在可變長的數據結構,就無法準確地得知對象數據的大小了,只能預先估計一下。如果估計小了,可能會造成空間溢出,程序崩潰的后果;如果估計大了,又會造成空間的浪費。但是,如果使用序列化的方法,就能很好地解決可變長數據結構的問題。
2.2.2 便捷性如果由程序員自己實現保存對象數據的功能,那么對于類中不同的數據結構,程序員都要編寫相應的保存代碼,簡單的數據結構還好說,如果是具有多種層次的數據結構,代碼的編寫將越來越復雜,這樣繁瑣且容易出錯。序列化提供了針對簡單數據類型,以及字符串類型、STL容器、指針等種種數據類型的持久化的方法,只需簡單地調用即可,具有很大的便捷性。
2.2.3 靈活性序列化提供了若干種將對象數據持久化的格式,比如以簡單文本格式保存、以XML格式保存、以SOAP格式保存、以二進制格式保存等等。還提供了多種保存持久化之后的對象的方式,比如保存到字符串、保存到文件等等,具有很大的靈活性。
2.2.4 可移植性使用將對象強制轉換為char*類型進行傳輸的方法,需要注意CPU字節(jié)序的問題。如果起始機器與目的機器的CPU字節(jié)序不同,就會造成目的機器讀到的數據無法恢復成原來對象的問題。雖然可以通過將本地字節(jié)序轉化為網絡字節(jié)序進行傳輸,傳到目的機器之后再將網絡字節(jié)序轉為本地字節(jié)序的方法解決這個問題,但是這就增加了程序員考慮問題的復雜性。序列化屏蔽了字節(jié)序的差異,使得被持久化對象的傳輸更具有可移植性。
此外,使用序列化還可以很好地跨平臺。
3 我們的需求3.1 對基于OTT的數據庫結構進行性能測試在使用基于OTT的數據庫結構的程序進行性能測試時,由于讀入的PNR數據是XML格式的文檔,所以,讀入XML文件到內存,將其轉為DOM樹,繼而將DOM樹中的數據轉化為OTT數據庫所需要的對象結構,需要耗費大量的時間。如果把這部分時間算在程序的性能時間中,將導致測試出來的性能存在較大的誤差。因此,最好的方式是,事先將XML格式的PNR數據轉化為程序可用的對象,在程序運行時直接讀入對象即可。這樣可以將解析XML格式的PNR數據的時間與程序運行的時間分離開,從而保證了性能測試的準確性。而將PNR數據轉為程序可用的對象保存下來,就是一個對象序列化的過程;程序讀入保存對象的文件并將其恢復為原來的對象,這就是一個對象反序列化的過程。
3.2 只能使用某種特定類型進行數據傳輸的情況在某些情況下,由于種種限制的約束,使得數據的傳輸只能使用某種特定的類型。比如,使用Tuxedo時,從客戶端向服務端傳數據只可以使用char*類型;比如,在使用共享內存?zhèn)鬟f數據時,只能采用連續(xù)的數組形式。在這些情況下,如果傳輸的數據是一個自定義類的對象的話,就會遇到挑戰(zhàn)。一種做法是直接將該對象強制轉化為所限定的類型,傳到目的地之后再由限定的類型強制轉為原來的類型。這種做法在性能上應該最快,但是使用這種方法必須得明確地知道所傳出數據的長度,所以發(fā)送變長數據并不方便。此外,它還存在跨平臺的兼容性問題。另一種做法就是利用對象序列化的方法,將對象保存為字節(jié)流,向目的地傳輸,在目的地再反序列化為自定義類的對象。這種方法相對比較通用,安全和規(guī)范,但是性能上可能不如前一種方法。
4 使用C++將對象進行序列化的幾種方法使用C++進行對象序列化的方法可以有以下三種:基于Boost庫的方法;基于.Net Framework的方法;以及基于MFC的方法。本章將就三種方法的實現機制、實現步驟,以及注意事項進行說明。
由于我們的開發(fā)環(huán)境在Windows下,部署環(huán)境在Unix下,因此我們的開發(fā)需要使用兩個平臺都可以兼容的技術。經過驗證,基于.Net和基于MFC的方法僅適用于Windows的環(huán)境,而Boost庫在Windows和Unix下都有相應的版本,因此在項目中應優(yōu)先考慮使用Boost庫進行對象的序列化。盡管如此,本文中仍然列出使用.Net和MFC進行序列化的方法,以供參考。三種方法相應的代碼實現的例子將附在文章之后。
4.1 使用Boost庫4.1.1 實現機制這里,我們用術語序列化(serialization)來表示將一組原始的C++數據結構表示為字節(jié)流達到可逆析構的目的。這樣的系統(tǒng)可以用來在另一個程序環(huán)境中重新建立原來的數據結構。因此,它也可以作為對象持久性(object persistence),遠程參數傳遞(remote parameter passing),或者其他特性的實現基礎。在我們的系統(tǒng)中,將使用術語檔案(archive)表示一個具體的字節(jié)流。檔案可以是二進制文件,文本文件,XML文件,或者其他用戶定義的類型。
Boost序列化庫的目標是:
l 代碼的可移植性–只依靠ANSI C++的特性。
l 代碼的經濟性–挖掘各種C++的特性如RTTI、模板、和多繼承等等使用戶容易使用并且代碼短小。
l 類版本的獨立性。–當一個類的定義改變時,老版本的類的檔案仍然可以被導入新版本的類中。
l 指針的深度存儲和恢復。–保存或恢復指針的同時保存或恢復指針指向的數據。
l 正確的處理多個指針指向相同對象時的問題。
l 對STL和其他常用模板類的序列化的直接支持。
l 數據的可移植性–在一個平臺上建立的字節(jié)流在另一個平臺上也應該是正確的。
l 序列化和檔案格式的正交性–可以在不改變類的序列化部分時應用任何格式的文件作為檔案。
l 支持非侵入(Non-intrusive)式的實現。類不需要從某個特定的類派生或者實現特定的成員函數。這對于我們不能或不愿意修改類的定義的情況時是相當必要的。
l 檔案的接口應該足夠簡單使建立新類型的檔案的工作變得輕松。
l 檔案應該支持XML格式。
Boost中,與序列化有關的兩個庫是Archive庫和Serialization庫。
4.1.2 實現步驟首先,為被序列化的類實現一個對應的serialize(Archive & ar, const unsigned int version)方法;
其次,構造boost::archive::text_oarchive類或其他archive輸出類的對象,并將其關聯(lián)到一個輸出流,利用<<運算符將被序列化的對象輸出到某個文檔中;
最后,構造boost::archive::text_iarchive類或其他archive輸入類的對象,并將其關聯(lián)到一個輸入流,讀入數據,利用>>運算符會付出被序列化的對象。
4.1.3 注意事項使用這種方法需要注意的是:
l Boost從1.32版本之后才提供對序列化的支持,所以一定要用版本在1.32之后的;
l Boost中的Serialization庫需要編譯之后得到庫文件才能使用,并加入項目的附加依賴項中才可使用;
l 根據需要包含boost/serialization和boost/archive下的一些頭文件。
4.2 使用.NET4.2.1 實現機制.NET的運行時環(huán)境用來支持用戶定義類型的流化的機制。它在此過程中,先將對象的公共字段和私有字段以及類的名稱(包括類所在的程序集)轉換為字節(jié)流,然后再把字節(jié)流寫入數據流。在隨后對對象進行反序列化時,將創(chuàng)建出與原對象完全相同的副本。
.Net框架對序列化機制具有非常好的支持,它提供了兩個名字空間(namespace):System.Runtime.Serialization和System.Runtime.Serialization.Formatters以完成序列化機制的大部分功能。
序列化機制的實現是依靠格式器(Formatter)而完成的,它是一個從System.Runtime.Serialization.IFormatter繼承下來的類的對象。格式器完成了將程序數據轉化到能被存儲并傳輸的格式的工作,同時也完成了將數據轉化回來的工作。.Net框架為程序員提供了兩種類型的格式器,一種通常是應用于桌面類型的應用程序的,它一個是System.Runtime.Serialization.Formatters.Binary.BinaryFormatter類的對象,而另一種則更主要的應用于.Net Remoting和XML Web服務等領域的,它一個是System.Runtime.Serialization.Formatters.Soap.SoapFormatter類的對象。從它們的名稱來看,不妨將它們分別稱為二進制格式器和XML格式器。它們對應于.Net提供的兩種序列化技術:
二進制序列化保持類型保真度,這對于在應用程序的不同調用之間保留對象的狀態(tài)很有用。例如,通過將對象序列化到剪貼板,可在不同的應用程序之間共享對象,可以將對象序列化到流、磁盤、內存和網絡等等。它的優(yōu)點在于可以將所有的對象成員都保存下來,并且性能優(yōu)于XML序列化。
XML 序列化僅序列化公共屬性和字段,且不保持類型保真度。當您要提供或使用數據而不限制使用該數據的應用程序時,這一點是很有用的。由于 XML 是一個開放式標準,因此,對于通過 Web 共享數據而言,這是一個很好的選擇。SOAP 同樣是一個開放式標準,這使它也成為一個頗具吸引力的選擇。它的優(yōu)點在于互操作性好,可讀性強。
4.2.2 實現步驟使用.Net下的二進制序列化方法進行對象序列化的步驟如下:
首先,要使用 Serializable 屬性對對象的類進行標記;
其次,利用BinaryFormatter的Serialize方法將對象寫入到一個文件流中;
最后,利用BinaryFormatter的DeSerialize方法讀取文件流,恢復對象。
4.2.3 注意事項使用這種方法需要注意的是:
l 需要使用System::Runtime::Serialization::Formatters::Binary命名空間和 System::Runtime::Serialization命名空間;
l 被序列化的類在聲明時必須標識[Serializable]屬性;
l 所涉及的類必須是托管類,即類的聲明前需要有ref關鍵字,用gcnew關鍵字表示在托管堆上分配內存,指針符號用^來標識等。
4.3 使用MFC4.3.1 實現機制對象的序列化歸根結底是將對象的數據寫入載體,再重新讀取為對象的過程。MFC中對數據的讀寫創(chuàng)造了十分好的支持,這使得我們可以十分方便的利用MFC的數據讀寫類來實現對象序列化的需要。
MFC 為數據讀寫設計了三個基本的類——CFile(CFile類)、CStdioFile(標準I/O文件類)、CArchive(CArchive類)。其中標準CStdioFile類提供相當于C的流式文件的功能,可以用文本或者二進制方式打開,可以被緩沖。CFile類提供了非緩沖的二進制輸入輸出文件,它既可以與CArchive類結合實現VisualC++設計中常用的文件序列化,也可以由設計者自己訂制存儲方案,實現數據的讀寫操作(此方法的兼容問題需要解決,保密性強)。CArchive類是VisualC++程序設計中最常用的文件處理的方法,CArchive類不僅可以實現簡單數據結構的讀寫操作,還可以通過對CObiect類的派生實現對復雜數據結構的讀寫操作,因此,利用CArchive類,可以輕松實現任意數據結構的序列化。
4.3.2 實現步驟實現序列化的的類需要滿足一系列條件:
1. 該類需要從CObject類派生(可以是間接派生);
2. 在類中中進行DECLARE_SERIAL宏定義;
3. 類存在有缺省的構造函數;
4. 類中實現了Serialize(CArchive&)函數,并且在其中調用基類的序列化函數;
5. 使用IMPLEMENT_SERIAL宏指明類名及版本號。
滿足了這些條件之后,就可以進行序列化與反序列化了。
序列化時,首先,實例化一個CArchive類的對象,將其與輸出文件相關聯(lián);其次,利用CArchive類的<<運算符重載將需要序列化的對象保存在文件中。
反序列化時,將CArchive類的對象與保存對象的文件相關聯(lián);然后新建一個需要反序列化的對象,利用CArchive類的>>運算符重載將文件里的內容恢復到需要反序列化的對象中。
4.3.3 注意事項使用這種方法需要注意的是:
l 需要包含afx.h頭文件;
l 它不支持string類型的序列化,但是支持CString類型的序列化;
l 需要將項目屬性中的MFC屬性配置為“在共享DLL中使用MFC”或“在靜態(tài)庫中使用MFC”,否則編譯時會報錯。
5 使用Boost庫進行對象序列化的關鍵技術5.1 基礎1、基本類型的存檔和讀取
對基本類型. 直接使用以下語句就可以完成存檔或讀取:
l 用 ar << data或ar & data; 寫入存檔
l 用 ar >> data或ar & data; 從存檔取出
2、自定義類型的存檔和讀取
對自定義類型. 則會調用 serialize() 函數,serialize 函數用來“存儲/裝載”其數據成員。這個處理采用遞歸的方式,直到所有包含在類中的數據“被存儲/被裝載”。
l 侵入式: t.serialize(ar, version)
l 非侵入式: serialize(ar, t, version)
3、所需包含的頭文件:
l 以簡單文本格式實現存檔:text_oarchive和text_iarchive
l 寬字符的文本格式存檔 :text_woarchive text_wiarchive
l xml存檔:xml_oarchive xml_iarchive
l 使用寬字符的xml文檔(用于utf-8)輸出:xml_woarchive xml_wiarchive
l 二進制的存檔 (注意 二進制存檔是不可移植的):binary_oarchive binary_iarchive
5.2 侵入式和非侵入式對于被序列化的類,有兩種實現其對應的serialize方法的方式,一種是侵入式,即把serialize方法作為被序列化類的一個成員方法來實現;另一種是非侵入式,即將serialize方法放在另一個名字空間下,作為被序列化類的一個友元方法來實現。在不可修改被序列化的類的代碼的情況下,應該采用非侵入式的方式。
侵入式的例子:
class MyPoint
{
int mX;
int mY;
private:
friend class boost::serialization::access; //侵入式版本的要加這個.
//存入和讀取都使用下邊的 serialize() 函數.
//其中的 Archive 是一個輸入或輸出的文檔. 當輸入的時候 & 為 >> . 當輸出的時候 & 為 <<.
template<class Archive>
void serialize(Archive& ar, const unsigned int version)
{
ar & mX; //序列化數據成員
ar & mY;
}
public:
MyPoint() {}
MyPoint(int x, int y) : mX(x), mY(y) {}
};
非侵入式的例子:
class MyPoint
{
private:
// 注意關鍵字”friend”和多了一個類引用作參數
template<class Archive>
friend void serialize(Archive& ar, MyPoint&, unsigned int const);
int mX;
int mY;
public:
MyPoint() {}
MyPoint(int x, int y) : mX(x), mY(y) {}
};
//非侵入式
namespace boost { //實現放在這個名字空間下
namespace serialization {
template<class Archive>
void serialize(Archive & ar, MyPoint& p, const usigned int version)
{
ar & p.mX & p.mY; //可以連著 &
}
}
} //namespace 結束
對派生類進行序列化需要有一個前提,即它的父類必須也實現了serialize方法,也可以序列化。如果在派生類的父類沒有實現serialize方法,僅對派生類進行序列化,將不能保存派生類從父類繼承下來的數據信息,而僅能保存屬于派生類自身的數據信息。
對派生類進行序列化的步驟是:
1、包含boost/serialization/base_object.hpp頭文件;
2、在serialize模版方法中,使用ar & boost::serialization::base_object<父類>(*this)這樣的語法來保存父類的數據,不能直接調用父類的serialize函數。
一個例子如下:
#include <boost/serialization/base_object.hpp> //一定要包含此頭文件
class B:A
{
friend class boost::serialization::access;
char c;
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & boost::serialization::base_object<A>(*this);//注意這里
ar & c;
}
public:
…
};
5.4 數組的序列化對于數組進行序列化,就是保存數組中的每一個數據成員,因此相當于對數組中的每一個數據成員做序列化??梢杂靡韵滦问剑?/span>
for(int i = 0; i < sizeof(array); i++)
{
ar & array[i];
}
但是事實上,Boost的序列化庫能檢測出被序列化的對象是一個數組,將產生上述等價的代碼,例子如下:
class bus_route
{
friend class boost::serialization::access;
bus_stop stops[10];
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & stops;
}
public:
bus_route(){}
};
5.5 指針的序列化序列化整個對象是要求在另一個地方和時間重新構造原始數據結構。在使用指針的情況下,為了達到重新構造原始數據結構的目的,僅僅存儲指針的值是不夠的,指針指向的對象也必須被存儲。當成員最后被裝載,一個新的對象被創(chuàng)建,指向新的對象的新的指針被裝載到類的成員中。
所有這一切由Boost的序列化庫自動完成,程序員只需直接序列化指針即可。(說是這么說,使用要慎重,因為例子并沒有調通。)一個例子如下:
class bus_route{ friend class boost::serialization::access; bus_stop * stops[10]; template<class Archive> void serialize(Archive & ar, const unsigned int version) { int i; for(i = 0; i < 10; ++i) ar & stops[i]; }public: bus_route(){}};5.6 對STL容器的序列化對于STL容器,比如vector或list,需要在頭文件中包含<boost/serialization/vector.hpp>或<boost/serialization/list.hpp>等,然后就可以直接進行序列化了。一個例子如下:
#include <boost/serialization/list.hpp>class bus_route{ friend class boost::serialization::access; std::list<bus_stop *> stops; template<class Archive> void serialize(Archive & ar, const unsigned int version) { ar & stops; }public: bus_route(){}};5.7 被序列化的類的成員是其他類的對象的情況如果被序列化的類有成員是其他類的對象,那么,只有在其對象成員的類也實現了serialize方法并能被序列化的情況下,該類才能被序列化。
比如前幾個例子中,類bus_route中有成員是bus_stop類的對象。那么只有bus_stop類實現了serialize方法后,bus_route類才能被序列化。
5.8 輸出Boost的序列化庫可以以三種格式進行輸出,分別是:簡單文本格式、XML格式,以及二進制格式。其中每種格式又可以輸出到c++的ostream流中,比如,ostringstream(字符串輸出流),ofstream(文件輸出流)。下例是一個以簡單文本格式輸出到字符串流中的例子。
//序列化,輸出到字符串
std::ostringstream ossOut(ostringstream::out); //把對象寫到字符串輸出流中
boost::archive::text_oarchive oa(ossOut);
TestClass objTestClass;
oa << objTestClass;
string strTrans = ossOut.str();
……
//反序列化,從字符串輸入
istringstream ossIn(strTrans); //從字符串輸入流中讀入數據
boost::archive::text_iarchive ia(ossIn);
TestClass newObjTestClass;
ia >> newObjTestClass;
6 結論1、 在基于OTT結構的數據庫結構的性能測試中,針對數據庫中的每一個表,定義了一個相應的類,我們的目標是將這些類的對象進行序列化。但是,在試圖序列化的過程中遇到一個問題,即:所有的OTT表的類都繼承自一個由Oracle庫文件定義的類oracle::occi::PObject。而派生類的序列化要求其父類也必須實現序列化接口,否則就會派生類繼承的父類的成員就會在序列化時丟失(見5.3節(jié))。這就要求修改庫文件,是PObject也實現序列化接口。可是貿然地修改庫文件可能會導致連鎖反應,引起其他引用庫文件的程序出錯,此外,還有知識產權的問題。所以,使用Boost序列化庫來對OTT表的類進行序列化的路可能走不通。應考慮其他方法。
2、 在使用共享內存?zhèn)鬟f對象數據時,可以將對象數據以簡單文本格式進行序列化,再用ostringstream流輸出到字符串中,進行傳遞,完全可行。
7 附錄7.1 資源1、 Boost中Serialization庫的文檔:http://www.boost.org/doc/libs/1_37_0/libs/serialization/doc/index.html;
2、 Boost序列化庫教程:http://dozb.bokee.com/1692310.html#derivedclasses;
3、 Learning boost 1 Serialization:http://blog.csdn.net/freerock/archive/2007/08/17/1747928.aspx
4、 C++中使用boost::serialization庫――應用篇:http://www.cnblogs.com/mslk/archive/2005/11/25/284491.html;
5、 C++ Reference: IOstream Library: ostream:http://www.cplusplus.com/reference/iostream/ostream/;
7.2 程序示例對照表l CplusSerializeBoost:使用Boost的序列化庫進行序列化;
l CplusSerializeDotNet:使用.Net進行序列化;
l CplusSerializeMFC:使用MFC進行序列化。