REST and RESTfull web service
過去,我提到 REST 這個字眼時,多半指的是一種常用的 Web-based 應用軟體設計慣例或樣式 (我個人偏好用"慣例"一詞,不過用"樣式/pattern"好像比較專業)。既然是慣例,那在設計和使用上就比較隨興。不過隨著 REST 樣式的大量應用,有愈來愈多案例開始使用更制式化的設計樣式,這些高度制式化的 REST 服務,就稱之為 "RESTful web service"。 "-ful" 這個字尾正是在強調它們的設計方式完全符合 REST 文獻的建議內容。
相對於 RESTful ,以往那種基於慣例與相容性的實作方式,有人就稱為 RPC 。不過說到 RPC ,我第一時間想到的是 Unix 系統的 RPC (Remote Procedure Call),而且它的歷史更為悠久。為了避免混淆,所以我傾向於用 "REST-like" 這個稱呼。
首先我們來看看 REST (REpresentational State Transfer) 是什麼。基本上,它的概念來自於 Roy Thomas Fielding 寫的一篇文章《Architectural Styles and the Design of Network-based Software Architectures》。其概念結合了 HTTP 與 URL 兩種協定,以及如何運用於網路軟體架構設計。
REST 把軟體視為 "資源"(Resource),以 URL (Uniform Resource Locator) 定位資源所在處。資源的使用者則藉由 HTTP 協定中所定義的"方法"(method)操作資源。REST 所稱的軟體,其實是資料與資料處理方法的包裝,也就是 OOP 中的 "個體"、"物件"。同時在 HTTP 中,也定義了四種基本方法,即 GET, POST, PUT, DELETE
(除此之外還有一些較不常用的方法,詳細內容請自行參考 HTTP/1.1: RFC 2616)。以上四種基本方法大致上對應了四種資料處理動作,即 Create, Read, Update, Delete (CRUD)。
HTTP Method | Data operate | Description |
---|---|---|
POST | Create | Create a resource without id. |
GET | Read | Get a resource. |
PUT | Update | Update a resource or create a resource with id if not existed. |
DELETE | Delete | Delete a resource |
方法回傳的訊息也依 HTTP 分成兩個部份,一個是"狀態碼"(Status Code),另一個則是"資源內容"(Content)。
以 URL 定位資源,根據 HTTP 內容指示操作動作與回應訊息。一個符合上述實作方式的網路服務,就稱之為 RESTful web service 。有些文章則更進一步,將 ATOM 協定也加了進來,主要是看上 ATOM 格式的特點,將之運用於資源內容的更新工作。
對了,有些 RESTful 文章還會強調要透過 HTTP Authorization 限制使用者存取資源的權限,而不是用表單加 Cookie。
不過那畢竟是篇學術文章,有些內容在實務上並不易使用。所以以往我們傾向採用 "更務實的作法"。這就是為什麼現在區分 RESTfull web servie 和 REST-like web service 的原因。
實務上有什麼不同呢?主要是歷史因素,早期的瀏覽器只實作了 HTTP 中的兩種方法,即 GET, POST。這個歷史因素沿續至今,使得絕大多數的 Web 應用服務只接受 GET, POST 兩種方法傳遞的資料。同時把操作方法也放在 GET, POST 的資料欄位中,而不是看 HTTP 的 'method' 。例如:
例1. http://localhost/resource?method=delete
例2.
<form action="resource" method="post">
<input name="id" value="101"/>
<input name="name" value="rock"/>
<button name="method" value="get" type="submit">Query(Get)</button>
</form>
第一個例子,瀏覽器送出的 HTTP Method 是 GET ,但在 URL 中指示的操作動作是刪除(delete)。第二個例子,瀏覽器送出的 HTTP Method 是 POST ,但在表單欄位中指示的動作是查詢資源(get)。
這些例子的使用方式都不合 HTTP 當初制定的原義。但卻是現在最普遍的設計方式。普遍到什麼程度呢?可以問問身邊負責 Web 軟體開發的朋友,有一半以上不知道什麼 HTTP Method 是指什麼,當然更不知道有 PUT, DELETE 這些內容。
儘管今日的瀏覽器已經實作了其他 HTTP Method ,但是仍然缺乏相對應的簡便調用方法。就以前例而言,我們知道在瀏覽器網址列上輸入 URL 就是以 GET 方法送出需求。在表單的 method
屬性中可以指定要用 GET 或 POST 方法送出需求。但要送 PUT 或 DELETE 需求要如何做呢?很遺憾,目前只能透過程序性的手段處理。也就是用 JavaScript 建立一個 XMLHttpRequest 個體(XMLHttpRequest 已經被列入 W3C 工作中,目前的草案參見The XMLHttpRequest Object, W3C Working Draft 26 October 2007,這真是令人樂見的結果),然後在 XMLHttpRequest::open()
方法的第一個參數中指定 HTTP Method。麻煩多了,不是嗎?
現在實務上最常採用的作法是將 URL 對應成 /class/method/id/parameter
。我稱之為 "REST-like web service"。我們單單寫 "REST" 時,意義較廣,一般指的就是這種方式。
實務運用上,其實這兩種設計方式的差異不大。一般而言,在 loader 與介面上做一些簡單的修改動作,就可以讓原有的 REST-like web service 同時相容 RESTful web service 。以下是一個 PHP 實作的例子。
Example: Controller
<?php
/**
* 對動態語言而言,不用 interface 也可以。
* 如果你不習慣動態語言的使用方式,不用 interface 覺得沒有安全感,
* 那就用吧。
*/
interface RESTMethod {
public function restGet($segments);
public function restPost($segments);
public function restPut($segments);
public function restDelete($segments);
}
class Controller /*implements RESTMethod*/ {
public function index() {
echo 'create|read|update|delete';
}
// RESTful: controller/id by POST.
// traditional: controller/create
public function create() {
echo 'create new resource';
}
// RESTful: controller/id by GET.
// traditional: controller/id or controller/read/id
public function read($id) {
echo 'read resource: ' . $id;
}
// RESTful: controller/id by PUT.
// traditional: controller/update/id with POST data.
public function update($id=false) {
if ( !$id )
return $this->create();
echo 'update resource: '. $id;
}
// RESTful: controller/id by DELETE.
// traditional: controller/delete/id
public function delete($id) {
echo 'delete resource: '. $id;
}
public function restPost($segments) {
echo 'invoke restPost.';
readfile('php://input'); // read the raw put data.
return $this->create();
}
public function restGet($segments) {
echo 'invoke restGet.';
return $this->read($segments[0]);
}
public function restPut($segments) {
echo 'invoke restPut.';
return $this->update($segments[0]);
}
public function restDelete($segments) {
echo 'invoke restDelete.';
return $this->delete($segments[0]);
}
}
?>
Example: Loader
<?php
class Loader {
private $control;
private $segments;
//private $RESTMethodMap;
function __construct($Controller) {
//$this->control = new $controllerConfig['controllerName'];
//$this->RESTMethodMap = $controllerConfig['RESTMethodMap'];
$this->control = new $Controller;
}
function run() {
if (PHP_SAPI == 'cli') {
global $argv;
$_SERVER['PATH_INFO'] = '/' . implode('/', array_slice($argv, 1));
}
if ( !isset($_SERVER['PATH_INFO']) or $_SERVER['PATH_INFO'] == '/') {
$this->segments = false;
}
else {
$this->segments = explode('/', $_SERVER['PATH_INFO']);
}
if ( !$this->segments ) { // Without parameter
return $this->control->index();
}
if (method_exists($this->control, $this->segments[1])) {
//request resource by traditional way.
$method = $this->segments[1];
//deny directly invoke method of interface RESTMethod
if (strpos($method, 'rest') === 0) {
header('HTTP/1.0 403 Forbidden');
echo 'The server understood the request, but is refusing to fulfill it.
Authorization will not help and the request SHOULD NOT be repeated.';
return false;
}
$objMethod = array(
$this->control,
$method
);
$arguments = array_slice($this->segments, 2);
call_user_func_array($objMethod, $arguments);
}
else {
//request resource by RESTful way.
//$method = $this->RESTMethodMap[$_SERVER['REQUEST_METHOD']];
$method = 'rest' . ucfirst(strtolower($_SERVER['REQUEST_METHOD']));
$arguments = array_slice($this->segments, 1);
$this->control->$method($arguments);
}
}
} //end class Loader
/*
$controllerConfig = array(
'controllerName' => 'Controller',
'RESTMethodMap' => array(
'GET' => 'read',
'POST' => 'create',
'PUT' => 'update',
'DELETE'=> 'delete'
)
);
*/
$loader = new Loader('Controller');
//$loader->run($controllerConfig);
$loader->run();
?>
樂多舊回應