1. 程式人生 > >Web 服務程式設計,REST 與 SOAP

Web 服務程式設計,REST 與 SOAP

REST 簡介

在開始我們的正式討論之前,讓我們簡單看一下 REST 的定義。

REST(Representational State Transfer)是 Roy Fielding 提出的一個描述互聯絡統架構風格的名詞。為什麼稱為 REST?Web 本質上由各種各樣的資源組成,資源由 URI 唯一標識。瀏覽器(或者任何其它類似於瀏覽器的應用程式)將展示出該資源的一種表現方式,或者一種表現狀態。如果使用者在該頁面中定向到指向其它資源的連結,則將訪問該資源,並表現出它的狀態。這意味著客戶端應用程式隨著每個資源表現狀態的不同而發生狀態轉移,也即所謂 REST。

關於 REST 本身,本文就不再這裡過多地討論,讀者可以參考 developerWorks 上其它介紹 REST 的文章。本文的重點在於通過 REST 與 SOAP Web 服務的對比,幫助讀者更深刻理解 REST 架構風格的特點,優勢。

應用場景介紹(線上使用者管理)

本文將藉助於一個應用場景,通過基於 REST 和 SOAP Web 服務的不同實現,來對兩者進行對比。該應用場景的業務邏輯會盡量保持簡單且易於理解,以有助於把我們的重心放在 REST 和 SOAP Web 服務技術特質對比上。

需求描述

這是一個線上的使用者管理模組,負責使用者資訊的建立,修改,刪除,查詢。使用者的資訊主要包括:

  • 使用者名稱(唯一標誌在系統中的使用者)
  • 頭銜
  • 公司
  • EMAIL
  • 描述

需求用例圖如下:

圖 1. 需求用例圖


如圖 1 所示,客戶端 1(Client1)與客戶端 2(Client2)對於資訊的存取具有不同的許可權,客戶端 1 可以執行所有的操作,而客戶端 2 只被允許執行使用者查詢(Query User)與使用者列表查詢(Query User List)。關於這一點,我們在對 REST Web 服務與 SOAP Web 服務安全控制對比時會具體談到。下面我們將分別向您介紹如何使用 REST 和 SOAP 架構實現 Web 服務。

使用 REST 實現 Web 服務

本部分將基於 Restlet 框架來實現該應用。Restlet 為那些要採用 REST 結構體系來構建應用程式的 Java 開發者提供了一個具體的解決方案。關於更多的 Restlet 相關內容,本文不做深入討論,請見參考資源列表。

設計

我們將採用遵循 REST 設計原則的 ROA(Resource-Oriented Architecture,面向資源的體系架構)進行設計。ROA 是什麼?簡單點說,ROA 是一種把實際問題轉換成 REST 式 Web 服務的方法,它使得 URI、HTTP 和 XML 具有跟其他 Web 應用一樣的工作方式。

在使用 ROA 進行設計時,我們需要把真實的應用需求轉化成 ROA 中的資源,基本上遵循以下的步驟:

  • 分析應用需求中的資料集。
  • 對映資料集到 ROA 中的資源。
  • 對於每一資源,命名它的 URI。
  • 為每一資源設計其 Representations。
  • 用 hypermedia links 表述資源間的聯絡。

接下來我們按照以上的步驟來設計本文的應用案例。

線上使用者管理所涉及的資料集就是使用者資訊,如果對映到 ROA 資源,主要包括兩類資源:使用者及使用者列表。使用者資源的 URI 用http://localhost:8182/v1/users/{username} 表示,使用者列表資源的 URI 用 http://localhost:8182/v1/users 表示。它們的 Representation 如下,它們都採用瞭如清單 1 和清單 2 所示的 XML 表述方式。

清單 1. 使用者列表資源 Representation
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<users>
	<user>
			<name>tester</name>
			<link>http://localhost:8182/v1/users/tester</link>
	</user>
	<user>
			<name>tester1</name>
			<link>http://localhost:8182/v1/users/tester1</link>
	</user>
</users>

清單 2. 使用者資源 Representation
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<user>
	<name>tester</name>
	<title>software engineer</title>
	<company>IBM</company>
	<email>[email protected]</email>
	<description>testing!</description>
</user>

客戶端通過 User List Resource 提供的 LINK 資訊 ( 如 : <link>http://localhost:8182/v1/users/tester</link>) 獲得具體的某個 USER Resource。

Restful Web 服務架構

首先給出 Web 服務使用 REST 風格實現的整體架構圖,如下圖所示:

圖 2. REST 實現架構
REST

接下來,我們將基於該架構,使用 Restlet 給出應用的 RESTful Web 服務實現。

下面的章節中,我們將給出 REST Web 服務實現的核心程式碼片段。關於完整的程式碼清單,讀者可以通過資源列表下載。

客戶端實現

清單 3 給出的是客戶端的核心實現部分,其主要由四部分組成:使用 HTTP PUT 增加、修改使用者資源,使用 HTTP GET 得到某一具體使用者資源,使用 HTTP DELETE 刪除使用者資源,使用 HTTP GET 得到使用者列表資源。而這四部分也正對應了圖 2 關於架構描述的四對 HTTP 訊息來回。關於 UserRestHelper 類的完整實現,請讀者參見本文所附的程式碼示例。

清單 3. 客戶端實現
public class UserRestHelper {
//The root URI of our ROA implementation.
public static final tring APPLICATION_URI = "http://localhost:8182/v1";

//Get the URI of user resource by user name. 
private static String getUserUri(String name) {
	return APPLICATION_URI + "/users/" + name;
}

//Get the URI of user list resource.
private static String getUsersUri() {
	return APPLICATION_URI + "/users";
}
//Delete user resource from server by user name.
//使用 HTTP DELETE 方法經由 URI 刪除使用者資源
public static void deleteFromServer(String name) {
	Response response = new Client(Protocol.HTTP).delete(getUserUri(name));
	…… 
}
//Put user resource to server.
//使用 HTTP PUT 方法經由 URI 增加或者修改使用者資源
public static void putToServer(User user) {
	//Fill FORM using user data.
	Form form = new Form();
 	form.add("user[title]", user.getTitle());
 	form.add("user[company]", user.getCompany());
 	form.add("user[email]", user.getEmail());
 	form.add("user[description]", user.getDescription());
	Response putResponse = new Client(Protocol.HTTP).put(
	getUserUri(user.getName()), form.getWebRepresentation());
 	……
}
//Output user resource to console.
public static void printUser(String name) {
	printUserByURI(getUserUri(name));
}

//Output user list resource to console.
//使用 HTTP GET 方法經由 URI 顯示使用者列表資源
public static void printUserList() {
	Response getResponse = new Client(Protocol.HTTP).get(getUsersUri());
	if (getResponse.getStatus().isSuccess()) { 
			DomRepresentation result = getResponse.getEntityAsDom();
 //The following code line will explore this XML document and output
 //each user resource to console.
			……
	} else { 
	 	System.out.println("Unexpected status:"+ getResponse.getStatus()); 
	}
}

//Output user resource to console.
//使用 HTTP GET 方法經由 URI 顯示使用者資源
private static void printUserByURI(String uri) { 
	Response getResponse = new Client(Protocol.HTTP).get(uri);
	if (getResponse.getStatus().isSuccess()) { 
 		DomRepresentation result = getResponse.getEntityAsDom();
 		//The following code line will explore this XML document and output
 //current user resource to console.
 ……
 	} else { 
 		System.out.println("unexpected status:"+ getResponse.getStatus()); 
 	}
}
}

伺服器端實現

清單 4 給出的是伺服器端對於使用者資源類(UserResourc)的實現,其核心的功能是響應有關使用者資源的 HTTP GET/PUT/DELETE 請求,而這些請求響應邏輯正對應了 UserRestHelper 類中關於使用者資源類的 HTTP 請求。

清單 4. 伺服器端實現
public class UserResource extends Resource {
private User _user;
private String _userName;
public UserResource(Context context, Request request, Response response) {
//Constructor is here.
……
}
//響應 HTTP DELETE 請求邏輯
public void delete() {
	// Remove the user from container.
	getContainer().remove(_userName);
 	getResponse().setStatus(Status.SUCCESS_OK);
}

//This method will be called by handleGet.
public Representation getRepresentation(Variant variant) {
 Representation result = null;
 if (variant.getMediaType().equals(MediaType.TEXT_XML)) {	
 	Document doc = createDocument(this._user);
 	result = new DomRepresentation(MediaType.TEXT_XML, doc);
 }
 return result;
}
//響應 HTTP PUT 請求邏輯。
public void put(Representation entity) {
 if (getUser() == null) {
 //The user doesn't exist, create it
 setUser(new User());
 getUser().setName(this._userName);
 getResponse().setStatus(Status.SUCCESS_CREATED);
 } else {
 	getResponse().setStatus(Status.SUCCESS_NO_CONTENT);
 }
 //Parse the entity as a Web form.
 Form form = new Form(entity);
 getUser().setTitle(form.getFirstValue("user[title]"));
 getUser().setCompany(form.getFirstValue("user[company]"));
 getUser().setEmail(form.getFirstValue("user[email]"));
 getUser().setDescription(form.getFirstValue("user[description]")); 
 //Put the user to the container.
	getApplication().getContainer().put(_userName, getUser()); 
}
//響應 HTTP GET 請求邏輯。
public void handleGet() {
	super.handleGet();
	if(this._user != null ) {
	    getResponse().setEntity(getRepresentation(
	               new Variant(MediaType.TEXT_XML)));
	    getResponse().setStatus(Status.SUCCESS_OK);	
	} else {
		getResponse().setStatus(Status.CLIENT_ERROR_NOT_FOUND);		
	}
}
//build XML document for user resource.
private Document createDocument(User user) {
 //The following code line will create XML document according to user info. 
	……
}
//The remaining methods here
……
}

UserResource 類是對使用者資源類的抽象,包括了對該資源的建立修改(put 方法),讀取(handleGet 方法 )和刪除(delete 方法),被創建出來的 UserResource 類例項被 Restlet 框架所託管,所有操縱資源的方法會在相應的 HTTP 請求到達後被自動回撥。

另外,在服務端,還需要實現代表使用者列表資源的資源類 UserListResource,它的實現與 UserResource 類似,響應 HTTP GET 請求,讀取當前系統內的所有使用者資訊,形成如清單 1 所示的使用者列表資源 Representation,然後返回該結果給客戶端。具體的實現請讀者參見本文所附的程式碼示例。

使用 SOAP 實現 Web 服務

本文對於 SOAP 實現,就不再像 REST 那樣,具體到程式碼級別的實現。本節將主要通過 URI,HTTP 和 XML 來巨集觀上表述 SOAP Web 服務實現的技術本質,為下一節 REST Web 服務與 SOAP Web 服務的對比做鋪墊。

SOAP Web 服務架構

同樣,首先給出 SOAP 實現的整體架構圖,如下圖所示:

圖 3. SOAP 實現架構
REST

可以看到,與 REST 架構相比,SOAP 架構圖明顯不同的是:所有的 SOAP 訊息傳送都使用 HTTP POST 方法,並且所有 SOAP 訊息的 URI 都是一樣的,這是基於 SOAP 的 Web 服務的基本實踐特徵。

獲得使用者資訊列表

基於 SOAP 的客戶端建立如清單 5 所示的 SOAP XML 文件,它通過類 RPC 方式來獲得使用者列表資訊。

清單 5. getUserList SOAP 訊息
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
	<soap:Body>
		<p:getUserList xmlns:p="http://www.exmaple.com"/>
	</soap:Body>
</soap:Envelope>

客戶端將使用 HTTP 的 POST 方法,將上述的 SOAP 訊息傳送至 http://localhost:8182/v1/soap/servlet/messagerouter URI,SOAP SERVER 收到該 HTTP POST 請求,通過解碼 SOAP 訊息確定需要呼叫 getUserList 方法完成該 WEB 服務呼叫,返回如下的響應:

清單 6. getUserListResponse 訊息
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
	<soap:Body>
			<p:get
				UserListResponse xmlns:p="http://www.exmaple.com">
				<Users>
				<username>tester<username>
				<username>tester1<username>
				......
				</Users>
				<p: getUserListResponse >
	</soap:Body>
</soap:Envelope>

獲得某一具體使用者資訊

清單 7. getUserByName SOAP 訊息
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
	<soap:Body>
	 <p:getUserByName xmlns:p="http://www.exmaple.com">
				<username>tester</username>
				</p:getUserByName >
	</soap:Body>
</soap:Envelope>

同樣地,客戶端將使用 HTTP 的 POST 方法,將上述的 SOAP 訊息傳送至 http://localhost:8182/v1/soap/servlet/messagerouterURI,SOAP SERVER 處理後返回的 Response 如下:

清單 8. getUserByNameResponse SOAP 訊息
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
	<p:getUserByNameResponse xmlns:p="http://www.exmaple.com">
			<name>tester</name>
			<title>software engineer</title>
			<company>IBM</company>
			<email>[email protected]</email>
			<description>testing!</description>
	</p:getUserByNameResponse>
</soap:Body>
</soap:Envelope>

實際上,建立新的使用者,過程也比較類似,在這裡,就不一一列出,因為這兩個例子對於本文在選定的點上對比 REST 與 SOAP 已經足夠了。

REST 與 SOAP 比較

本節從以下幾個方面來對比上面兩節給出 REST 實現與 SOAP 實現。

介面抽象

RESTful Web 服務使用標準的 HTTP 方法 (GET/PUT/POST/DELETE) 來抽象所有 Web 系統的服務能力,而不同的是,SOAP 應用都通過定義自己個性化的介面方法來抽象 Web 服務,這更像我們經常談到的 RPC。例如本例中的 getUserList 與 getUserByName 方法。

RESTful Web 服務使用標準的 HTTP 方法優勢,從大的方面來講:標準化的 HTTP 操作方法,結合其他的標準化技術,如 URI,HTML,XML 等,將會極大提高系統與系統之間整合的互操作能力。尤其在 Web 應用領域,RESTful Web 服務所表達的這種抽象能力更加貼近 Web 本身的工作方式,也更加自然。

同時,使用標準 HTTP 方法實現的 RRESTful Web 服務也帶來了 HTTP 方法本身的一些優勢:

  • 無狀態性(Stateless)

HTTP 協議從本質上說是一種無狀態的協議,客戶端發出的 HTTP 請求之間可以相互隔離,不存在相互的狀態依賴。基於 HTTP 的 ROA,以非常自然的方式來實現無狀態服務請求處理邏輯。對於分散式的應用而言,任意給定的兩個服務請求 Request 1 與 Request 2, 由於它們之間並沒有相互之間的狀態依賴,就不需要對它們進行相互協作處理,其結果是:Request 1 與 Request 2 可以在任何的伺服器上執行,這樣的應用很容易在伺服器端支援負載平衡 (load-balance)。

  • 安全操作與冪指相等特性(Safety /Idempotence)

HTTP 的 GET、HEAD 請求本質上應該是安全的呼叫,即:GET、HEAD 呼叫不會有任何的副作用,不會造成伺服器端狀態的改變。對於伺服器來說,客戶端對某一 URI 做 n 次的 GET、HAED 呼叫,其狀態與沒有做呼叫是一樣的,不會發生任何的改變。

HTTP 的 PUT、DELTE 呼叫,具有冪指相等特性 , 即:客戶端對某一 URI 做 n 次的 PUT、DELTE 呼叫,其效果與做一次的呼叫是一樣的。HTTP 的 GET、HEAD 方法也具有冪指相等特性。

HTTP 這些標準方法在原則上保證你的分散式系統具有這些特性,以幫助構建更加健壯的分散式系統。

安全控制

為了說明問題,基於上面的線上使用者管理系統,我們給定以下場景:

參考一開始我們給出的用例圖,對於客戶端 Client2,我們只希望它能以只讀的方式訪問 User 和 User List 資源,而 Client1 具有訪問所有資源的所有許可權。

如何做這樣的安全控制?

通行的做法是:所有從客戶端 Client2 發出的 HTTP 請求都經過代理伺服器 (Proxy Server)。代理伺服器制定安全策略:所有經過該代理的訪問 User 和 User List 資源的請求只具有讀取許可權,即:允許 GET/HEAD 操作,而像具有寫許可權的 PUT/DELTE 是不被允許的。

如果對於 REST,我們看看這樣的安全策略是如何部署的。如下圖所示:

圖 4. REST 與代理伺服器 (Proxy Servers)
REST

一般代理伺服器的實現根據 (URI, HTTP Method) 兩元組來決定 HTTP 請求的安全合法性。

當發現類似於(http://localhost:8182/v1/users/{username},DELETE)這樣的請求時,予以拒絕。

對於 SOAP,如果我們想借助於既有的代理伺服器進行安全控制,會比較尷尬,如下圖:

圖 5. SOAP 與代理伺服器 (Proxy Servers)
REST

所有的 SOAP 訊息經過代理伺服器,只能看到(http://localhost:8182/v1/soap/servlet/messagerouter, HTTP POST)這樣的資訊,如果代理伺服器想知道當前的 HTTP 請求具體做的是什麼,必須對 SOAP 的訊息體解碼,這樣的話,意味著要求第三方的代理伺服器需要理解當前的 SOAP 訊息語義,而這種 SOAP 應用與代理伺服器之間的緊耦合關係是不合理的。

關於快取

眾所周知,對於基於網路的分散式應用,網路傳輸是一個影響應用效能的重要因素。如何使用快取來節省網路傳輸帶來的開銷,這是每一個構建分散式網路應用的開發人員必須考慮的問題。

HTTP 協議帶條件的 HTTP GET 請求 (Conditional GET) 被設計用來節省客戶端與伺服器之間網路傳輸帶來的開銷,這也給客戶端實現 Cache 機制 ( 包括在客戶端與伺服器之間的任何代理 ) 提供了可能。HTTP 協議通過 HTTP HEADER 域:If-Modified-Since/Last- Modified,If-None-Match/ETag 實現帶條件的 GET 請求。

REST 的應用可以充分地挖掘 HTTP 協議對快取支援的能力。當客戶端第一次傳送 HTTP GET 請求給伺服器獲得內容後,該內容可能被快取伺服器 (Cache Server) 快取。當下一次客戶端請求同樣的資源時,快取可以直接給出響應,而不需要請求遠端的伺服器獲得。而這一切對客戶端來說都是透明的。

圖 6. REST 與快取伺服器 (Cache Server)
REST

而對於 SOAP,情況又是怎樣的呢?

使用 HTTP 協議的 SOAP,由於其設計原則上並不像 REST 那樣強調與 Web 的工作方式相一致,所以,基於 SOAP 應用很難充分發揮 HTTP 本身的快取能力。

圖 7. SOAP 與快取伺服器 (Cache Server)
REST

兩個因素決定了基於 SOAP 應用的快取機制要遠比 REST 複雜:

其一、所有經過快取伺服器的 SOAP 訊息總是 HTTP POST,快取伺服器如果不解碼 SOAP 訊息體,沒法知道該 HTTP 請求是否是想從伺服器獲得資料。

其二、SOAP 訊息所使用的 URI 總是指向 SOAP 的伺服器,如本文例子中的 http://localhost:8182/v1/soap/servlet/messagerouter,這並沒有表達真實的資源 URI,其結果是快取伺服器根本不知道那個資源正在被請求,更不用談進行快取處理。

關於連線性

在一個純的 SOAP 應用中,URI 本質上除了用來指示 SOAP 伺服器外,本身沒有任何意義。與 REST 的不同的是,無法通過 URI 驅動 SOAP 方法呼叫。例如在我們的例子中,當我們通過

getUserList SOAP 訊息獲得所有的使用者列表後,仍然無法通過既有的資訊得到某個具體的使用者資訊。唯一的方法只有通過 WSDL 的指示,通過呼叫 getUserByName 獲得,getUserList 與 getUserByName 是彼此孤立的。

而對於 REST,情況是完全不同的:通過 http://localhost:8182/v1/users URI 獲得使用者列表,然後再通過使用者列表中所提供的 LINK 屬性,例如 <link>http://localhost:8182/v1/users/tester</link>獲得 tester 使用者的使用者資訊。這樣的工作方式,非常類似於你在瀏覽器的某個頁面上點選某個 hyperlink, 瀏覽器幫你自動定向到你想訪問的頁面,並不依賴任何第三方的資訊。

總結

典型的基於 SOAP 的 Web 服務以操作為中心,每個操作接受 XML 文件作為輸入,提供 XML 文件作為輸出。在本質上講,它們是 RPC 風格的。而在遵循 REST 原則的 ROA 應用中,服務是以資源為中心的,對每個資源的操作都是標準化的 HTTP 方法。

本文主要集中在以上的幾個方面,對 SOAP 與 REST 進行了對比,可以看到,基於 REST 構建的系統其系統的擴充套件能力要強於 SOAP,這可以體現在它的統一介面抽象、代理伺服器支援、快取伺服器支援等諸多方面。並且,伴隨著 Web Site as Web Services 演進的趨勢,基於 REST 設計和實現的簡單性和強擴充套件性,有理由相信,REST 將會成為 Web 服務的一個重要架構實踐領域。


原文地址:https://www.ibm.com/developerworks/cn/webservices/0907_rest_soap/