1. 程式人生 > >Protobuf詳解(.Java檔案)

Protobuf詳解(.Java檔案)

我們在開發一些RPC呼叫的程式時,通常會涉及到物件的序列化/反序列化的問題,比如一個“Person”物件從Client端通過TCP方式傳送到Server端;因為TCP協議(UDP等這種低階協議)只能傳送位元組流,所以需要應用層將Java物件序列化成位元組流,資料接收端再反序列化成Java物件即可。“序列化”一定會涉及到編碼(encoding,format),目前我們可選擇的編碼方式:

    1)使用JSON,將java物件轉換成JSON結構化字串。在web應用、移動開發方面等,基於Http協議下,這是常用的,因為JSON的可讀性較強。效能稍差。

    2)基於XML,和JSON一樣,資料在序列化成位元組流之前,都轉換成字串。可讀性強,效能差,異構系統、open api型別的應用中常用。

    3)使用JAVA內建的編碼和序列化機制,可移植性強,效能稍差。無法跨平臺(語言)。

    4)其他開源的序列化/反序列化框架,比如Apache Avro,Apache Thrift,這兩個框架和Protobuf相比,效能非常接近,而且設計原理如出一轍;其中Avro在大資料儲存(RPC資料交換,本地儲存)時比較常用;Thrift的亮點在於內建了RPC機制,所以在開發一些RPC互動式應用時,Client和Server端的開發與部署都非常簡單。

    評價一個序列化框架的優缺點,大概有2個方面:1)結果資料大小,原則上說,序列化後的資料尺寸越小,傳輸效率越高。 2)結構複雜度,這會影響序列化/反序列化的效率,結構越複雜,越耗時。

    Protobuf是一個高效能、易擴充套件的序列化框架,它的效能測試有關資料可以參看官方文件。通常在TCP Socket通訊(RPC呼叫)相關的應用中使用;它本身非常簡單,易於開發,而且結合Netty框架可以非常便捷的實現一個RPC應用程式,同時Netty也為Protobuf解決了有關Socket通訊中“半包、粘包”等問題(反序列化時,位元組成幀)。

1、安裝Protobuf

    從“https://developers.google.com/protocol-buffers/docs/downloads”下載安裝包,windows下的使用不再贅言;在linux或者mac下,下載tar.gz的壓縮包,解壓後執行:

Java程式碼  收藏程式碼
  1. $ ./configure  
  2. $ make  
  3. $ make check  
  4. $ make install  

    此後,可以通過“protoc --version”檢視是否安裝成功了,安裝過程不需要配置環境變數。安裝主要是為了能夠使用命令編譯proto檔案,實際部署環境並不需要。

2、樣例

    Protobuf需要一個schema宣告檔案,字尾為“.proto”的文字檔案,內容樣例如下:

Java程式碼  收藏程式碼
  1. option java_package = "com.test.protobuf";  
  2. option java_outer_classname="PersonProtos";  
  3. message Person {  
  4.   required string name = 1;  
  5.   required int32 id = 2;  
  6.   optional string email = 3;  
  7.   enum PhoneType {  
  8.     MOBILE = 0;  
  9.     HOME = 1;  
  10.     WORK = 2;  
  11.   }  
  12.   message PhoneNumber {  
  13.     required string number = 1;  
  14.     optional PhoneType type = 2 [default = HOME];  
  15.   }  
  16.   repeated PhoneNumber phone = 4;  
  17. }  

    如果你曾經使用過thrift、avro,你會發現它們都需要一個類似的schema檔案,只是結構規則不同罷了。特別備註:protbuf和thrift的宣告檔案相似度極高。

    “message”表示,宣告一個“類”,即java中的class。message中可以內嵌message,就像java的內部類一樣。一個message有多個filed,“required string name = 1”則表示:name欄位在序列化、反序列化時為第一個欄位,string型別,“required”表示這個欄位的值是必選;可以看出每個filed都至少有著三個部分組成,其中filed的“位置index”全域性唯一。“optional”表示這個filed是可選的(允許為null)。“repeated”表示這個filed是一個集合(list)。也可以通過[default = ]為一個“optional”的filed指定預設值。

    我們可以在一個.proto檔案中宣告多個“message”,不過大部分情況下我們把互相繼承或者依賴的類寫入一個.proto檔案,將那些沒有關聯關係的類分別寫入不同的檔案,這樣便於管理。

    我們可以在.proto檔案的頭部宣告一些額外的資訊,比如“java_package”表示當“generate code”時將生成的java程式碼放入指定的package中。“java_outer_classname”表示生成的java類的名稱。

    然後執行如下命令,生成JAVA程式碼:

Java程式碼  收藏程式碼
  1. protoc --java_out=./ Persion.proto  

    通過“--java_out”指定生成JAVA程式碼儲存的目錄,後面緊跟“.proto”檔案的路徑。此後我們看到生成 了Package和一個PersonProto.java檔案,我們只需要把此java檔案複製到專案中即可。

3、JAVA例項

    1)pom.xml

Java程式碼  收藏程式碼
  1. <dependency>  
  2.      <groupId>com.google.protobuf</groupId>  
  3.      <artifactId>protobuf-java</artifactId>  
  4.      <version>2.6.1</version>  
  5. </dependency>  

    2)測試:

Java程式碼  收藏程式碼
  1. PersonProtos.Person.Builder personBuilder = PersonProtos.Person.newBuilder();  
  2. personBuilder.setEmail("[email protected]");  
  3. personBuilder.setId(1000);  
  4. PersonProtos.Person.PhoneNumber.Builder phone = PersonProtos.Person.PhoneNumber.newBuilder();  
  5. phone.setNumber("18610000000");  
  6. personBuilder.setName("張三");  
  7. personBuilder.addPhones(phone);  
  8. PersonProtos.Person person = personBuilder.build();  

    獲得到person例項後,我們可以通過如下方式,將person物件序列化、反序列化。

Java程式碼  收藏程式碼
  1. //第一種方式  
  2. //序列化  
  3. byte[] data = person.toByteArray();//獲取位元組陣列,適用於SOCKET或者儲存在磁碟。  
  4. //反序列化  
  5. PersonProtos.Person result = PersonProtos.Person.parseFrom(data);  
  6. System.out.println(result.getEmail());  

   這種方式,適用於很多場景,Protobuf會根據自己的encoding方式,將JAVA物件序列化成位元組陣列。同時Protobuf也可以從位元組陣列中重新decoding,得到Java新的例項。

Java程式碼  收藏程式碼
  1. //第二種序列化:粘包,將一個或者多個protobuf物件位元組寫入stream。  
  2. ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();  
  3. //生成一個由:[位元組長度][位元組資料]組成的package。特別適合RPC場景  
  4. person.writeDelimitedTo(byteArrayOutputStream);  
  5. //反序列化,從steam中讀取一個或者多個protobuf位元組物件  
  6. ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());  
  7. result = PersonProtos.Person.parseDelimitedFrom(byteArrayInputStream);  
  8. System.out.println(result.getEmail());  

    第二種方式,是RPC呼叫中、Socket傳輸時適用,在序列化的位元組陣列之前,新增一個varint32的數字表示位元組陣列的長度;那麼在反序列化時,可以通過先讀取varint,然後再依次讀取此長度的位元組;這種方式有效的解決了socket傳輸時如何“拆包”“封包”的問題。在Netty中,適用了同樣的技巧。

Java程式碼  收藏程式碼
  1. //第三種序列化,寫入檔案或者Socket  
  2. FileOutputStream fileOutputStream = new FileOutputStream(new File("/test.dt"));  
  3. person.writeTo(fileOutputStream);  
  4. fileOutputStream.close();  
  5. FileInputStream fileInputStream = new FileInputStream(new File("/test.dt"));  
  6. result = PersonProtos.Person.parseFrom(fileInputStream);  
  7. System.out.println(result);  

    第三種方式,比較少用。但是比較通用,意思為將序列化的位元組陣列寫入到OutputStream中,具體的拆包工作,交給了高層框架。

4、protobuf入門介紹

    以上述Person.proto檔案為例:

Java程式碼  收藏程式碼
  1. message Person {  
  2.   required string name = 1;  
  3.   required int32 id = 2;  
  4.   optional string email = 3;  
  5. }  

    聲明瞭三個filed,每個filed都“規則”、“型別”、“欄位名稱”和一個“唯一的數字tag”。

    1)其中“規則”可以為如下幾個值:

    “required”:表示此欄位值必填,一個結構良好的message至少有一個flied為“required”。

    “optional”:表示此欄位值為可選的。對於此型別的欄位,可以通過default來指定預設值,這是一個良好的設計習慣。

Java程式碼  收藏程式碼
  1. optional int32 page = 3 [default = 10];  

    如果沒有指定預設值,在encoding時protobuf將會用一個特殊的預設值來替代。對於string,預設值為空,bool型別預設為false,數字型別預設位0,對於enum則預設值為列舉列表的第一個值。

    “repeated”:表示這個欄位的值可以允許被重複多次,如果轉換成JAVA程式碼,此filed資料結構為list,有序的。可以在“repeated”型別的filed後使用“packed”--壓縮,提高資料傳輸的效率。

Java程式碼  收藏程式碼
  1. repeated int32 numbers = 4 [packed=true];  

    特別需要注意:當你指定一個filed位required時,需要慎重考慮這個filed是否永遠都是“必須的”。將一個required調整為optional,需要同時重新部署資料通訊的Client和Server端,否則將會對解析帶來問題。

    2)可以在一個.proto檔案中,同時宣告多個message,這樣是允許的。

    3)為message或者filed添加註釋,風格和JAVA一樣:

Java程式碼  收藏程式碼
  1. optional int32 page = 3;// Which page number do we want?  

    4)資料型別與JAVA對應關係:

protobuf java
double double
float float
int32 int
int64 long
bool boolean
string String
bytes ByteString

    其中“ByteString”是Protobuf自定義的JAVA API。

     5)列舉:和JAVA中Enum API一致,如果開發者希望某個filed的值只能在一些限定的列表中,可以將次filed宣告為enum型別。Protobuf中,enum型別的每個值是一個int32的數字,不像JAVA中那樣enum可以定義的非常複雜。如果enum中有些值是相同的,可以將“allow_alias”設定為true。

Java程式碼  收藏程式碼
  1. message Person {  
  2.   required Type type = 1;  
  3.   enum Type {  
  4.   option allow_alias = true;  
  5.     TEACHER = 0;  
  6.     STUDENT = 1;  
  7.     OTHER = 1;//the same as STUDENT  
  8.   }  
  9. }  

     6)import:如果當前.proto檔案中引用了其他proto檔案的message型別,那麼可以在此檔案的開頭宣告import。

Java程式碼  收藏程式碼
  1. import "other_protos.proto";  

    不過這會引入一個小小的麻煩,如果你的“other_protos.proto”檔案變更了目錄,需要連帶修改其他檔案。

    7)嵌入message:類似於java的內部類,即在message中,嵌入其他message。如Person.proto例子中的PhoneNumber。

    8)更新message型別:如果一個現有的message型別無法滿足當前的需要,比如你需要新增一個filed,但是仍然希望使用生成的舊程式碼來解析。

        (1)不要修改現有fileds的數字tag,即欄位的index數字。

        (2)新增欄位必須為optional或者repeated型別,同時還要為它們設定“default”值,這意味著“old”程式碼序列化的messages能夠被“new”程式碼解析。“new”程式碼生成的資料也能被“old”程式碼解析,對於“old”程式碼而言,那些沒有被宣告的filed將會在解析式忽略。

        (3)非“required”filed可以被刪除,但是它的“數字tag”不能被其他欄位重用。

        (4)int32、uint32、int64、uint64、bool,是互相相容的,它們可以從一個型別修改成另外一個,而不會對程式帶來錯誤。參見原始碼WireFormat.FiledType

        (5)sint32和sint64是相容的,但和其他數字型別是不相容的。

        (6)string和bytes是相容的,只要為UTF-8編碼的。注意protobuf中string預設是UTF-8編碼的。

        (7)optional與repeated是相容的。如果輸入的資料格式是repeated,但是client希望接受的資料是optional,對於原生型別,那麼client將會使用repeated的最後一個值,對於message型別,client將會merge這些輸入的資料。

        (8)修改“default”值通常不會有任何問題,只要保證這個預設值不會被真正的使用。

    9)Map結構:

Java程式碼  收藏程式碼
  1. map<key_type, value_type> map = 3;  

    其中key_type可以為任何“整形”或者string型別,value_type可以為任意型別,只要JAVA API能夠支援。map型別不能被“repeated”、“optional”或者“required”修飾,傳輸過程中無法確保map中資料的順序,

對於文字格式,map是按照key排序。

    10)如下為一些有用的選項:

        (1)java_package:在.proto檔案的頂部設定,指定生成JAVA檔案時類所在的package。

Java程式碼  收藏程式碼
  1. option java_package = "com.example.foo";  

        (2)java_outer_classname:在.proto檔案的頂部設定,指定生成JAVA檔案時類的名字。一個.proto檔案只會生成一個JAVA類。

Java程式碼  收藏程式碼
  1. option java_outer_classname = "FooProtos";  

        (3)packed:對於repeated型別有效,指定輸入的資料是否“壓縮”。

5、protobuf序列化原理:

    其實protobuf的序列化原理並不是什麼高超的“絕技”:如果你曾經瞭解過thrift、avro,或者從事過socket通訊,那麼你對protobuf的序列化方式並不感到驚奇;如下為protobuf的序列化format:

Java程式碼  收藏程式碼
  1. [serializedSize]{[int32(tag,type)][value]...}  

    對於一個message,序列化時首先就算這個message所有filed序列化需要佔用的位元組長度,計算這個長度是非常簡單的,因為protobuf中每種型別的filed所佔用的位元組數是已知的(bytes、string除外),只需要累加即可。這個長度就是serializedSize,32為integer,在protobuf的某些序列化方式中可能使用varint32(一個壓縮的、根據數字區間,使用不同位元組長度的int);此後是filed列表輸出,每個filed輸出包含int32(tag,type)和value的位元組陣列,從上文我們知道每個filed都有一個唯一的數字tag表示它的index位置,type為欄位的型別,tag和type分別佔用一個int的高位、低位位元組如果filed為string、bytes型別,還會在value之前額外的補充新增一個varint32型別的數字,表示string、bytes的位元組長度。

    那麼在反序列化的時候,首先讀取一個32為的int表示serializedSize,然後讀取serializedSize個位元組儲存在一個bytebuffer中,即讀取一個完整的package。然後讀取一個int32數字,從這個數字中解析出tag和type,如果type為string、bytes,然後補充讀取一個varint32就知道了string的位元組長度了,此後根據type或者位元組長度,讀取後續的位元組陣列並轉換成java type。重複上述操作,直到整個package解析完畢。

    protobuf的這種序列化format,極大的介紹了輸入、輸出的資料大小,而且複雜度非常低,從而效能較高。

6、protobuf與Netty程式設計:

    1)Netty Server端樣例

Java程式碼  收藏程式碼
  1. public class ProtobufNettyServerTestMain {  
  2.     public static void main(String[] args) {  
  3.         //bossGroup : NIO selector threadPool  
  4.         EventLoopGroup bossGroup = new NioEventLoopGroup();  
  5.         //workerGroup : socket data read-write worker threadPool  
  6.         EventLoopGroup workerGroup = new NioEventLoopGroup();  
  7.         try {  
  8.             ServerBootstrap bootstrap = new ServerBootstrap();  
  9.             bootstrap.group(bossGroup,workerGroup)  
  10.                     .channel(NioServerSocketChannel.class)  
  11.                     .childHandler(new ChannelInitializer<SocketChannel>() {  
  12.                         @Override  
  13.                         protected void initChannel(SocketChannel ch) throws Exception {  
  14.                             ch.pipeline().addLast(new ProtobufVarint32FrameDecoder())  
  15.                                     .addLast(new ProtobufDecoder(PersonProtos.Person.getDefaultInstance()))  
  16.                                     .addLast(new ProtobufVarint32LengthFieldPrepender())  
  17.                                     .addLast(new ProtobufEncoder())  
  18.                                     .addLast(new ProtobufServerHandler());//自定義handler  
  19.                         }  
  20.                     }).childOption(ChannelOption.TCP_NODELAY,true);  
  21.             System.out.println("begin");  
  22.             //bind到本地的18080埠  
  23.             ChannelFuture future = bootstrap.bind(18080).sync();  
  24.             //阻塞,直到channel.close  
  25.             future.channel().closeFuture().sync();  
  26.             System.out.println("end");  
  27.         } catch (Exception e) {  
  28.             e.printStackTrace();  
  29.         } finally {  
  30.             //輔助執行緒優雅退出  
  31.             workerGroup.shutdownGracefully();  
  32.             bossGroup.shutdownGracefully();  
  33.         }  
  34.     }  
  35. }  

    備註:channel內部維護一個pipeline,類似一個filter連結串列一樣,所有的socket讀寫都會經過,對於write操作(outbound)會從pipeline列表的last-->first方向依次呼叫Encoder處理器;對於read操作(inbound)會從first-->last依次呼叫Decoder處理器。此外Encoder處理對於read操作不起效,Decoder處理器對write操作不起效,原理 稍後在Netty相關章節介紹。

    ProtobufEncoder:非常簡單,內部直接使用了message.toByteArray()將位元組資料放入bytebuf中輸出(out中,交由下一個encoder處理)。

    ProtobufVarint32LengthFieldPrepender:因為ProtobufEncoder只是將message的各個filed按照規則輸出了,並沒有serializedSize,所以socket無法判定package(封包)。這個Encoder的作用就是在ProtobufEncoder生成的位元組陣列前,prepender一個varint32數字,表示serializedSize。

    ProtobufVarint32FrameDecoder:這個decoder和Prepender做的工作正好對應,作用就是“成幀”,根據seriaziedSize讀取足額的位元組陣列--一個完整的package。

    ProtobufDecoder:和ProtobufEncoder對應,這個Decoder需要指定一個預設的instance,decoder將會解析byteArray,並根據format規則為此instance中的各個filed賦值。

    2)ProtobufServerHandler.java

    傳送Protobuf資料和接收client傳送的資料。一個自定義的處理器,通常我們的業務會在這裡處理。

Java程式碼  收藏程式碼
  1. public class ProtobufServerHandler extends ChannelInboundHandlerAdapter {  
  2.     @Override  
  3.     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
  4.         PersonProtos.Person person = (PersonProtos.Person)msg;  
  5.        //經過pipeline的各個decoder,到此Person型別已經可以斷定  
  6.         System.out.println(person.getEmail());  
  7. 相關推薦

    Protobuf(.Java檔案)

    我們在開發一些RPC呼叫的程式時,通常會涉及到物件的序列化/反序列化的問題,比如一個“Person”物件從Client端通過TCP方式傳送到Server端;因為TCP協議(UDP等這種低階協議)只能傳送位元組流,所以需要應用層將Java物件序列化成位元組流,資料接收端再反

    java定時任務

    導致 println 正常 延遲執行 first 指定 線程終止 ont 打印 在我們編程過程中如果需要執行一些簡單的定時任務,無須做復雜的控制,我們可以考慮使用JDK中的Timer定時任務來實現。下面LZ就其原理、實例以及Timer缺陷三個方面來解析java Timer定

    Java類的生命周期

    字段 view 數據類型 分配內存 lar ati final 並不是 編譯 引言 最近有位細心的朋友在閱讀筆者的文章時,對Java類的生命周期問題有一些疑惑,筆者打開百度搜了一下相關的問題,看到網上的資料很少有把這個問題講明白的,主要是因為目前國內Java

    caffe protobuf

    方法 type source protobuf 輸出 filter out cat hdf 1.數據層 layer {   name: "cifar"   type: "Data"   top: "data" #一般用bottom表示輸入,top表示輸出,多個t

    windows命令行中java和javac、javap使用(java編譯命令)

    路徑 point 目錄 pan static article 字節碼 區別 string 如題,首先我們在桌面,開始->運行->鍵入cmd 回車,進入windows命令行。進入如圖所示的畫面: 可知,當前默認目錄為C盤Users文件夾下的Administr

    java中的數據結構

    span 通過 組成 ret hashcode p s 函數 arr 均衡   線性表,鏈表,哈希表是常用的數據結構,在進行Java開發時,JDK已經為我們提供了一系列相應的類來實現基本的數據結構。這些類均在java.util包中。本文試圖通過簡單的描述,向讀者闡述各個類的

    Java的自動裝箱與拆箱(Autoboxing and unboxing)

    初始 BE 運算 null 異常 內存 判斷 運行 double 一、什麽是自動裝箱拆箱 很簡單,下面兩句代碼就可以看到裝箱和拆箱過程 1 //自動裝箱 2 Integer total = 99; 3 4 //自定拆箱 5 int totalprim = total;

    Java的Spring框架中的註解的用法

    控制 extends 進行 -i 場景 1.7 遞歸 ins 規範 轉載:http://www.jb51.net/article/75460.htm 1. 使用Spring註解來註入屬性 1.1. 使用註解以前我們是怎樣註入屬性的 類的實現: class UserMa

    幹貨——Java中的關鍵字

    java虛擬機 color bsp cfi 為什麽 max main spa 不能 在平時編碼中,我們可能只註意了這些static,final,volatile等關鍵字的使用,忽略了他們的細節,更深層次的意義。 本文總結了Java中所有常見的關鍵字以及一些例子。

    Java 基礎之 Java 反射機制

    一行代碼 strac classname for 內部 系統資源 用戶 管理 ann 一、什麽是 Java 的反射機制? ??反射(Reflection)是Java的高級特性之一,是框架實現的基礎,定義:JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有

    java中的byte類型

    font 資料 結果 可能 詳解 小程序 工作 定義 值範圍 Java也提供了一個byte數據類型,並且是基本類型。java byte是做為最小的數字來處理的,因此它的值域被定義為-128~127,也就是signed byte。下面這篇文章主要給大家介紹了關於java中by

    數據結構 - 單源最短路徑之迪傑斯特拉(Dijkstra)算法(Java)

    previous 代碼 map class matrix () count 就是 可能   給出一個圖,求某個端點(goal)到其余端點或者某個端點的最短路徑,最容易想到的求法是利用DFS,假設求起點到某個端點走過的平均路徑為n條,每個端點的平均鄰接端點為m,那求出這個最短

    Java 中的三種代理模式

    繼承 jvm 保存 3.2 指令集 throwable eth args 代理類 代理模式 代理(Proxy)是一種設計模式,提供了對目標對象另外的訪問方式;即通過代理對象訪問目標對象.這樣做的好處是:可以在目標對象實現的基礎上,增強額外的功能操作,即擴展目標

    Java中的時區類TimeZone的用法

    void system類 深入 pri comment 相對 系統 就會 lean 一、TimeZone 簡介 TimeZone 表示時區偏移量,也可以計算夏令時。 在操作 Date, Calendar等表示日期/時間的對象時,經常會用到TimeZone;因為不同的時區,

    面向介面程式設計-Java

     相信看到這篇文字的人已經不需要了解什麼是介面了,我就不再過多的做介紹了,直接步入正題,介面測試如何編寫。那麼在這一篇裡,我們用一個例子,讓各位對這個重要的程式設計思想有個直觀的印象。為充分考慮到初學者,所以這個例子非常簡單,望各位高手見諒。   為了擺脫新手的概念,我這裡也儘量不用main

    Java中的Object.getClass()方法

    詳解Java中的Object.getClass()方法   詳解Object.getClass()方法,這個方法的返回值是Class型別,Class c = obj.getClass(); 通過物件c,我們可以獲取該物件的所有成員方法,每個成員方法都是一個Method物件;我們也可以獲取該物件的

    smali檔案

    詳解smali檔案 上面我們介紹了Dalvik的相關指令,下面我們則來認識一下smali檔案.儘管我們使用java來寫Android應用,但是Dalvik並不直接載入.class檔案,而是通過dx工具將.class檔案優化成.dex檔案,然後交由Dalvik載入.這樣說來,我們無法通

    舉例java例項變數,靜態變數,區域性變數

    public class Variable { public int m,n;//對子類可見的例項變數 private double k;//只對本類可見的例項變數,一般情況下,設為私有,通過使用訪問修飾符來被子類使用。 public static String P;//靜態變數(

    RoundingMode 幾個引數 java.math.RoundingMode 幾個引數

    第一版 java.math.RoundingMode 幾個引數詳解 java.math.RoundingMode裡面有幾個引數搞得我有點暈,現以個人理解對其一一進行總結: 為了能更好理解,我們可以畫一個XY軸 RoundingMode

    Java解析XML的四種方法(轉載)

    出處:http://developer.51cto.com/art/200903/117512.htm XML現在已經成為一種通用的資料交換格式,它的平臺無關性,語言無關性,系統無關性,給資料整合與互動帶來了極大的方便。對於XML本身的語法知識與技術細節,需要閱讀相關的技術文獻,這裡