SOAP(Simple Object Access Protocal) 技術有助於實現大量異構程序和平台之間的互操作性,從而使存在的應用能夠被廣泛的用戶所訪問。SOAP是把成熟的基於HTTP的WEB技術與XML的靈活性和可擴展性組合在了一起。
這篇文章帶你全面回顧對象遠程進程調用(ORPC)技術的歷程,以幫助你理解SOAP技術的基礎,以及它克服存在技術(如CORBA和DCOM)的許多缺陷的方法。隨後講述詳細的SOAP編碼規則,並把焦點放在SOAP是怎樣映射到存在的ORPC概念上的。
引言:
當我在1984年開始把計算作為我的職業的時候,大多數程序員並不關心網絡協議。但是在九十年代網絡變得無所不在,現在如果有誰使用計算機卻不使用某種形式網絡連接是很難以想象的。今天,一般的程序員對建立可擴展的分布式應用表現出更大的興趣,而不再只是關注於用MFC實現個性化的可浮動半透明非矩形的Coolbars了。
程序員通常喜歡用編程模型來思考問題,而很少考慮網絡協議。盡管這樣做通常是很好的,但在這篇文章中我將討論的SOAP是一個沒有明顯的編程模型的網絡協議。這並不意味著SOAP的體系結構從根本上會改變你編程的方式。相反,SOAP的一個主要目標是使存在的應用能被更廣泛的用戶所使用。為了實現這個目的,沒有任何SOAP API或SOAP 對象請求代理(SOAP ORB),SOAP是假設你將使用盡可能多的存在的技術。幾個主要的CORBA廠商已經承諾在他們的ORB產品中支持SOAP協議。微軟也承諾在將來的COM版本中支持SOAP。
DevelopMentor已經開發了參考實現,它使得在任何平台上的任何Java或Perl程序員都可以使用SOAP。
在SOAP後面的指導理念是“它是第一個沒有發明任何新技術的技術”。SOAP采用了已經廣泛使用的兩個協議:HTTP和XML。HTTP用於實現SOAP的RPC風格的傳輸,而XML是它的編碼模式。采用幾行代碼和一個XML解析器,HTTP服務器(如MS的IIS或Apache)立刻成為了SOAP的ORBs。 因為目前超過一半的Web服務器采用IIS或Apache, SOAP將會從這兩個產品的廣泛而可靠的使用中獲取利益。這並不意味著所有的SOAP請求必須通過Web服務器來路由,傳統的Web 服務器只是分派SOAP請求的一種方式。因此Web服務如IIS或Apache對建立SOAP使能的應用是充分的,但決不是必要的。
正如這篇文章將要描述的,SOAP簡單地用XML來編碼HTTP的傳輸內容。SOAP最常用的應用是作為一個RPC協議。為了理解SOAP怎樣工作,有必要簡要回顧一下RPC協議的歷史。
RPCs的歷史
建立分布式應用的兩個主要通信模型是消息傳送(經常與隊列組合在一起)和請求/響應。消息傳遞系統允許通信任何一方在任何時間發送消息。請求/響應協議把通信模式限制在請求/響應的雙方。基於消息的應用強烈地意識到它們正在與外部的並行進程進行通信,並且需要一個顯式的設計風格。基於請求/響應的應用更象一個單進程的應用,因為發送請求的應用或多或少被阻塞直至收到來自另一個進程的響應。這使得請求/響應通信自然地適合於RPC應用。
盡管消息通信和請求/響應各有他們的優點,他們都是可以用對方來實現的。消息系統可以用較底層的請求/響應協議來建立。如微軟的Message Queue Server (MSMQ)內部采用了DCE RPC來建立大多數的控制邏輯。RPC系統也可以采用較底層的消息系統來建立。MSMQ提供的關聯 ID正是為了這個目的。不管評價如何,大多數的應用仍趨向於使用RPC協議,因為它們廣泛的使用,它們更簡單的設計,以及更自然的到傳統的編程技術的映射。
在八十年代,兩個主要的RPC協議是Sun RPC 和DCE RPC。最流行的Sun RPC應用是大多數UNIX系統所使用的Network File System (NFS)。最流行的DCE RPC應用則是Windows NT?,它采用DCE RPC 協議來實現許多系統服務。這兩個協議被證明適用於很大范圍的應用。但是,在八十年代末期,面向對象技術的風靡使軟件界沉迷於在面向對象語言和基於RPC的通信之間建立一個紐帶。
在九十年代產生的對象RPC (ORPC) 協議正是試圖把面向對象和網絡協議聯系起來。ORPC 和 RPC 協議的主要不同是ORPC代碼化了從通信終端到語言級對象的映射。在每個ORPC請求的頭中都有一個cookie,服務器端的程序能用它來定位在服務器進程中的目標對象。通常這個cookie只是一個對數組的索引,但其它技術也經常被使用,如用符號名作為Hash表的鍵。
目前兩個主要的OPRC協議是DCOM 和 CORBA的 Internet Inter-ORB Protocol (IIOP) 或更一般的General Inter-ORB Protocol (GIOP)。DCOM和IIOP/GIOP的請求格式非常相似。兩個協議都用一個對象端點ID來確定目標對象,用方法標識符來決定調用哪個方法。
這兩個協議主要有兩點不同:主要的一點不同是采用IIOP/GIOP時,接口標識符是隱含的,因為一個給定的CORBA對象只實現一個接口(盡管OMG當前正在進行每個對象有多個接口支持的標准化工作)。DCOM與IIOP/GIOP請求的另一個細微差別是在傳輸體中參數值的格式。在DCOM中,傳輸體用網絡數據表達(NDR)的格式來寫,在IIOP/GIOP中,傳輸體用公共數據表達(CDR)的格式來寫。NDR和 CDR分別處理在各種平台上的不同的數據表達。但是在這兩種格式之間有一些小的差別,這使它們相互之間並不兼容。
在ORPC與RPC協議之間的另一個重要的不同是通信端點的命名方式。在ORPC協議中,對於ORPC端點的一些可傳遞的表達方式被要求在網絡之間傳遞對象引用。在CORBA/IIOP,這個表達方式被稱為可交互的對象引用(IOR)。IORs包含用緊湊格式表達的尋址信息,使用了它任何CORBA產品都可以決定一個對象端點。在DCOM中,這種表達方式被稱為OBJREF,它組合了分布的引用計算和端點/對象標識。CORBA和DCOM都提供了在網絡上尋找對象端點的高級機制,但最終這些機制都映射回到了IORs或OBJREFs。
目前的技術存在的問題?
盡管DCOM和IIOP都是固定的協議,業界還沒有完全轉向其中任何一個協議。沒有融合的部分原因是文化的問題所致。而且在當一些組織試圖標准化一個或另一個協議的時候,兩個協議的技術適用性就被提出質疑。傳統上認為DCOM和CORBA都是合理服務器到服務器端的通信協議。但是,二者對客戶到服務器端的通信都存在明顯的弱點,尤其是客戶機被散布在Internet上的時候。
DCOM 和 CORBA/IIOP都是依賴於單個廠商的解決方案來最大優勢地使用協議。盡管兩個協議都在各種平台和產品上被實現了,但現實是選定的發布需要采用單一廠商的實現。在DCOM的情況下,這意味著每個機器要運行在Windows NT。(盡管DCOM已經被轉移到其它平台,但它只在Windows?上獲得了廣泛的延伸)。在CORBA情況下,這意味著每個機器要運行同樣的ORB產品。的確讓兩個CORBA產品用IIOP相互調用是有可能的,但是許多高級的服務(如安全和事務)此時通常不是可交互的。而且,任何專門廠商為同樣的機器的通信所作的優化很難起作用,除非所有的應用被建立在同一個ORB產品上。
DCOM 和CORBA/IIOP都依賴於周密管理的環境。兩個任意的計算機使得DCOM或IIOP 在環境之外被成功調用(calls out of the box)的幾率是很低的。特別是在考慮安全性的時候尤其是這樣。盡管寫一個能成功地運用DCOM或IIOP的緊縮包(shrink-wrap)應用是可能的,但這樣做要比基於socket的應用要更多地關注細節。這對於乏味但必需的配置和安裝管理任務特別適用。
DCOM 和 CORBA/IIOP都依賴於相當高技術的運行環境。盡管進程內的COM似乎特別簡單,但COM/DCOM遠程處理程序絕對不只是幾天就解決的事情。IIOP 是一個比DCOM更容易實現的協議,但兩個協議都有相當多的深奧的規則來處理數據排列、類型信息和位操作。這使得一般的程序員在沒有領會ORB產品或OLE32.DLL的情況下去構造一個簡單的CORBA或DCOM調用也變得很困難。
也許對DCOM和CORBA/IIOP來說,最令人難以忍受的一點是它們不能在Internet 上發揮作用。對DCOM來說,一般用戶的iMac 或廉價的運行Windows 95的PC 兼容機要想使用你的服務器執行基於領域認證幾乎是不可能的。更糟的是,如果防火牆或代理服務器分隔開了客戶和服務器的機器,任何IIOP或DCOM包要通過的可能性是很低的,主要是由於大多數Internet連接技術對HTTP協議的偏愛所致。盡管一些廠商如Microsoft, Iona和Visigenic都已經建立了通道技術,但這些產品很容易對配置錯誤敏感而且它們是不可交互的。
在一個服務器群落中這些問題並不能影響DCOM或IIOP的使用。因為在服務器群落中主機的數量很少(一般是成百上千,而不是成千上萬),這就抵消了DCOM基於ping的生命周期管理的成本。在服務器群落中,所有主機被一個公共管理域管理的機率很大,使得統一的配置變得可能。相對少量的機器也能保持商業ORB產品可控制使用的成本,因為只需要更少量的ORB許可權。如果只有IIOP在服務器群落中被使用,就只需要少量的ORB許可權。最後,在服務器群落中所有主機有直接的IP連接也是可能的,這就消除了與防火牆相關的DCOM和 IIOP問題。
HTTP作為一個更好的RPC
在服務器群落中使用DCOM 和CORBA 是通用的做法,但客戶機則使用HTTP進入服務器群落。HTTP與RPC的協議很相似,它簡單、配置廣泛,並且對防火牆比其它協議更容易發揮作用。HTTP請求一般由Web服務器軟件(如IIS和Apache)來處理,但越來越多的應用服務器產品正在支持HTTP作為除DCOM和IIOP外的又一個協議。
象DCOM和IIOP一樣,HTTP層通過TCP/IP進行請求/響應通信。一個HTTP的客戶端用TCP連接到HTTP服務器。在HTTP中使用的標准端口號是80,但任何其它端口也能被使用。在建立TCP連接後,客戶端可以發送一個請求消息到服務器端。服務器在處理請求後發回一個HTTP響應消息到客戶端。請求和響應消息都可以包含任意的傳輸體的信息,通常用Content-Length和Content-Type的 HTTP 頭來標記。下面是一個合法的HTTP請求消息:
POST /foobar HTTP/1.1
Host: 209.110.197.12
Content-Type: text/plain
Content-Length: 12
Hello, World
你可能已經注意到HTTP頭只是一般文本。這使得用包檢查程序或基於文本的Internet工具(如telnet)來診斷HTTP問題變得更容易。HTTP基於文本的屬性也使得HTTP更容易適用於在Web開發中流行的低技術水平的編程環境。
HTTP請求的第一行包含三個組件:HTTP方法,請求-URI,協議版本。在前面的例子中,這些分別對應於POST, /foobar, 和 HTTP/1.1。Internet工程任務組(IETF)已經標准化了數量固定的HTTP方法。GET是HTTP用來訪問Web的方法。 POST是建立應用程序的最常用的HTTP方法。和GET不一樣,POST允許任意數據從客戶端發送到服務器端。請求URI (Uniform Resource Identifier)是一個HTTP服務器端軟件,它用來識別請求的目標的簡單的標識符(它更象一個IIOP/GIOP object_key 或一個DCOM IPID)。關於URIs更多的信息請參照"URIs, URLs, and URNs"。在這個例子中協議的版本是HTTP/1.1, 它表示遵守RFC 2616的規則。HTTP/1.1比HTTP/1.0多增加了幾個特性,包括對大塊數據傳輸的支持以及對在幾個HTTP請求之間保持TCP連接的支持。
請求的第三行和第四行指定了請求體的尺寸和類型。Content-Length 頭指定了體信息的比特數。Content-Type類型標識符指定MIME類型為體信息的語法。HTTP (象 DCE一樣) 允許服務器和客戶端協商用於編制信息的傳輸語法。大多數DCE應用采用NDR.。大多數Web應用采用text/html 或其它基於文本的語法。
注意在上面樣例中Content-Length頭與請求體之間的空行。不同的HTTP頭被carriage-return/行碼序列劃定界限。這些頭與體之間用另外的carriage-return/行碼序列來劃定界限。請求接著包括原始字節,這些字節的語法和長度由Content-Length和Content-Type HTTP 頭來識別。在這個例子中,內容是十二字節的普通文本字符串"Hello, World"。
在處理了請求之後,HTTP服務器被期望發回一個HTTP響應到客戶端。響應必須包括一個狀態代碼來表示請求的結果。響應也可以包含任意的體信息。下面是一個HTTP響應消息:
200 OK
Content-Type: text/plain
Content-Length: 12
dlroW ,olleH
在這個例子中,服務器返回狀態代碼200,它是HTTP中標准的成功代碼。如果服務器端不能破解請求代碼,它將返回下列的響應:
400 Bad Request
Content-Length: 0
如果HTTP服務器決定到目標URI的請求應該臨時轉向另外的一個不同的URI,下列響將被返回:
307 Temporarily Moved
Location: http://209.110.197.44/foobar
Content-Length: 0
這個響應告知客戶,請求將能夠通過重新傳遞它到在Location頭中指定的地址來被滿足。
所有的標准狀態碼和頭都在RFC 2616中被描述。它們中很少的內容與SOAP用戶直接相關,但有一個明顯的例外。在HTTP/1.1,底層的TCP連接在多個請求/響應對之間重用。HTTP Connection頭允許客戶端或服務器中任何一方關閉底層的連接。通過增加下列HTTP頭到請求或響應中,雙方都會要求在處理請求後關閉它們的TCP連接:
Connection: close
當與HTTP/1.0軟件交互時,為了保持TCP連接,建議發送方加入下列HTTP頭到每個請求或響應中:
Connection: Keep-Alive
這個頭使缺省的HTTP/1.0協議在每次響應後重新開始TCP連接的行為無法使用。
HTTP的一個優點是它正被廣泛的使用和接受。圖4表示了一個簡單的Java程序,它發送前面表示的請求並從響應中解析出結果字符串。
下面則是一個簡單的C程序用CGI來讀取來自HTTP請求的字符串並通過HTTP響應把它的逆序串返回。
#include <stdio.h>
int main(int argc, char **argv) {
char buf[4096];
int cb = read(0, buf, sizeof(buf));
buf[cb] = 0;
strrev(buf);
printf("200 OK\r\n");p>
printf("Content-Type: text/plain\r\n");
printf("Content-Length: %d\r\n", cb);
printf("\r\n");
printf(buf);
return 0;
服務器的實現是用Java servlet,以避免CGI的每個請求一個進程的開銷。
一般來說CGI是花費代價最小的寫HTTP服務器端代碼的方法。實際上,每一個HTTP服務器端產品都提供了一個更有效的機制來讓你的代碼處理一個HTTP請求。IIS提供了ASP和ISAPI作為寫HTTP代碼的機制。Apache允許你用運行在Apache後台程序中的 C或Perl來寫模塊。大多數應用服務器軟件允許你寫Java servlet,COM組件,EJB Session beans或基於可攜帶對象適配器(POA)接口的CORBA servants。
XML作為一個更好的網絡數據表達方式(NDR)
HTTP是一個相當有用的RPC協議,它提供了IIOP或DCOM在組幀、連接管理以及序列化對象應用等方面大部分功能的支持。( 而且URLs與IORs和OBJREFs在功能上令人驚歎的接近)。HTTP所缺少的是用單一的標准格式來表達一個RPC調用中的參數。這則正是XML的用武之地。
象NDR和CDR,XML是一個與平台無關的中性的數據表達協議。XML允許數據被序列化成一個可以傳遞的形式,使得它容易地在任何平台上被解碼。XML有以下不同於NDR和CDR的特點:
有大量XML編碼和解碼軟件存在於每個編程環境和平台上XML基於文本,相當容易用低技術水平的編程環境來處理XML是特別靈活的格式,它容易用一致的方式來被擴展為支持可擴展性,在XML中每一個元素和屬性有一個名域URI與它相聯系,這個URI用xmlns屬性來指定。
考慮下面的XML文檔:
<reverse_string xmlns="urn:schemas-develop-com:StringProcs">
<string1>Hello, World</string1>
<comment xmlns=‘http://foo.com/documentation‘>
This is a comment!!
</comment>
</reverse_string>
元素<reverse_string>和<string1>的名域URI是urn:schemas-develop-com:StringProcs。元素<comment>的名域URI是http://foo.com/documentation。第二個URI也是一個URL的事實是不重要的。在這兩種情況下,URI簡單地被用來消除元素<reverse_string>,<string1>,<comment>和任何碰巧有同樣標記名的其它元素間的歧義。
為了方便,XML允許名域URIs被映射為局部唯一的前綴。這意味著下面的XML文檔在語義上等同於上面的文檔:
<sp:reverse_string
xmlns:sp="urn:schemas-develop-com:StringProcs"
xmlns:doc=‘http://foo.com/documentation‘
>
<sp:string1>Hello, World</sp:string1>
<doc:comment>
This is a comment!!
</doc:comment>
</sp:reverse_string>
後面的形式對作者來說更容易,尤其是如果有許多名域URIs在使用時。
XML也支持帶類型的數據表達。正在推出的XML Schema規范為描述XML數據類型標准化了一個詞匯集。下面是一個元素<reverse_string>的XML Schema的描述:
<schema
xmlns=‘http://www.w3.org/1999/XMLSchema‘
targetNamespace=‘urn:schemas-develop-com:StringProcs‘
>
<element name=‘reverse_string‘>
<type>
<element name=‘string1‘ type=‘string‘ />
<any minOccurs=‘0‘ maxOccurs=‘*‘/>
</type>
</element>
</schema>
這個XML Schema定義闡述了XML名域urn:schemas-develop-com:StringProcs包含了一個名為<reverse_string>的元素,這個元素包含了一個名為string1的子元素(類型為string),它被0個或更多沒有指定的元素所遵守。
XML Schema 規范還定義了一組內置的原始數據類型和建立一個XML文檔中元素的類型的機制。下面的XML文檔用XML Schema類型屬性來把元素和類型名聯系在一起:
<customer
xmlns=‘http://customer.is.king.com‘
xmlns:xsd=‘http://www.w3.org/1999/XMLSchema‘
>
<name xsd:type=‘string‘>Don Box</name>
<age xsd:type=‘float‘>23.5</name>
</customer>
連接XML文檔事例到XML Schema描述的新的一個機制在本文寫作的時候正在標准化過程中。
HTTP + XML = SOAP
SOAP把XML的使用代碼化為請求和響應參數編碼模式,並用HTTP作傳輸。這似乎有點抽象。具體地講,一個SOAP方法可以簡單地看作遵循SOAP編碼規則的HTTP請求和響應。一個SOAP終端則可以看作一個基於HTTP的URL,它用來識別方法調用的目標。象CORBA/IIOP一樣,SOAP不需要具體的對象被綁定到一個給定的終端,而是由具體實現程序來決定怎樣把對象終端標識符映射到服務器端的對象。
SOAP請求是一個HTTP POST請求。SOAP請求的content-type必須用text/xml。而且它必須包含一個請求-URI。服務器怎樣解釋這個請求-URI是與實現相關的,但是許多實現中可能用它來映射到一個類或者一個對象。一個SOAP請求也必須用SOAPMethodName HTTP頭來指明將被調用的方法。簡單地講,SOAPMethodName頭是被URI指定范圍的應用相關的方法名,它是用#符作為分隔符將方法名與URI分割開:
SOAPMethodName: urn:strings-com:IString#reverse
這個頭表明方法名是reverse,范圍URI是urn:strings-com:Istring。 在SOAP中,規定方法名范圍的名域URI在功能上等同於在DCOM 或 IIOP中規定方法名范圍的接口ID。
簡單的說,一個SOAP請求的HTTP體是一個XML文檔,它包含方法中[in]和[in,out]參數的值。這些值被編碼成為一個顯著的調用元素的子元素,這個調用元素具有SOAPMethodName HTTP頭的方法名和名域URI。調用元素必須出現在標准的SOAP <Envelope>和<Body>元素內(後面會更多討論這兩個元素)。下面是一個最簡單的SOAP方法請求:
POST /string_server/Object17 HTTP/1.1
Host: 209.110.197.2
Content-Type: text/xml
Content-Length: 152
SOAPMethodName: urn:strings-com:IString#reverse
<Envelope>
<Body>
<m:reverse xmlns:m=‘urn:strings-com:IString‘>
<theString>Hello, World</theString>
</m:reverse>
</Body>
</Envelope>
SOAPMethodName頭必須與<Body>下的第一個子元素相匹配,否則調用將被拒絕。這允許防火牆管理員在不解析XML的情況下有效地過濾對一個具體方法的調用。
SOAP響應的格式類似於請求格式。響應體包含方法的[out]和 [in,out]參數,這個方法被編碼為一個顯著的響應元素的子元素。這個元素的名字與請求的調用元素的名字相同,但以Response後綴來連接。下面是對前面的SOAP請求的SOAP響應:
200 OK
Content-Type: text/xml
Content-Length: 162
<Envelope>
<Body>
<m:reverseResponse xmlns:m=‘urn:strings-com:IString‘>
<result>dlroW ,olleH</result>
</m:reverseResponse>
</Body>
</Envelope>
這裡響應元素被命名為reverseResponse,它是方法名緊跟Response後綴。要注意的是這裡是沒有SOAPMethodName HTTP頭的。這個頭只在請求消息中需要,在響應消息中並不需要。
讓許多SOAP新手困惑的是SOAP中沒有關於SOAP服務器怎樣使用請求頭來分發請求的要求;這被留為一個實現上的細節。一些SOAP服務器將映射請求-URIs到類名,並分派調用到靜態方法或到在請求持續期內存活的類的實例。其它SOAP服務器則將請求-URIs映射到始終存活的對象,經常是用查詢字符串來編碼一個用來定位在服務器進程中的對象關鍵字。還有一些其它的SOAP服務器用HTTP cookies來編碼一個對象關鍵字,這個關鍵字可被用來在每次方法請求中恢復對象的狀態。重要的是客戶對這些區別並不知道。客戶軟件只是簡單遵循HTTP和XML的規則來形成SOAP請求,讓服務器自由以它認為最合適的方式來為請求服務。
SOAP體的核心
SOAP的XML特性是為把數據類型的實例序列化成XML的編碼模式。為了達到這個目的,SOAP不要求使用傳統的RPC風格的代理。而是一個SOAP方法調用包含至少兩個數據類型:請求和響應。考慮這下面個COM IDL代碼:
[ uuid(DEADF00D-BEAD-BEAD-BEAD-BAABAABAABAA) ]
interface IBank : IUnknown {
HRESULT withdraw([in] long account,
[out] float *newBalance,
[in, out] float *amount
[out, retval] VARIANT_BOOL *overdrawn);
}
在任何RPC協議下,account和amount參數的值將出現在請求消息中,newBalance,overdrawn參數的值,還有amount參數的更新值將出現在響應消息中。
SOAP把方法請求和方法響應提升到了一流狀態。在SOAP中,請求和響應實際上類型的實例。為了理解一個方法比如IBank::withdraw怎樣映射一個SOAP請求和響應類型,考慮下列的數據類型:
struct withdraw {
long account;
float amount;
};
這是一個所有的請求參數被打包成為一個單一的數據類型。同樣下面的數據表示打包所有響應參數到一個單一的數據類型。
struct withdrawResponse {
float newBalance;
float amount;
VARIANT_BOOL overdrawn;
};
再給出下面的簡單的Visual Basic程序,它使用了以前定義的Ibank接口:
Dim bank as IBank
Dim amount as Single
Dim newBal as Single
Dim overdrawn as Boolean
amount = 100
Set bank = GetObject("soap:http://bofsoap.com/am")
overdrawn = bank.withdraw(3512, amount, newBal)
你能夠想象底層的代理(可能是一個SOAP,DCOM,或IIOP代理)看上去象圖8中所表示的那樣。這裡,在發送請求消息之前,參數被序列化成為一個請求對象。同樣被響應消息接收到的響應對象被反序列化為參數。一個類似的轉變同樣發生在調用的服務器端。
當通過SOAP調用方法時,請求對象和響應對象被序列化成一種已知的格式。每個SOAP體是一個XML文檔,它具有一個顯著的稱為<Envelope>的根元素。標記名<Envelope>由SOAP URI (urn:schemas-xmlsoap-org:soap.v1)來劃定范圍,所有SOAP專用的元素和屬性都是由這個URI來劃定范圍的。SOAP Envelope包含一個可選的<Header>元素,緊跟一個必須的<Body>元素。<Body>元素也有一個顯著的根元素,它或者是一個請求對象或者是一個響應對象。下面是一個IBank::withdraw請求的編碼:
<soap:Envelope
xmlns:soap=‘urn:schemas-xmlsoap-org:soap.v1‘>
<soap:Body>
<IBank:withdraw xmlns:IBank=
‘urn:uuid:DEADF00D-BEAD-BEAD-BEAD-BAABAABAABAA‘>
<account>3512</account>
<amount>100</amount>
</IBank:withdraw>
</soap:Body>
</soap:Envelope>
下列響應消息被編碼為:
<soap:Envelope
xmlns:soap=‘urn:schemas-xmlsoap-org:soap.v1‘>
<soap:Body>
<IBank:withdrawResponse xmlns:IBank=
‘urn:uuid:DEADF00D-BEAD-BEAD-BEAD-BAABAABAABAA‘>
<newBalance>0</newBalance>
<amount>5</amount>
<overdrawn>true</overdrawn>
</IBank:withdrawResponse>
</soap:Body>
</soap:Envelope>
注意[in, out]參數出現在兩個消息中。
在檢查了請求和響應對象的格式後,你可能已經注意到序列化格式通常是:
<t:typename xmlns:t=‘namespaceuri‘> ;
<fieldname1>field1value</fieldname1>
<fieldname2>field2value</fieldname2>
</t:typename>
在請求的情況下,類型是隱式的C風格的結構,它由對應方法中的[in]和[in, out]參數組成。對響應來說,類型也是隱式的C風格的結構,它由對應方法中的[out]和[in, out]參數組成。這種每個域對應一個子元素的風格有時被稱為元素正規格式(ENF)。一般情況下,SOAP只用XML特性來傳達描述包含在元素內容中信息的注釋。
象DCOM和IIOP一樣,SOAP支持協議頭擴展。SOAP用可選的<Header>元素來傳載被協議擴展所使用的信息。如果客戶端的SOAP軟件包含要發送頭信息,原始的請求將可能如圖9所示。在這種情況下命名causality的頭將與請求一起序列化。收到請求後,服務器端軟件能查看頭的名域URI,並處理它識別出的頭擴展。這個頭擴展被http://comstuff.com URI識別,並期待一個如下的對象:
struct causality {
UUID id;
};
在這種情況下的請求,如果頭元素的URI不能被識別,頭元素可以被安全地忽略。
但你不能安全的忽略所有的SOAP體中的頭元素。如果一個特定的SOAP頭對正確處理消息是很關鍵的,這個頭元素能被用SOAP屬性mustUnderstand=’true’標記為必須的。這個屬性告訴接收者頭元素必須被識別並被處理以確保正確的使用。為了強迫前面causality頭成為一個必須的頭,消息將被寫成如下形式:
<soap:Envelope
xmlns:soap=‘urn:schemas-xmlsoap-org:soap.v1‘>
<soap:Header>
<causality
soap:mustUnderstand=‘true‘
xmlns="http://comstuff.com">
<id>362099cc-aa46-bae2-5110-99aac9823bff</id>
</causality>
</soap:Header>
<!— soap:Body element elided for clarity —>
</soap:Envelope>
SOAP軟件遇到不能識別必須的頭元素情況時,必須拒絕這個消息並出示一個錯誤。如果服務器在一個SOAP請求中發現一個不能識別的必須的頭元素,它必須返回一個錯誤響應並且不發送任何調用到目標對象。如果客戶端在一個SOAP請求中發現一個不能識別出的必須的頭元素,它必須向調用者返回一個運行時錯誤。(在COM情況下,這將映射為一個明顯的HRESULT)
SOAP數據類型
在SOAP消息中,每個元素可能是一個SOAP結構元素,一個根元素,一個存取元素或一個獨立的元素。在SOAP中,soap:Envelope, soap:Body和 soap:Header 是唯一的三個結構元素。它們的基本關系由下列XML Schema所描述:
<schema
targetNamespace=‘urn:schemas-xmlsoap-org:soap.v1‘>
<element name=‘Envelope‘>
<type>
<element name=‘Header‘ type=‘Header‘
minOccurs=‘0‘ />
<element name=‘Body‘ type=‘Body‘
minOccurs=‘1‘ />
</type>
</element>
</schema>
在SOAP元素的四種類型中,除了結構元素外都被用作表達類型的實例或對一個類型實例的引用。
根元素是顯著的元素,它是soap:Body 或是 soap:Header的直接的子元素。其中soap: Body只有一個根元素,它表達調用、響應或錯誤對象。這個根元素必須是soap:Body的第一個子元素,它的標記名和域名URI必須與HTTP SOAPMethodName頭或在錯誤消息情況下的soap:Fault相對應。而soap:Header元素有多個根元素,與消息相聯系的每個頭擴展對應一個。這些根元素必須是soap:Header的直接子元素,它們的標記名和名域URI表示當前存在擴展數據的類型。
存取元素被用作表達類型的域、屬性或數據成員。一個給定類型的域在它的SOAP表達將只有一個存取元素。存取元素的標記名對應於類型的域名。考慮下列Java 類定義:
package com.bofsoap.IBank;
public class adjustment {
public int account ;
public float amount ;
}
在一個SOAP消息中被序列化的實例如下所示:
<t:adjustment
xmlns:t=‘urn:develop-com:java:com.bofsoap.IBank‘>
<account>3514</account>
<amount>100.0</amount>
</t:adjustment>
在這個例子中,存取元素account和amount被稱著簡單存取元素,因為它們訪問對應於在W3C XML Schema規范 (見 http://www.w3.org/TR/XMLSchema-2) 的Part 2中定義的原始數據類型的值。這個規范指定了字符串,數值,日期等數據類型的名字和表達方式以及使用一個新的模式定義中的<datatype>結構來定義新的原始類型的機制。
對引用簡單類型的存取元素,元素值被簡單地編碼為直接在存取元素下的字符數據,如上所示。對引用組合類型的存取元素(就是那些自身用子存取元素來構造的存取元素),有兩個技術來對存取元素進行編碼。最簡單的方法是把被結構化的值直接嵌入在存取元素下。考慮下面的Java類定義:
package com.bofsoap.IBank;
public class transfer {
public adjustment from;
public adjustment to;
}
如果用嵌入值編碼存取元素,在SOAP中一個序列化的transfer對象如下所示:
<t:transfer
xmlns:t=‘urn:develop-com:java:com.bofsoap.IBank‘
>
<from>
<account>3514</account>
<amount>-100.0</amount>
</from>
<to>
<account>3518</account>
<amount>100.0</amount>
</to>
</t:transfer>
在這種情況下,adjustment對象的值被直接編碼在它們的存取元素下。
在考慮組合存取元素時,需要說明幾個問題。先考慮上面的transfer類。類的from和to的域是對象引用,它可能為空。SOAP用XML Schemas的null屬性來表示空值或引用。下面例子表示一個序列化的transfer對象,它的from域是空的:
<t:transfer
xmlns:t=‘urn:develop-com:java:com.bofsoap.IBank‘
xmlns:xsd=‘http://www.w3.org/1999/XMLSchema/instance‘
>
<from xsd:null=‘true‘ />
<to>
<account>3518</account>
<amount>100.0</amount>
</to>
</t:transfer>
在不存在的情況下, xsd:null屬性的隱含值是false。給定元素的能否為空的屬性是由XML Schema定義來控制的。例如下列XML Schema將只允許from存取元素為空:
<type name=‘transfer‘ >
<element
name=‘from‘
type=‘adjustment‘
nullable=‘true‘
/>
<element
name=‘to‘
type=‘adjustment‘
nullable=‘false‘ <!— false is the default —>
/>
</type>
在一個元素的Schema聲明中如果沒有nullable屬性,就意味著在一個XML文檔中的元素是不能為空的。Null存取元素的精確格式當前還在修訂中�要了解用更多信息參考最新版本的SOAP規范。
與存取元素相關的另一個問題是由於類型關系引起的可代換性。由於前面的adjustment類不是一個final類型的類,transfer對象的from和to域實際引用繼承類型的實例是可能的。為了支持這種類型兼容的替換,SOAP使用一個名域限定的類型屬性的XML Schema約定。這種類型屬性的值是一個對元素具體的類型的限制的名字。考慮下面的adjustment擴展類:
package com.bofsoap.IBank;
public class auditedadjustment extends adjustment {
public int auditlevel;
}
給出下面Java語言:
transfer xfer = new transfer();
xfer.from = new auditedadjustment();
xfer.from.account = 3514; xfer.from.amount = -100;
xfer.from.auditlevel = 3;
xfer.to = new adjustment();
xfer.to.account = 3518; xfer.from.amount = 100;
在SOAP中transfer對象的序列化形式如下所示:
<t:transfer
xmlns:xsd=‘http://www.w3.org/1999/XMLSchema‘
xmlns:t=‘urn:develop-com:java:com.bofsoap.IBank‘
>
<from xsd:type=‘t:auditedadjustment‘ >
<account>3514</account>
<amount>-100.0</amount>
<auditlevel>3</auditlevel >
</from>
<to>
<account>3518</account>
<amount>100.0</amount>
</to>
</t:transfer>
在這裡xsd:type屬性引用一個名域限定的類型名,它能被反序列化程序用於實例化對象的正確類型。因為to存取元素引用到一個被預料的類型的實例(而不是一個可代替的繼承類型),xsd:type屬性是不需要的。
剛才的transfer類設法回避了一個關鍵問題。如果正被序列化的transfer對象用下面這種方式初始化將會發生什麼情況:
transfer xfer = new transfer();
xfer.from = new adjustment();
xfer.from.account = 3514; xfer.from.amount = -100;
xfer.to = xfer.from;
基於以前的議論,在SOAP 中transfer對象的序列化形式如下所示:
<t:transfer
xmlns:t=‘urn:develop-com:java:com.bofsoap.IBank‘>
<from>
<account>3514</account>
<amount>-100.0</amount>
</from>
<to>
<account>3514</account>
<amount>-100.0</amount>
</to>
</t:transfer>
這個表達有兩個問題。首先最容易理解的問題是同樣的信息被發送了兩次,這導致了一個比實際所需要消息的更大的消息。一個更微妙的但是更重要的問題是由於反序列化程序不能分辨兩個帶有同樣值的adjustment對象與在兩個地方被引用的一個單一的adjustment對象的區別,兩個存取元素間的身份關系就被丟失。如果這個消息接收者已經在結果對象上執行了下面的測試,(xfer.to == xfer.from)將不會返回true。
void processTransfer(transfer xfer) {
if (xfer.to == xfer.from)
handleDoubleAdjustment(xfer.to);
else
handleAdjustments(xfer.to, xfer.from);
}
(xfer.to.equals(xfer.from))可能返回true的事實只是比較了兩個存取元素的值而不是它們身份。
為了支持必須保持身份關系的類型的序列化,SOAP支持多引用存取元素。目前我們接觸到的存取元素是單引用存取元素,也就是說,元素值是嵌入在存取元素下面的,而且其它存取元素被允許引用那個值(這很類似於在NDR中的[unique]的概念)。多引用存取元素總是被編碼為只包含已知的soap:href屬性的空元素。soap:href屬性總是包含一個代碼片段標識符,它對應於存取元素引用到的實例。如果to和from存取元素已經被編碼為多引用存取元素,序列化的transfer對象如下所示:
<t:transfer
xmlns:t=‘urn:develop-com:java:com.bofsoap.IBank‘>
<from soap:href=‘#id1‘ />
<to soap:href=‘#id1‘ />
</t:transfer>
這個編碼假設與adjustment類兼容的一個類型的實例已經在envelope中的其它地方被序列化,而且這個實例已經被用soap:id屬性標記,如下所示:
<t:adjustment soap:id=‘id1‘
xmlns:t=‘urn:develop-com:java:com.bofsoap.IBank‘>
<account>3514</account>
<amount>-100.0</amount>
</t:adjustment>
對多引用存取元素,把代碼段的標識符(例如#id1)分解到正確的實例是反序列化程序的工作。
前面的討論解釋了多引用存取元素怎樣與它的目標實例相關聯。下面要討論的是目標實例在哪裡被序列化。這就關系到獨立元素和包的概念。
獨立元素
在SOAP中,一個獨立元素表示至少被一個多引用存取元素引用的類型的實例。所有的獨立元素用soap:id屬性作標記,而且這個屬性的值在整個SOAP envelope中必須是唯一的。獨立的元素被編碼就好象是它們被一個存取元素打包,這個存取元素的標記名是實例的名域限制的類型名。在上面的例子中,實例的名域限制的類型名是t:adjustment。
SOAP限制獨立元素能被編碼的場所。SOAP定義了一個能適用於任何元素的屬性:(soap:Package)。這個屬性被用於控制獨立元素能在哪裡被解碼。SOAP序列化規則指出獨立元素必須編碼為soap:Header元素或soap:Body元素的直接子元素,或者是任何其它標記為soap:Package=‘true’的元素。通過把一個元素注釋為包,你能保證編碼那個實例的XML元素是完全自包含的,並且在這個包以外沒有任何引用到這個元素的多引用存取元素。
假設transfer 類對應於一個方法請求。如果transfer類型不是一個包,被to和from存取元素引用的獨立元素將作為soap:Body元素的直接子元素出現,如圖10所示。如果transfer類型是一個合法的SOAP包類型,編碼可能象圖11所示。注意,因為transfer元素是一個包,所有多引用存取器元素都引用被包含的元素。這使得把transfer元素看成一個能從它的父輩元素中分離出的獨立的XML代碼段變得更為容易。
多引用存取元素總是引用獨立元素的模型是有一個例外的。SOAP允許包含字符串和二進值數據的存取元素是多引用存取元素的目標。這意味著下面的代碼是合法的:
<t:mytype>
<field1 soap:href="http://www.bianceng.cn/index.php#id1" />
<field2 soap:id="id1">Hello, SOAP</field2>
</t:mytype>
盡管事實是存取元素2有一個soap:id屬性,它實際上是一個存取元素而不是獨立元素。
SOAP數組
數組被編碼為組合類型的一個特殊的例子。在SOAP中,一個數組必須有一個秩(維數)和一個容量。一個數組被編碼為一個組合類型,其中每一個數組元素被編碼為一個子元素,這個子元素的名字是元素的名域限制的類型名。
假設有下面的COM IDL類型定義:
struct POINTLIST {
long cElems;
[size_is(cElems)] POINT points[];
};
這個類型的實例將被序列化為:
<t:POINTLIST xmlns:t=‘uri for POINTLIST‘>
<cElems>3</cElems>
<points xsd:type=‘t:POINT[3]‘ >
<POINT>lt;x>3</x>lt;y>4</y>lt;/POINT>
<POINT>lt;x>7</x>lt;y>5</y>lt;/POINT>
<POINT>lt;x>1</x>lt;y>9</y>lt;/POINT>
</points>
<t:POINTLIST>
如果points域被標記為[ptr]屬性,這個編碼將用一個多引用存取元素,如下所示:
<t:POINTLIST xmlns:t=‘uri for POINTLIST‘>
<cElems>3</cElems>
<points soap:href="http://www.bianceng.cn/index.php#x9" />
</t:POINTLIST>
<t:ArrayOfPOINT soap:id=‘x9‘ xsd:type=‘t:POINT[3]‘>
<POINT>lt;x>3</x>lt;y>4</y>lt;/POINT>
<POINT>lt;x>7</x>lt;y>5</y>lt;/POINT>
<POINT>lt;x>1</x>lt;y>9</y>lt;/POINT>
</t:ArrayOfPOINT>
當把一個數組編碼為一個獨立元素時,標記名是帶前綴ArrayOf的類型名。
象NDR和CDR一樣,SOAP支持部分轉換的數組。如果子元素的數量少於所聲明的容量,這些元素被假設正從數組的末尾丟失。這能夠通過在正包含的數組元素上使用soap:offset屬性來被忽略。
<t:ArrayOfPOINT soap:id=‘x9‘ xsd:type=‘t:POINT[5]‘
soap:offset=‘[1]‘>
<POINT>lt;x>1</x>lt;y>9</y>lt;/POINT>
</t:ArrayOfPOINT>
soap:offset屬性表示出現在數組中的第一個元素的索引。在上面的例子中,元素0,2到4都是不被轉換的。SOAP也支持稀疏數組,這是通過使用soap:position屬性來把每個元素用它的絕對索引來注釋而實現的:
<t:ArrayOfPOINT soap:id=‘x9‘ xsd:type=‘t:POINT[9]‘>
<POINT soap:position=‘[3]‘>lt;x>3</x>lt;y>4</y>lt;/POINT>
<POINT soap:position=‘[7]‘>lt;x>4</x>lt;y>5</y>lt;/POINT>
</t:ArrayOfPOINT>
在這個例子中,元素0到2,4到6,以及8到9都不是被轉換的。
請注意,在SOAP中數組的精確語法在這篇文章寫作時還在被重新審查以調整到即將推出的W3C XML Schema規范中。要不斷了解SOAP規范的最新版本來獲得更多的細節。
錯誤處理
一個服務器有時將不能正確地為一個方法請求提供服務。這可能是由於一般的HTTP錯誤造成的(如請求-URI不能被映射到本地的資源或一個HTTP級的安全違反)。也可能是在SOAP翻譯軟件中的問題,如馬歇爾打包錯誤或一個必須的頭不能被認出。其它可能的原因包括一個請求不能正確地被服務,或者應用/對象代碼決定要返回一個應用級的錯誤給調用者。這些情況在SOAP規范中都被清楚地加以處理。
如果在分發對任何SOAP代碼的調用之前一個錯誤發生在HTTP層,一個純HTTP響應必須被返回。標准的HTTP狀態代碼編號將被采用,400級的代碼表示一個客戶引發的錯誤,500級的代碼表示服務器引發的錯誤。這通常在代碼執行前由Web服務器軟件自動處理。
假設在HTTP層一切正常,錯誤發生的下一個地方是在那些翻譯和分發對應用代碼(如COM對象和CORBA伺服對象)的SOAP調用。如果錯誤發生在這一層,服務器必須返回一個錯誤消息來代替一個標准的響應消息。一個錯誤消息是下列被編碼為soap:Body的根元素的類型的實例。
<schema targetNamespace=‘urn:schemas-xmlsoap-org:soap.v1‘ >
<element name=‘Fault‘>
<type>
<element name=‘faultcode‘ type=‘string‘ />
<element name=‘faultstring‘ type=‘string‘ />
<element name=‘runcode‘ type=‘string‘ />
<element name=‘detail‘ />
</type>
</element>
</schema>
faultcode存取元素必須包含一個用已知的整數表示的SOAP錯誤代碼或者一個專門應用的名域限制的值。當前的SOAP 錯誤代碼如圖12所示。Faultstring存取元素包含對發生的錯誤的可讀性的描述。runcode 存取元素包含一個字符串,它的值必須是Yes, No或 Maybe,表明被請求的操作實際上是否在錯誤產生之前被執行。Detail存取元素是可選的,用於包含一個專門應用的異常對象。
下面是一個對應於一個包含無法識別的必須的頭元素的請求的SOAP錯誤的例子:
<soap:Envelope
xmlns:soap=‘urn:schemas-xmlsoap-org:soap.v1‘
>
<soap:Body>
<soap:Fault> ;
<faultcode>200</faultcode>
<faultstring>
Unrecognized ‘causality‘ header
</faultstring>
<runcode>No</runcode>
</soap:Fault>
</soap:Body>
</soap:Envelope>
假設具體應用的錯誤需要被返回,你可能看到如圖13所示的代碼。在應用定義的錯誤的情況下,考慮應用的異常/錯誤對象時detail存取元素起到了soap:Body 元素的作用。
奧秘
一個遺留的HTTP問題還需要進一步闡明。SOAP支持(但不需要)HTTP擴展框架約定來指定必須的HTTP頭擴展。這些約定主要有兩個目的。首先,它們允許任意的URI被用於限定給定的HTTP頭的范圍(象XML名域一樣)。第二,這些約定允許把必須的頭與可選的頭區分開來(象soap:mustUnderstand)。下面是一個使用HTTP擴展框架來把SOAPMethodName頭定義成為一個必須的頭擴展:
M-POST /foobar HTTP/1.1
Host: 209.110.197.2
Man: "urn:schemas-xmlsoap-org:soap.v1; ns=42"
42-SOAPMethodName: urn:bobnsid:IFoo#DoIt
Man頭映射SOAP URI到前綴為42的頭,並表示沒有認出SOAP的服務器必須返回一個HTTP錯誤,狀態代碼為501 (沒有被實現) 或 510 (沒有被擴展)。HTTP方法必須是M-POST,表明目前是必須的頭擴展。
結論
SOAP是一個被類型化的序列化格式,它恰巧用HTTP 作為請求/響應消息傳輸協議。SOAP被設計為與正將出現的XML Schema規范密切配合,並支持在Internet的任何地方運行的COM, CORBA, Perl, Tcl, 和 Java-language, C, Python, 或 PHP 等程序間的互操作性。
希望本文給了你一個對這個協議具體細節的更清晰的理解。我鼓勵你用SOAP進行實驗,或者試著使用SOAP使能的系統之一(列在http://www.develop.com/soap/),或者自己做一些工作。我本人發現采用腳本語言(Jscript),使一個基本的SOAP客戶與服務器建立並運行只花費了不到一個小時。針對你對HTTP和XML的熟悉程度,以及你的目標平台的成熟度,你所花費的時間會有所不同。