最近更新: 2008-03-13

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 MethodData operateDescription
POSTCreateCreate a resource without id.
GETReadGet a resource.
PUTUpdateUpdate a resource or create a resource with id if not existed.
DELETEDeleteDelete 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();
?>
相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/5692241.html

樂多舊回應
ubndwos@hotmail.com(張勝韋) (#comment-19547101)
Fri, 17 Jul 2009 08:56:37 +0800
這一篇對我來說真的很有幫助,我一直不太懂restful,看完後稍有一點的啟發,但是還是有些細節太懂,不知有沒有更進一步的講解。
leebig1982@gmail.com(Lucas) (#comment-20161007)
Wed, 09 Dec 2009 11:30:35 +0800
我也是~最近公司主管也要我們去了解這一塊,看完石頭大的文章後也有更進一步的了解了
未留名 (#comment-21822011)
Tue, 21 Jun 2011 12:46:25 +0800
請問特別用兩種方式呼叫有不同嗎?
call_user_func_array(...)
$this->control->$method($arguments);
未留名 (#comment-21834479)
Tue, 28 Jun 2011 20:59:39 +0800
兩者的執行效能不同。

call_user_func_array() 快上一些。

另一個原因,我要示範不同的使用方式。

未留名 (#comment-21864809)
Wed, 13 Jul 2011 15:47:45 +0800
不好意思,小弟為初學者

是否可以提供一個Request和Response的小範例參考

想自行開發一個簡單的WebService