Веб-сервисы в теории и на практике для начинающих

    Что такое веб-сервисы?



    Прежде всего, веб-сервисы (или веб-службы) — это технология. И как и любая другая технология, они имеют довольно четко очерченную среду применения.

    Если посмотреть на веб-сервисы в разрезе стека сетевых протококолов, мы увидим, что это, в классическом случае, не что иное, как еще одна надстройка поверх протокола HTTP.

    С другой стороны, если гипотетически разделить Интернет на несколько слоев, мы сможем выделить, как минимум, два концептуальных типа приложений — вычислительные узлы, которые реализуют нетривиальные функции и прикладные веб-ресурсы. При этом вторые, зачастую заинтересованы в услугах первых.

    Но и сам Интернет — разнороден, т. е. различные приложения на различных узлах сети функционируют на разных аппаратно-программных платформах, и используют различные технологии и языки.

    Чтобы связать все это и предоставить возможность одним приложениям обмениваться данными с другими, и были придуманы веб-сервисы.

    По сути, веб-сервисы — это реализация абсолютно четких интерфейсов обмена данными между различными приложениями, которые написаны не только на разных языках, но и распределены на разных узлах сети.

    Именно с появлением веб-сервисов развилась идея SOA — сервис-ориентированной архитектуры веб-приложений (Service Oriented Architecture).

    Протоколы веб-сервисов



    На сегодняшний день наибольшее распространение получили следующие протоколы реализации веб-сервисов:

    • SOAP (Simple Object Access Protocol) — по сути это тройка стандартов SOAP/WSDL/UDDI
    • REST (Representational State Transfer)
    • XML-RPC (XML Remote Procedure Call)


    На самом деле, SOAP произошел от XML-RPC и является следующей ступенью его развития. В то время как REST — это концепция, в основе которой лежит скорее архитектурный стиль, нежели новая технология, основанный на теории манипуляции объектами CRUD (Create Read Update Delete) в контексте концепций WWW.

    Безусловно, существуют и иные протоколы, но, поскольку они не получили широкого распространения, мы остановимся в этом кратком обзоре на двух основных — SOAP и REST. XML-RPC ввиду того, что является несколько «устаревшим», мы рассматривать подробно не будем.

    Нас в первую очередь интересуют вопросы создания новых веб-служб, а не реализация клиентов к существующим (как правило поставщики веб-сервисов поставляют пакеты с функциями API и документацией, посему вопрос построения клиентов к существующим веб-службам менее интересен с точки зрения автора).

    SOAP против REST



    Проблемы данного противостояния хорошо описаны в статье Леонида Черняка, найденой на портале www.citforum.ru.

    По мнению же автора, кратко можно выделить следующее:

    SOAP более применим в сложных архитектурах, где взаимодействие с объектами выходит за рамки теории CRUD, а вот в тех приложениях, которые не покидают рамки данной теории, вполне применимым может оказаться именно REST ввиду своей простоты и прозрачности. Действительно, если любым объектам вашего сервиса не нужны более сложные взаимоотношения, кроме: «Создать», «Прочитать», «Изменить», «Удалить» (как правило — в 99% случаев этого достаточно), возможно, именно REST станет правильным выбором. Кроме того, REST по сравнению с SOAP, может оказаться и более производительным, так как не требует затрат на разбор сложных XML команд на сервере (выполняются обычные HTTP запросы — PUT, GET, POST, DELETE). Хотя SOAP, в свою очередь, более надежен и безопасен.

    В любом случае вам решать, что больше подойдет вашему приложению. Вполне вероятно, вы даже захотите реализовать оба протокола, чтобы оставить выбор за пользователями службы и — это ваше право.

    Практическое применение веб-сервисов



    Поскольку речь идет о практическом применении, нам нужно выбрать платформу для построения веб-службы и поставить задачу. Так как автору ближе всего PHP 5, мы и выберем его в качестве технологии для построения службы, а в качестве задачи примем следующие требования.

    Допустим, нам необходимо создать службу, предоставляющую доступ к информации о курсах валют, которая собирается нашим приложением, и накапливается в базе данных. Далее посредством веб-сервиса, данная информация передается сторонним приложениям для отображения в удобном для них виде.

    Как видим задача довольно проста и, с точки зрения самой службы, ограничивается лишь чтением информации, но в практических целях нам этого будет достаточно.

    Этап первый — реализация приложения сбора информации о курсах валют.



    Информацию о курсах валют мы будем собирать со страниц сайта НБУ (Национального Банка Украины) ежедневно и складывать в базу данных под управлением СУБД MySQL.

    Создадим структуру данных.

    Таблица валют (currency):

    +-------------+------------------+
    | Field       | Type             |
    +-------------+------------------+
    | code        | int(10) unsigned |
    | charcode    | char(3)          |
    | description | varchar(100)     |
    | value       | int(10) unsigned |
    | base        | tinyint(1)       |
    +-------------+------------------+


    Таблица номиналов обмена (exchange):

    +------------+------------------+
    | Field      | Type             |
    +------------+------------------+
    | id         | bigint(20) ai    |
    | rate_date  | timestamp        |
    | rate_value | float            |
    | code       | int(10) unsigned |
    +------------+------------------+


    Для работы с базой данных воспользуемся ORM слоем на базе пакета PHP Doctrine. Реализуем граббер:

    класс Grubber (models/Grabber.php):

    <?php
    /*
     * @package Currency_Service
     */
    class Grabber {
    
        /**
         * Extracts data from outer web resource and returns it
         *
         * @param  void
         * @return array
         */
        public static function getData() {
            /**
             * Extracting data drom outer web-resource
             */
            $content = file_get_contents( 'http://www.bank.gov.ua/Fin_ryn/OF_KURS/Currency/FindByDate.aspx');
            if(preg_match_all( '/(\d+)<\/td>([A-Z]+)<\/td>(\d+)<\/td>(.+?)<\/td>(\d+\.\d+)<\/td>/i', $content, $m) == false) {
                throw new Exception( 'Can not parse data!');
            }
    
            /**
             * Preformatting data to return;
             */
            $data = array();
            foreach ($m[1] as $k => $code) {
                $data[] = array(
                    'code'        => $code,
                    'charcode'    => $m[2][$k],
                    'value'       => $m[3][$k],
                    'description' => $m[4][$k],
                    'rate_value'  => $m[5][$k]
                );
            }
            return $data;
        }
    
        public static function run() {
            $data = self::getData();
    
            /**
             * Sets default currency if not exists
             */
            if (!Doctrine::getTable( 'Currency')->find( 980)) {
                $currency = new Currency();
                $currency->setCode( 980)
                         ->setCharcode( 'UAH')
                         ->setDescription( 'українська гривня')
                         ->setValue( 1)
                         ->setBase( 1)
                         ->save();
            }
    
            foreach ($data as $currencyData) {
                /**
                 * Updating table of currencies with found values
                 */
                if (!Doctrine::getTable( 'Currency')->find( $currencyData['code'])) {
                    $currency = new Currency();
                    $currency->setCode( $currencyData['code'])
                             ->setCharcode( $currencyData['charcode'])
                             ->setDescription( $currencyData['description'])
                             ->setValue( $currencyData['value'])
                             ->setBase( 0)
                             ->save();
                }
    
                /**
                 * Updating exchange rates
                 */
                $date = date( 'Y-m-d 00:00:00');
                $exchange = new Exchange();
                $exchange->setRateDate( $date)
                         ->setRateValue( $currencyData['rate_value'])
                         ->setCode( $currencyData['code'])
                         ->save();
            }
    
        }
    }
    ?>


    и сам граббер (grabber.php):

    <?php
    require_once('config.php');
    Doctrine::loadModels('models');
    Grabber::run();
    ?>


    Теперь заставим наш граббер отрабатывать раз в сутки в 10:00 утра, путем добавления команды запуска граббера в таблицы cron:

    0 10 * * * /usr/bin/php /path/to/grabber.php


    Все — у нас есть достаточно полезный сервис.

    Теперь реализуем веб-сервис, который позволит другим приложениям извлекать данные из нашей базы.

    Реализация SOAP сервиса



    Для реализации веб-сервиса на базе SOAP протокола, мы воспользуемся встроенным пакетом в PHP для работы с SOAP.

    Поскольку наш веб-сервис будет публичным, хорошим вариантом будет создание WSDL файла, который описывает структуру нашего веб-сервиса.

    WSDL (Web Service Definition Language) — представляет из себя XML файл определенного формата. Подробное описание синтаксиса можно найти здесь.

    На практике будет удобно воспользоваться функцией автоматической генерации файла, которую предоставляет IDE Zend Studio for Eclipse. Данная функция позволяет генерировать WSDL файл из классов PHP. Поэтому, прежде всего, мы должны написать класс, реализующий функциональность нашего сервиса.

    класс CurrencyExchange (models/CurrencyExchange.php):

    <?php
    /**
     * Class providing web-service with all necessary methods
     * to provide information about currency exchange values
     *
     * @package Currency_Service
     */
    class CurrencyExchange {
    
        /**
         * Retrievs exchange value for a given currency
         *
         * @param  integer $code - currency code
         * @param  string $data - currency exchange rate date
         * @return float - rate value
         */
        public function getExchange( $code, $date) {
            $currency = Doctrine::getTable( 'Currency')->find( $code);
            $exchange = $currency->getExchange( $date);
            return $exchange ? (float)$exchange->getRateValue() : null;
        }
    
        /**
         * Retrievs all available currencies
         *
         * @return array - list of all available currencies
         */
        public function getCurrencyList() {
            return Doctrine::getTable( 'Currency')->findAll()->toArray();
        }
    
    }
    ?>


    Отметим, что для автоматической генерации WSDL, нам необходимо написать комментарии в стиле javadoc, потому что именно в них мы прописываем информацию о типах принимаемых аргументов и возвращаемых значений. Неплохо также описывать в нескольких словах работу методов — ведь WSDL послужит описанием API для сторонних разработчиков, которые будут использовать ваш веб-сервис.

    Не пишите в докблоках param void или return void — для WSDL это не критично, но вот при реализации REST доступа к тому-же классу у вас возникнут проблемы.


    Теперь в Zend Studio входим в меню File->Export..., выбираем PHP->WSDL, добавляем наш класс, прописываем URI-адрес нашего сервиса и создаем WSDL-файл. Результат должен быть примерно таким: http://mikhailstadnik.com/ctws/currency.wsdl

    Если вы будете добавлять новую функциональность в ваш веб-сервис, вам нужно будет пересоздавать WSDL-файл. Но здесь не так все гладко. Следует учитывать, что SOAP-клиент, который уже запрашивал ваш WSDL файл, кеширует его на своей стороне. Поэтому, если вы замените старое содержимое новым в WSDL файле, некторые клиенты его не прочтут. А значит, при добавлении новой функциональности, дописывайте версию в имя вашего файла. И не забудбте обеспечить обратную совместимость для старых клиентов, особенно если вы не являетесь их поставщиком.

    С другой стороны, WSDL довольно жестко задает структуру веб-сервиса, а это значит, что, если существует необходимость ограничить функциональность клиента по сравнению с сервером, вы можете не включать определенные методы ваших классов в WSDL. Таким образом они не смогут быть вызваны, несмотря на то, что существуют.

    Реализация же самого сервера не предстваляет теперь никакой сложности:

    файл index.php:

    <?php
    require_once('config.php');
    
    Doctrine::loadModels('models');
    
    $server = new SoapServer( 'http://mikhailstadnik.com/ctws/currency.wsdl');
    $server->setClass( 'CurrencyExchange');
    $server->handle();
    ?>


    Вы можете попробовать веб-сервис в работе по адресу: http://mikhailstadnik.com/ctws/
    Там же доступен тестовый клиент: http://mikhailstadnik.com/ctws/client.php

    Код простейшего клиента может быть таким:

    <?php
    $client = new SoapClient( 'http://mikhailstadnik.com/ctws/currency.wsdl');
    echo 'USD exchange: ' . $client->getExchange( 840, date( 'Y-m-d'));
    ?>


    Реализация REST сервиса



    REST — это не стандарт и не спецификация, а архитектурный стиль, выстроенный на существующих, хорошо известных и контролируемых консорциумом W3C стандартах, таких, как HTTP, URI (Uniform Resource Identifier), XML и RDF (Resource Description Format). В REST-сервисах акцент сделан на доступ к ресурсам, а не на исполнение удаленных сервисов; в этом их кардинальное отличие от SOAP-сервисов.

    И все же удаленный вызов процедур применим и в REST. Он использует методы PUT, GET, POST, DELETE HTTP протокола для манипуляции объектами. Кардинальное отличие его от SOAP в том, что REST остается HTTP-запросом.

    Поскольку в PHP пока еще нет реалзации REST, мы воспользуемся Zend Framwork, в который включена реализация как REST клиента, так и REST севера.

    Воспользуемся уже готовым классом CurrencyExchange. Напишем сам сервер:

    rest.php:

    <?php
    require_once 'config.php';
    require_once 'Zend/Rest/Server.php';
    
    Doctrine::loadModels('models');
    
    $server = new Zend_Rest_Server();
    $server->setClass( 'CurrencyExchange');
    $server->handle();
    ?>


    Как видите все очень сходно и просто.

    Однако, следует оговорить, что наш REST-сервис менее защищен, чем SOAP-сервис, так как любой добавленый метод в класс CurrencyExchange при его вызове отработает (сам класс определяет сруктуру сервиса).

    Проверим работу нашего сервиса. Для этого достаточно передать параметры вызова метода в сроке GET-запроса:

    ?method=getExchange&code=840&date=2008-11-29


    или

    ?method=getExchange&arg1=840&arg2=2008-11-29


    При желании или необходимости вы можете самомтоятельно задавать структуру ваших XML ответов для сервиса REST. В этом случае, также будет необходимо позаботиться и о создании определения типа вашего XML документа (DTD — Document Type Definition). Это будет минимальным описанием API вашего сервиса.

    Простейший тестовый клиент к REST сервису может быть в нашем случае таким:

    <?php
    $client = new Zend_Rest_Client( 'http://mikhailstadnik.com/ctws/rest.php');
    $result = $client->getExchange( 840, date( 'Y-m-d'))->get();
    if ($result->isSuccess()) {
        echo 'USD exchange: ' . $result->response;
    }
    ?>


    В принципе, Zend_Rest на сегодняшний день нельзя назвать наиболее точной реализацией принципов REST. Утрируя, можно говорить о том, что эта реализация свелась к удаленному вызову процедур (RPC), хотя философия REST гораздо шире.

    Вы можете скачать пример в исходных кодах c PHP Doctrine и Zend Framework (4,42 Мб).

    Заключение



    Мы выполнили задачу минимум и показали, что такое веб-сервисы, для чего они нужны и как их реализовывать. Естественно, приведенный пример, возможно, несколько оторван от жизни, но он был выбран лишь в качестве инструмента для объяснения предмета и сущности веб-сервисов.

    Кроме того мы увидели, что реализация веб-сервиса — задача довольно простая при использовании современного инструментария, который позволяет сконцентрироваться, в первую очередь, на разработке функциональности самого сервиса, не заботясь о низкоуровневой реализации протоколов.

    Автор надеется, что данный материал будет действительно полезен тем, кто становится на тропу разработки веб-служб.

    Удачи в девелопменте!
    Поделиться публикацией

    Комментарии 30

      +7
      Web-сервисы на Java

      1. Класс сервиса:

      package com.horstmann.corejava;
      import java.util.*;
      import javax.jws.*;
      /**
       * This class is the implementation for a Warehouse web service
       * @version 1.0 2007-10-09
       * @author Cay Horstmann
       */
      @WebService
      public class Warehouse
      {
         public Warehouse() 
         {
            prices = new HashMap<String, Double>();
            prices.put("Blackwell Toaster", 24.95);
            prices.put("ZapXpress Microwave Oven", 49.95);
         }
      
         public double getPrice(@WebParam(name="description") String description)
         {
            Double price = prices.get(description);
            return price == null ? 0 : price;
         }
         private Map<String, Double> prices;
      }
      


      2. Класс сервера:

      package com.horstmann.corejava;
      import javax.xml.ws.*;
      public class WarehouseServer
      {
         public static void main(String[] args)
         {
            Endpoint.publish("http://localhost:8080/WebServices/warehouse", new Warehouse());
         }
      }


      3. Класс клиента:

      import java.rmi.*;
      import javax.naming.*;
      import com.horstmann.corejava.server.*;
      /**
       * The client for the warehouse program.
       * @version 1.0 2007-10-09
       * @author Cay Horstmann
       */
      public class WarehouseClient
      {
         public static void main(String[] args) throws NamingException, RemoteException
         {
            WarehouseService service = new WarehouseService();      
            Warehouse port = service.getPort(Warehouse.class);
            String descr = "Blackwell Toaster";
            double price = port.getPrice(descr);
            System.out.println(descr + ": " + price);
         }
      }


      Источник: archive.williamspublishing.com/cgi-bin/materials.cgi?isbn=978-5-8459-1482-8

      По-сути, ничего сложного нет.
        0
        Но это не все файлы.
        Ещё кучу «заглушек» надо наштамповать с помощью wsgen и wsimport (входят в поставку JDK).

        Среды программирования NetBeans и Eclipse это делают «на автомате», но после их работы код получается сверх-избыточным.
          0
          В приведенном примере используется динамическая генерация стабов в рантайме. Но в большинстве случаев конечно используется статическая генерация со всеми вытекающими заглушками.
            0
            Пробовал откомпилировать и запустить. Но что-то не ощутил динамической генерации стабов. Надо руками всё делать.
              0
              Какая версия JDK? или JEE?
              Работает начиная с 1.6 и 5 соответственно.
        0
        А есть ли реализации WSDL (версия 1.x) клиентов (для PHP, Java) которые нормально понимают HTTP binding (REST)? Большинство с чем сталкивался нормально парсят только SOAP binding.
          +1
          а это не оно, случаем? wso2.org/projects/wsf/php/features там много занятных либ есть очень, рекомендую посмотреть
            0
            Пакеты javax.ws.rs, javax.ws.rs.core, javax.ws.rs.ext, имплементирующие JAX-RS API (JSR-311), входят в Sun JRE 1.6.

            Примеры, использующие JAX-RS API, можно посмотреть в NetBeans 6.5.
              +1
              Ошибся. Библиотека JAX-RS API поставляется вместе с NetBeans6.5, в JRE/JDK нету этого API.
            +1
            понял, увы, далеко не все, но было очень интересно.
            надо разбираться.
              –1
              Как по мне, приведенные в статье примеры далеко не для начинающих. Возможно из-за использования фреймворков.
                +1
                WSDL, сгенерированный Zend'ом для более-менее развесистого сервера, просто неподъемен. Вы забыли, что SOAP еще обеспечивает более строгую типизацию и сильно удобен для сообщения между сервисами.
                  0
                  Спасибо, обязательно внесу исправления в текст. Вы не против?
                    0
                    Разумеется не против
                  0
                  Хм. Использование фреймворков в этой статье делает SOAP и REST концептуально неразличимыми для читателя.
                    0
                    Увы, это так, и я об этом сам сказал и в статье. Возможно дополню чуть позже другим примером именно для REST. Прямо сейчас занят другой статьей не хочу отвлекаться :)
                    +1
                    > mikhailstadnik.com/ctws/currency.wsdl
                    А для WSDL файлика разве не должен отдаваться content-type application/xml или подобный?
                    А то, боюсь, не все клиенты смогут нормально его прочитать…
                      0
                      Ну в данном случае это больше проблема настройки веб-сервера, так как отдается просто статичный файл. Тем не менее имея полный доступ к серверу — поправил :) спасибо
                      +3
                      Zend_Rest выглядит действительно как RPC.
                      По моему REST URLдолжен выглядеть так: /rates/EUR-USD/2008-12-10/
                      * вызов просто /rates/ возвращает все доступные пары валют
                      * вызов /rates/EUR-USD/ возвращает все значения пары EUR-USD

                        0
                        А я не понял главного — для чего нужны все эти SOAP и XML-RPC :)
                        Уж не судите строго, а объясните подробнее, если не сложно.

                        Вот возьмём «склонятор» яндекса nano.yandex.ru/project/inflect/
                        Почему бы просто не сделать вот так

                        file_get_contents(«export.yandex.ru/inflect.xml?name=...»);

                        И потом разобрать пришедший xml?

                        Вот будет выглядеть реализация этого примера с помощью SOAP?
                          0
                          Посмотрите с другой стороны — предположите, что вам нужно сделать «склонятор» :)
                          Кроме того, современные инструменты позволяют не разбирать XML самому, если уж речь идет о клиенте.
                            0
                            Смотрю, но ничего не вижу :)
                            Я представляю себе задачу как простое получение параметра, формирование для него результата и его вывод в виде xml. Чтобы собрать и разобрать xml есть SimpleXML и т.п.

                            Т.е. я вижу тут xml как главного посредника, а зачем всё остальное… не мойму :)
                              +1
                              По сути вашего вопроса:
                              XML действительно посредник, но сам по себе XML — это формат без какой либо определенной структуры. А это значит что вам следует ее определять самому. И донести это до всех, кто захочет с вашей структурой работать.
                              А SOAP — это стандарт известный всем :) И многие языки УЖЕ имеют инструментарий по сборке и разбору структуры его XML (что в примерах к статье и показано).

                              А сам принцип о котором вы говорите — больше из концепции REST, которая именно так все и упрощает (получил параметр — отдал ресурс). В рамках веб — это всем понятно.

                              А сам SOAP больше завязан на теории ООП и удаленного вызова процедур (манипулирование с удаленными объектами, неизвестно на чем написанными).
                                0
                                Спасибо, последний абзац немного прояснил ситуацию )
                            +1
                            SOAP по сути это стандарт, которого нужно придерживаться для того, чтобы Ваш сервис могли использовать клиенты, написаные на совершенно разных языках программирования и под различные платформы. Над SOAP есть множество стандартных настроек для решения типичных задач (authentication/reliable messaging/transactions/security tokens/etc), использование которых сделает ваш сервис совместимым с большим количеством клиентов.
                            В данном обзоре пропущена важная часть, связаная с веб-сервисами. Это WS-I Basic Profile — набор рекоммендаций и стандартов к SOAP, WSDL, UDDI для девелоперов веб-сервисов, предназначеная для того, чтобы увеличить interoperability(возможность взаимодействия\совместимость?) между приложениями.
                            Ну вообщем SOAP это стандартизированый XML c целым набором технологий, основная суть которых — повторюсь, interoperability.
                            0
                            и — это ваше право
                            По-моему, вернее будет так:
                            и это — ваше право
                              0
                              Очень хорошо, что этим занимаются люди. Я, в общем, никогда не писал вэб-сервисы на PHP, и даже наверное не планирую… но все-таки хорошо, что этим люди занимаются и показывают другим как это делать :)
                                0
                                Я в общем к тому, что держите плюс в карму))
                                0
                                Статья оказалась в тему, похоже в моей сфере веб-сервисам найдется применение. Но попытка сделать свой массо-габаритний макет на основе приведеных автором примеров не увенчалась успехом, пока. Пока-что основной трудностью для меня остается отладка (непонятно как обнаружить ошибки в сервере), код выполняется без ошибок, но ведет себя странно. Например, клиент выглядит так:

                                $client = new SoapClient('http://172.16.3.8/TestServer.wsdl');
                                $exist = $client->checkUser($_GET['login']);
                                var_dump($exist);
                                echo «REQUEST:\n». $client->__getLastRequest(). "\n";
                                echo «RESPONSE:\n». $client->__getLastResponse(). "\n";
                                var_dump($client->__getFunctions());

                                Выдает странное:
                                object(stdClass)[2]

                                REQUEST: RESPONSE:

                                array
                                0 => string 'checkUserResponse checkUser(checkUser $parameters)' (length=50)

                                Почему object(stdClass), почему запрос и ответ пустые?

                                Разбираюсь сам, но может кто подскажет направление, куда копать :)
                                  0
                                  Я ошибаюсь, или здесь запросы вцикле?

                                  Grabber::run() {
                                  foreach…
                                  }

                                  ( Grubber (models/Grabber.php) )

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое