作者:Don Box 簡單物件訪問協議(SOAP)初級指南 (轉)

worldblog發表於2007-12-10
作者:Don Box 簡單物件訪問協議(SOAP)初級指南 (轉)[@more@]

 

總結:
(本文假設讀者對COM和技術已經很熟悉。)
P(Simple Access Protocal) 技術有助於實現大量異構和平臺之間的互操作性,從而使存在的應用能夠被廣泛的所訪問。SOAP是把成熟的基於HTTP的技術與XML的靈活性和可擴充套件性組合在了一起。
這篇文章帶你全面回顧程式(ORPC)技術的歷程,以幫助你理解SOAP技術的基礎,以及它克服存在技術(如和DCOM)的許多缺陷的方法。隨後講述詳細的SOAP編碼規則,並把焦點放在SOAP是怎樣對映到存在的ORPC概念上的。
引言:
當我在1984年開始把計算作為我的職業的時候,大多數程式設計師並不關心。但是在九十年代網路變得無所不在,現在如果有誰使用卻不使用某種形式網路連線是很難以想象的。今天,一般的程式設計師對建立可擴充套件的分散式應用表現出更大的興趣,而不再只是關注於用MFC實現個性化的可浮動半透明非矩形的Coolbars了。
程式設計師通常喜歡用模型來思考問題,而很少考慮網路協議。儘管這樣做通常是很好的,但在這篇文章中我將討論的SOAP是一個沒有明顯的程式設計模型的網路協議。這並不意味著SOAP的體系結構從根本上會改變你程式設計的方式。相反,SOAP的一個主要目標是使存在的應用能被更廣泛的使用者所使用。為了實現這個目的,沒有任何SOAP 或SOAP 物件請求(SOAP ORB),SOAP是假設你將使用盡可能多的存在的技術。幾個主要的CORBA廠商已經承諾在他們的ORB產品中支援SOAP協議。也承諾在將來的COM版本中支援SOAP。DevelopMentor已經開發了參考實現,它使得在任何平臺上的任何或程式設計師都可以使用SOAP。
在SOAP後面的指導理念是“它是第一個沒有發明任何新技術的技術”。SOAP採用了已經廣泛使用的兩個協議:HTTP和XML。HTTP用於實現SOAP的RPC風格的傳輸,而XML是它的編碼。採用幾行程式碼和一個XML解析器,HTTP(如MS的IIS或)立刻成為了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應用是大多數系統所使用work File System (NFS)。最流行的DCE RPC應用則是 NT?,它採用DCE RPC 協議來實現許多系統服務。這兩個協議被證明適用於很大範圍的應用。但是,在八十年代末期,物件導向技術的風靡使界沉迷於在面嚮物件語言和基於RPC的通訊之間建立一個紐帶。
在九十年代產生的物件RPC (ORPC) 協議正是試圖把物件導向和網路協議聯絡起來。ORPC 和 RPC 協議的主要不同是ORPC程式碼化了從通訊終端到語言級物件的對映。在每個ORPC請求的頭中都有一個cookie,伺服器端的程式能用它來定位在伺服器程式中的目標物件。通常這個cookie只是一個對陣列的,但其它技術也經常被使用,如用符號名作為Hash表的鍵。
 
圖1 ORPC請求與響應
圖1表示一個典型的ORPC請求和響應。有幾個請求頭被伺服器端的處理程式用於分發呼叫。物件端點ID被用於定位在伺服器程式中目標物件。介面識別符號和方法識別符號用於決定在目標物件中哪一個方法被呼叫。傳輸體用於傳遞請求中的[in]和[in,out]引數的值(在響應中是[out]和[in,out])。要注意的是任選的協議擴充套件可以出現在頭和傳輸體之間。這是在協議設計中的慣例,因為它允許新的服務搭載在ORPC的請求和服務上。大多數ORPC系統用這個區域傳遞附加的上下文資訊(如事務資訊和因果關係識別符號)。
目前兩個主要的OPRC協議是D和 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中,這種表達方式被稱為OBF,它組合了分佈的引用計算和端點/物件標識。CORBA和DCOM都提供了在網路上尋找物件端點的高階機制,但最終這些機制都對映回到了IORs或OBJREFs。圖3是表示一個 IOR/OBJREF 怎樣與在IIOP/DCOM請求訊息中的定址資訊關聯起來的。
目前的技術存在的問題?
儘管DCOM和IIOP都是固定的協議,業界還沒有完全轉向其中任何一個協議。沒有融合的部分原因是文化的問題所致。而且在當一些組織試圖示準化一個或另一個協議的時候,兩個協議的技術適用性就被提出質疑。傳統上認為DCOM和CORBA都是合理伺服器到伺服器端的通訊協議。但是,二者對客戶到伺服器端的通訊都存在明顯的弱點,尤其是客戶機被散佈在Internet上的時候。
DCOM 和 CORBA/IIOP都是依賴於單個廠商的解決方案來最大優勢地使用協議。儘管兩個協議都在各種平臺和產品上被實現了,但現實是選定的釋出需要採用單一廠商的實現。在DCOM的情況下,這意味著每個機器要執行在。(儘管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連線技術對的偏愛所致。儘管一些廠商如, Iona和Visigenic都已經建立了通道技術,但這些產品很容易對配置錯誤敏感而且它們是不可互動的。
在一個伺服器群落中這些問題並不能影響DCOM或IIOP的使用。因為在伺服器群落中主機的數量很少(一般是成百上千,而不是成千上萬),這就抵消了DCOM基於的生命週期管理的成本。在伺服器群落中,所有主機被一個公共管理域管理的機率很大,使得統一的配置變得可能。相對少量的機器也能保持商業ORB產品可控制使用的成本,因為只需要更少量的ORB許可權。如果只有IIOP在伺服器群落中被使用,就只需要少量的ORB許可權。最後,在伺服器群落中所有主機有直接的IP連線也是可能的,這就消除了與防火牆相關的DCOM和 IIOP問題。
 
HTTP作為一個更好的RPC
在伺服器群落中使用DCOM 和CORBA 是通用的做法,但客戶機則使用HTTP進入伺服器群落。HTTP與RPC的協議很相似,它簡單、配置廣泛,並且對防火牆比其它協議更容易發揮作用。HTTP請求一般由Web伺服器軟體(如IIS和Apache)來處理,但越來越多的應用伺服器產品正在支援HTTP作為除DCOM和IIOP外的又一個協議。
象DCOM和IIOP一樣,HTTP層透過進行請求/響應通訊。一個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工具(如)來診斷HTTP問題變得更容易。HTTP基於文字的屬性也使得HTTP更容易適用於在Web開發中流行的低技術水平的程式設計環境。
HTTP請求的第一行包含三個元件:HTTP方法,請求-URI,協議版本。在前面的例子中,這些分別對應於POST, /foobar, 和 HTTP/1.1。Internet工程任務組(IETF)已經標準化了數量固定的HTTP方法。GET是HTTP用來訪問Web的方法。 POST是建立應用程式的最常用的HTTP方法。和GET不一樣,POST允許任意資料從客戶端傳送到伺服器端。請求URI (UnifoRe ntifier)是一個HTTP伺服器端軟體,它用來識別請求的目標的簡單的識別符號(它更象一個IIOP/GIOP object_key 或一個DCOM IPID)。關於URIs更多的資訊請參照"URIs, URLs, and URNs"。在這個例子中協議的版本是HTTP/1.1, 它表示遵守 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:
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
int main(int argc, char **argv) {
char buf[4096];
int cb = read(0, buf, sizeof(buf));
buf[cb] = 0;
strrev(buf);
printf("200 OKrn");p>
printf("Content-Type: text/plainrn");
printf("Content-Length: %drn", cb);
printf("rn");
printf(buf);
return 0;
圖5表示了一個更流行的版本,伺服器的實現是用Java ,以避免CGI的每個請求一個程式的開銷。
一般來說CGI是花費代價最小的寫HTTP伺服器端程式碼的方法。實際上,每一個HTTP伺服器端產品都提供了一個更有效的機制來讓你的程式碼處理一個HTTP請求。IIS提供了和ISAPI作為寫HTTP程式碼的機制。Apache允許你用執行在Apache後臺程式中的 C或Perl來寫模組。大多數應用伺服器軟體允許你寫Java servlet,COM元件, 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文件:

Hello, World

This is a comment!!


元素的名域URI是urn:schemas-develop-com:StringProcs。元素的名域URI是http://foo.com/documentation。第二個URI也是一個URL的事實是不重要的。在這兩種情況下,URI簡單地被用來消除元素和任何碰巧有同樣標記名的其它元素間的歧義。
為了方便,XML允許名域URIs被對映為區域性唯一的字首。這意味著下面的XML文件在語義上等同於上面的文件:
xmlns:sp="urn:schemas-develop-com:StringProcs"
xmlns:doc='http://foo.com/documentation'
>
Hello, World

This is a comment!!


後面的形式對作者來說更容易,尤其是如果有許多名域URIs在使用時。
XML也支援帶型別的資料表達。正在推出的XML Schema規範為描述XML資料型別標準化了一個詞彙集。下面是一個元素的XML Schema的描述:
xmlns=''
targetNamespace='urn:schemas-develop-com:StringProcs'
>







這個XML Schema定義闡述了XML名域urn:schemas-develop-com:StringProcs包含了一個名為的元素,這個元素包含了一個名為string1的子元素(型別為string),它被0個或更多沒有指定的元素所遵守。
XML Schema 規範還定義了一組內建的原始資料型別和建立一個XML文件中元素的型別的機制。下面的XML文件用XML Schema型別屬性來把元素和型別名聯絡在一起:
xmlns='http://customer.is.king.com'
xmlns:xsd=''
>
Don Box
23.5

連線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 和元素內(後面會更多討論這兩個元素)。下面是一個最簡單的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



Hello, World



SOAPMethodName頭必須與下的第一個子元素相匹配,否則呼叫將被拒絕。這允許防火牆管理員在不解析XML的情況下有效地過濾對一個具體方法的呼叫。
SOAP響應的格式類似於請求格式。響應體包含方法的[out]和 [in,out]引數,這個方法被編碼為一個顯著的響應元素的子元素。這個元素的名字與請求的呼叫元素的名字相同,但以Response字尾來連線。下面是對前面的SOAP請求的SOAP響應:
200 OK
Content-Type: text/xml
Content-Length: 162



dlroW ,olleH



這裡響應元素被命名為reverseResponse,它是方法名緊跟Response字尾。要注意的是這裡是沒有SOAPMethodName HTTP頭的。這個頭只在請求訊息中需要,在響應訊息中並不需要。
圖6和圖7表明SOAP是怎樣與以前討論的ORPC協議相互對應的。讓許多SOAP新手困惑的是SOAP中沒有關於SOAP伺服器怎樣使用請求頭來分發請求的要求;這被留為一個實現上的細節。一些SOAP伺服器將對映請求-URIs到類名,並分派呼叫到靜態方法或到在請求持續期活的類的例項。其它SOAP伺服器則將請求-URIs對映到始終存活的物件,經常是用查詢字串來編碼一個用來定位在伺服器程式中的物件關鍵字。還有一些其它的SOAP伺服器用HTTP 來編碼一個物件關鍵字,這個關鍵字可被用來在每次方法請求中恢復物件的狀態。重要的是客戶對這些區別並不知道。客戶軟體只是簡單遵循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;
};
再給出下面的簡單的程式,它使用了以前定義的Ibank介面:
Dim bank as IBank
Dim amount as Single
Dim newBal as Single
Dim overdrawn as Boolean
amount = 100
Set bank = GetObject("soap:")
overdrawn = bank.withdraw(3512, amount, newBal)
你能夠想象底層的代理(可能是一個SOAP,DCOM,或IIOP代理)看上去象圖8中所表示的那樣。這裡,在傳送請求訊息之前,引數被序列化成為一個請求物件。同樣被響應訊息接收到的響應物件被反序列化為引數。一個類似的轉變同樣發生在呼叫的伺服器端。
當透過SOAP呼叫方法時,請求物件和響應物件被序列化成一種已知的格式。每個SOAP體是一個XML文件,它具有一個顯著的稱為的根元素。標記名由SOAP URI (urn:schemas-xmlsoap-org:soap.v1)來劃定範圍,所有SOAP專用的元素和屬性都是由這個URI來劃定範圍的。SOAP Envelope包含一個可選的

元素,緊跟一個必須的元素。元素也有一個顯著的根元素,它或者是一個請求物件或者是一個響應物件。下面是一個IBank::withdraw請求的編碼:
xmlns:soap='urn:schemas-xmlsoap-org:soap.v1'>

'urn:uuid:DEADF00D-BEAD-BEAD-BEAD-BAABAABAABAA'>
3512
100



 
下列響應訊息被編碼為:
xmlns:soap='urn:schemas-xmlsoap-org:soap.v1'>

'urn:uuid:DEADF00D-BEAD-BEAD-BEAD-BAABAABAABAA'>
0
5
true



注意[in, out]引數出現在兩個訊息中。
 
在檢查了請求和響應物件的格式後,你可能已經注意到序列化格式通常是:
;
field1value
field2value

在請求的情況下,型別是隱式的C風格的結構,它由對應方法中的[in]和[in, out]引數組成。對響應來說,型別也是隱式的C風格的結構,它由對應方法中的[out]和[in, out]引數組成。這種每個域對應一個子元素的風格有時被稱為元素正規格式(ENF)。一般情況下,SOAP只用XML特性來傳達描述包含在元素內容中資訊的註釋。
象DCOM和IIOP一樣,SOAP支援協議頭擴充套件。SOAP用可選的
元素來傳載被協議擴充套件所使用的資訊。如果客戶端的SOAP軟體包含要傳送頭資訊,原始的請求將可能如圖9所示。在這種情況下命名causality的頭將與請求一起序列化。收到請求後,伺服器端軟體能檢視頭的名域URI,並處理它識別出的頭擴充套件。這個頭擴充套件被 URI識別,並期待一個如下的物件:
struct causality {
UUID id;
};
在這種情況下的請求,如果頭元素的URI不能被識別,頭元素可以被安全地忽略。
但你不能安全的忽略所有的SOAP體中的頭元素。如果一個特定的SOAP頭對正確處理訊息是很關鍵的,這個頭元素能被用SOAP屬性mustUnderstand=’true’標記為必須的。這個屬性告訴接收者頭元素必須被識別並被處理以確保正確的使用。為了強迫前面causality頭成為一個必須的頭,訊息將被寫成如下形式:
xmlns:soap='urn:schemas-xmlsoap-org:soap.v1'>

soap:mustUnderstand='true'
xmlns="">
362099cc-aa46-bae2-5110-99aac9823bff




SOAP軟體遇到不能識別必須的頭元素情況時,必須拒絕這個訊息並出示一個錯誤。如果伺服器在一個SOAP請求中發現一個不能識別的必須的頭元素,它必須返回一個錯誤響應並且不傳送任何呼叫到目標物件。如果客戶端在一個SOAP請求中發現一個不能識別出的必須的頭元素,它必須向呼叫者返回一個執行時錯誤。(在COM情況下,這將對映為一個明顯的HRESULT)
SOAP資料型別
在SOAP訊息中,每個元素可能是一個SOAP結構元素,一個根元素,一個存取元素或一個獨立的元素。在SOAP中,soap:Envelope, soap:Body和 soap:Header 是唯一的三個結構元素。它們的基本關係由下列XML Schema所描述:
targetNamespace='urn:schemas-xmlsoap-org:soap.v1'>


minOccurs='0' />
minOccurs='1' />



在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訊息中被序列化的例項如下所示:
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'>
3514
100.0

在這個例子中,存取元素account和amount被稱著簡單存取元素,因為它們訪問對應於在 XML Schema規範 (見 ) 的Part 2中定義的原始資料型別的值。這個規範指定了字串,數值,日期等資料型別的名字和表達方式以及使用一個新的模式定義中的結構來定義新的原始型別的機制。
對引用簡單型別的存取元素,元素值被簡單地編碼為直接在存取元素下的字元資料,如上所示。對引用組合型別的存取元素(就是那些自身用子存取元素來構造的存取元素),有兩個技術來對存取元素進行編碼。最簡單的方法是把被結構化的值直接嵌入在存取元素下。考慮下面的Java類定義:
package com.bofsoap.IBank;
public class traner {
public adjustment from;
public adjustment to;
}
如果用嵌入值編碼存取元素,在SOAP中一個序列化的transfer物件如下所示:
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'
>

3514
-100.0


3518
100.0


在這種情況下,adjustment物件的值被直接編碼在它們的存取元素下。
在考慮組合存取元素時,需要說明幾個問題。先考慮上面的transfer類。類的from和to的域是物件引用,它可能為空。SOAP用XML Schemas的null屬性來表示空值或引用。下面例子表示一個序列化的transfer物件,它的from域是空的:
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'
xmlns:xsd='/instance'
>


3518
100.0


在不存在的情況下, xsd:null屬性的隱含值是false。給定元素的能否為空的屬性是由XML Schema定義來控制的。例如下列XML Schema將只允許from存取元素為空:

name='from'
type='adjustment'
nullable='true'
/>
name='to'
type='adjustment'
nullable='false'
/>

在一個元素的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物件的序列化形式如下所示:
xmlns:xsd=''
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'
>

3514
-100.0
3


3518
100.0


在這裡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物件的序列化形式如下所示:
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'>

3514
-100.0


3514
-100.0


這個表達有兩個問題。首先最容易理解的問題是同樣的資訊被髮送了兩次,這導致了一個比實際所需要訊息的更大的訊息。一個更微妙的但是更重要的問題是由於反序列化程式不能分辨兩個帶有同樣值的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物件如下所示:
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'>



這個編碼假設與adjustment類相容的一個型別的例項已經在envelope中的其它地方被序列化,而且這個例項已經被用soap:id屬性標記,如下所示:
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'>
3514
-100.0

對多引用存取元素,把程式碼段的識別符號(例如#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允許包含字串和二進值資料的存取元素是多引用存取元素的目標。這意味著下面的程式碼是合法的:


Hello, SOAP

儘管事實是存取元素2有一個soap:id屬性,它實際上是一個存取元素而不是獨立元素。
SOAP陣列
陣列被編碼為組合型別的一個特殊的例子。在SOAP中,一個陣列必須有一個秩(維數)和一個容量。一個陣列被編碼為一個組合型別,其中每一個陣列元素被編碼為一個子元素,這個子元素的名字是元素的名域限制的型別名。
假設有下面的COM IDL型別定義:
struct POINTLIST {
long cElems;
[size_is(cElems)] POINT points[];
};
 
這個型別的例項將被序列化為:

3

34
75
19


如果points域被標記為[ptr]屬性,這個編碼將用一個多引用存取元素,如下所示:
 

3



34
75
19

當把一個陣列編碼為一個獨立元素時,標記名是帶字首ArrayOf的型別名。
象NDR和CDR一樣,SOAP支援部分轉換的陣列。如果子元素的數量少於所宣告的容量,這些元素被假設正從陣列的末尾丟失。這能夠透過在正包含的陣列元素上使用soap:offset屬性來被忽略。
soap:offset='[1]'>
19

soap:offset屬性表示出現在陣列中的第一個元素的索引。在上面的例子中,元素0,2到4都是不被轉換的。SOAP也支援稀疏陣列,這是透過使用soap:position屬性來把每個元素用它的絕對索引來註釋而實現的:

34
45

在這個例子中,元素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的根元素的型別的例項。
targetNamespace='urn:schemas-xmlsoap-org:soap.v1'
>








 

faultcode存取元素必須包含一個用已知的整數表示的SOAP錯誤程式碼或者一個專門應用的名域限制的值。當前的SOAP 錯誤程式碼如圖12所示。Faultstring存取元素包含對發生的錯誤的可讀性的描述。runcode 存取元素包含一個字串,它的值必須是Yes, No或 Maybe,表明被請求的操作實際上是否在錯誤產生之前被執行。Detail存取元素是可選的,用於包含一個專門應用的異常物件。
下面是一個對應於一個包含無法識別的必須的頭元素的請求的SOAP錯誤的例子:
xmlns:soap='urn:schemas-xmlsoap-org:soap.v1'
>

;
200

Unrecognized 'causality' header

No



假設具體應用的錯誤需要被返回,你可能看到如圖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, , 或 等程式間的互操作性。
希望本文給了你一個對這個協議具體細節的更清晰的理解。我鼓勵你用SOAP進行實驗,或者試著使用SOAP使能的系統之一(列在http://www.develop.com/soap/),或者自己做一些工作。我本人發現採用指令碼語言(),使一個基本的SOAP客戶與伺服器建立並執行只花費了不到一個小時。針對你對HTTP和XML的熟悉程度,以及你的目標平臺的成熟度,你所花費的時間會有所不同。

的名域URI是http://foo.com/documentation。第二個URI也是一個URL的事實是不重要的。在這兩種情況下,URI簡單地被用來消除元素和任何碰巧有同樣標記名的其它元素間的歧義。 為了方便,XML允許名域URIs被對映為區域性唯一的字首。這意味著下面的XML文件在語義上等同於上面的文件: Hello, World This is a comment!! 後面的形式對作者來說更容易,尤其是如果有許多名域URIs在使用時。 XML也支援帶型別的資料表達。正在推出的XML Schema規範為描述XML資料型別標準化了一個詞彙集。下面是一個元素的XML Schema的描述: 這個XML Schema定義闡述了XML名域urn:schemas-develop-com:StringProcs包含了一個名為的元素,這個元素包含了一個名為string1的子元素(型別為string),它被0個或更多沒有指定的元素所遵守。 XML Schema 規範還定義了一組內建的原始資料型別和建立一個XML文件中元素的型別的機制。下面的XML文件用XML Schema型別屬性來把元素和型別名聯絡在一起: Don Box 23.5 連線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 和元素內(後面會更多討論這兩個元素)。下面是一個最簡單的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 Hello, World SOAPMethodName頭必須與下的第一個子元素相匹配,否則呼叫將被拒絕。這允許防火牆管理員在不解析XML的情況下有效地過濾對一個具體方法的呼叫。 SOAP響應的格式類似於請求格式。響應體包含方法的[out]和 [in,out]引數,這個方法被編碼為一個顯著的響應元素的子元素。這個元素的名字與請求的呼叫元素的名字相同,但以Response字尾來連線。下面是對前面的SOAP請求的SOAP響應: 200 OK Content-Type: text/xml Content-Length: 162 dlroW ,olleH 這裡響應元素被命名為reverseResponse,它是方法名緊跟Response字尾。要注意的是這裡是沒有SOAPMethodName HTTP頭的。這個頭只在請求訊息中需要,在響應訊息中並不需要。 圖6和圖7表明SOAP是怎樣與以前討論的ORPC協議相互對應的。讓許多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:") overdrawn = bank.withdraw(3512, amount, newBal) 你能夠想象底層的代理(可能是一個SOAP,DCOM,或IIOP代理)看上去象圖8中所表示的那樣。這裡,在傳送請求訊息之前,引數被序列化成為一個請求物件。同樣被響應訊息接收到的響應物件被反序列化為引數。一個類似的轉變同樣發生在呼叫的伺服器端。 當透過SOAP呼叫方法時,請求物件和響應物件被序列化成一種已知的格式。每個SOAP體是一個XML文件,它具有一個顯著的稱為的根元素。標記名由SOAP URI (urn:schemas-xmlsoap-org:soap.v1)來劃定範圍,所有SOAP專用的元素和屬性都是由這個URI來劃定範圍的。SOAP Envelope包含一個可選的
元素,緊跟一個必須的元素。元素也有一個顯著的根元素,它或者是一個請求物件或者是一個響應物件。下面是一個IBank::withdraw請求的編碼: 3512 100   下列響應訊息被編碼為: 0 5 true 注意[in, out]引數出現在兩個訊息中。   在檢查了請求和響應物件的格式後,你可能已經注意到序列化格式通常是: ; field1value field2value 在請求的情況下,型別是隱式的C風格的結構,它由對應方法中的[in]和[in, out]引數組成。對響應來說,型別也是隱式的C風格的結構,它由對應方法中的[out]和[in, out]引數組成。這種每個域對應一個子元素的風格有時被稱為元素正規格式(ENF)。一般情況下,SOAP只用XML特性來傳達描述包含在元素內容中資訊的註釋。 象DCOM和IIOP一樣,SOAP支援協議頭擴充套件。SOAP用可選的
元素來傳載被協議擴充套件所使用的資訊。如果客戶端的SOAP軟體包含要傳送頭資訊,原始的請求將可能如圖9所示。在這種情況下命名causality的頭將與請求一起序列化。收到請求後,伺服器端軟體能檢視頭的名域URI,並處理它識別出的頭擴充套件。這個頭擴充套件被 URI識別,並期待一個如下的物件: struct causality { UUID id; }; 在這種情況下的請求,如果頭元素的URI不能被識別,頭元素可以被安全地忽略。 但你不能安全的忽略所有的SOAP體中的頭元素。如果一個特定的SOAP頭對正確處理訊息是很關鍵的,這個頭元素能被用SOAP屬性mustUnderstand=’true’標記為必須的。這個屬性告訴接收者頭元素必須被識別並被處理以確保正確的使用。為了強迫前面causality頭成為一個必須的頭,訊息將被寫成如下形式: 362099cc-aa46-bae2-5110-99aac9823bff SOAP軟體遇到不能識別必須的頭元素情況時,必須拒絕這個訊息並出示一個錯誤。如果伺服器在一個SOAP請求中發現一個不能識別的必須的頭元素,它必須返回一個錯誤響應並且不傳送任何呼叫到目標物件。如果客戶端在一個SOAP請求中發現一個不能識別出的必須的頭元素,它必須向呼叫者返回一個執行時錯誤。(在COM情況下,這將對映為一個明顯的HRESULT) SOAP資料型別 在SOAP訊息中,每個元素可能是一個SOAP結構元素,一個根元素,一個存取元素或一個獨立的元素。在SOAP中,soap:Envelope, soap:Body和 soap:Header 是唯一的三個結構元素。它們的基本關係由下列XML 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訊息中被序列化的例項如下所示: 3514 100.0 在這個例子中,存取元素account和amount被稱著簡單存取元素,因為它們訪問對應於在W3C XML Schema規範 (見 ) 的Part 2中定義的原始資料型別的值。這個規範指定了字串,數值,日期等資料型別的名字和表達方式以及使用一個新的模式定義中的結構來定義新的原始型別的機制。 對引用簡單型別的存取元素,元素值被簡單地編碼為直接在存取元素下的字元資料,如上所示。對引用組合型別的存取元素(就是那些自身用子存取元素來構造的存取元素),有兩個技術來對存取元素進行編碼。最簡單的方法是把被結構化的值直接嵌入在存取元素下。考慮下面的Java類定義: package com.bofsoap.IBank; public class transfer { public adjustment from; public adjustment to; } 如果用嵌入值編碼存取元素,在SOAP中一個序列化的transfer物件如下所示: 3514 -100.0 3518 100.0 在這種情況下,adjustment物件的值被直接編碼在它們的存取元素下。 在考慮組合存取元素時,需要說明幾個問題。先考慮上面的transfer類。類的from和to的域是物件引用,它可能為空。SOAP用XML Schemas的null屬性來表示空值或引用。下面例子表示一個序列化的transfer物件,它的from域是空的: 3518 100.0 在不存在的情況下, xsd:null屬性的隱含值是false。給定元素的能否為空的屬性是由XML Schema定義來控制的。例如下列XML Schema將只允許from存取元素為空: /> 在一個元素的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物件的序列化形式如下所示: 3514 -100.0 3 3518 100.0 在這裡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物件的序列化形式如下所示: 3514 -100.0 3514 -100.0 這個表達有兩個問題。首先最容易理解的問題是同樣的資訊被髮送了兩次,這導致了一個比實際所需要訊息的更大的訊息。一個更微妙的但是更重要的問題是由於反序列化程式不能分辨兩個帶有同樣值的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物件如下所示: 這個編碼假設與adjustment類相容的一個型別的例項已經在envelope中的其它地方被序列化,而且這個例項已經被用soap:id屬性標記,如下所示: 3514 -100.0 對多引用存取元素,把程式碼段的識別符號(例如#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允許包含字串和二進值資料的存取元素是多引用存取元素的目標。這意味著下面的程式碼是合法的: Hello, SOAP 儘管事實是存取元素2有一個soap:id屬性,它實際上是一個存取元素而不是獨立元素。 SOAP陣列 陣列被編碼為組合型別的一個特殊的例子。在SOAP中,一個陣列必須有一個秩(維數)和一個容量。一個陣列被編碼為一個組合型別,其中每一個陣列元素被編碼為一個子元素,這個子元素的名字是元素的名域限制的型別名。 假設有下面的COM IDL型別定義: struct POINTLIST { long cElems; [size_is(cElems)] POINT points[]; };   這個型別的例項將被序列化為: 3

34

75

19 如果points域被標記為[ptr]屬性,這個編碼將用一個多引用存取元素,如下所示:   3

34

75

19

當把一個陣列編碼為一個獨立元素時,標記名是帶字首ArrayOf的型別名。 象NDR和CDR一樣,SOAP支援部分轉換的陣列。如果子元素的數量少於所宣告的容量,這些元素被假設正從陣列的末尾丟失。這能夠透過在正包含的陣列元素上使用soap:offset屬性來被忽略。

19

soap:offset屬性表示出現在陣列中的第一個元素的索引。在上面的例子中,元素0,2到4都是不被轉換的。SOAP也支援稀疏陣列,這是透過使用soap:position屬性來把每個元素用它的絕對索引來註釋而實現的:

34

45

在這個例子中,元素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的根元素的型別的例項。   faultcode存取元素必須包含一個用已知的整數表示的SOAP錯誤程式碼或者一個專門應用的名域限制的值。當前的SOAP 錯誤程式碼如圖12所示。Faultstring存取元素包含對發生的錯誤的可讀性的描述。runcode 存取元素包含一個字串,它的值必須是Yes, No或 Maybe,表明被請求的操作實際上是否在錯誤產生之前被執行。Detail存取元素是可選的,用於包含一個專門應用的異常物件。 下面是一個對應於一個包含無法識別的必須的頭元素的請求的SOAP錯誤的例子: ; 200 Unrecognized 'causality' header No 假設具體應用的錯誤需要被返回,你可能看到如圖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的熟悉程度,以及你的目標平臺的成熟度,你所花費的時間會有所不同。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-990977/,如需轉載,請註明出處,否則將追究法律責任。

相關文章