Thursday, May 2, 2013

Взаимодействие веб-сервисов через REST http://habrahabr.ru/post/148492/

http://habrahabr.ru/post/148492/

Взаимодействие веб-сервисов через REST

При разработке современных веб-сервисов зачастую появляется вопрос, каким образом обеспечить простое и прозрачное взаимодействие нескольких разнородных систем. Благо, выбор большой: здесь и SOAP, и CORBA, иDCE/RPC, и, конечно же, REST. О создании межплатформенного API на его базе и пойдет речь.

Зачем делать?


Действительно, зачем «городить огород» и использовать разнородные системы, если можно единожды выбрать один инструмент — либо Perl (+фреймворк по вкусу), либо Rails, и делать все на нем? Примерно за тем же, за чем мы используем шлицевую отвертку для шлицевого винта, а крестовую — для крестового, а не наоборот (так можно, конечно, но это не эффективно). Каждый инструмент лучше подходит для того или иного, определенного набора задач.

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

Каналы не всегда хорошие, да и операторы порой делают throttling. А вот дать задание «агентам», чтобы потом скачать с большой скоростью через обычный HTTP/FTP уже готовые файлы — приятно.

Вот поэтому основной веб-сервис для простоты и удобства есть смысл разрабатывать на Rails, а агентов делать очень «тонкими», на том языке, что есть почти на всех Unix и на некоторых Windows-серверах: Perl.

Что делать?


Как я упомянул в начале статьи, сейчас существует множество протоколов для реализации API между сервисами. Раньше в этом случае я бы не задумывался, и использовал классический SOAP (по сути: XML + HTTP). Благо, есть неплохие инструменты реализации что для Perlчто для Rails.

Но сейчас все большую и большую популярность приобретают RESTful API, и не зря. Здесь не требуется каких-то схем, definition'ов, дополнительных WSDL-файлов и прочих сложностей. Суть подхода в использовании команд HTTP (GET, PUT, POST, DELETE) в комбинации с соответствующим URI. В зависимости от команды и URI, выполняется то или иное действие. Ответ приходит с помощью того же HTTP Response. Более подробно, с табличками и примерами, можно почитать здесь.

В нашем примере Perl будет выступать сервером, а Rails — клиентом.
Итак, с помощью чего реализовывать?

На стороне Perl

Perl сам по себе, без модулей, очень ограниченный инструмент. Поэтому для использования всей его силы и удобства, нам потребуется модуль Mojolicious, позиционирующий себя как «A next generation web framework for the Perl programming language».

На его базе можно делать как RESTful-сервер, так и RESTful-клиент.

На стороне Rails

До недавнего времени в Rails не было нормального механизма REST-взаимодействия со сторонними сервисами, несмотря на то, что эта идеология пронизывает фреймворк с ног до головы. Поэтому, делались различные GEM'ы, решающие эту задачу с той или иной степенью успешности.

Благо, сейчас можно полноценно использовать встроенные механизы Rails, а именно класс ActiveResource, позволяющий работать с удаленным сервисом почти так же, как и ActiveRecord.

Как делать?



На стороне Perl

Предположим, что у нас есть множество объектов Downloads на стороне Perl, и со стороны Rails мы хотим выполнять с ними следующие действия:
  • Создавать
  • Получать
  • Изменять
  • Удалять

С помощью Mojolicious сделать RESTful веб-сервис под нашу задачу весьма несложно:

  #!/usr/bin/perl -w    use Mojolicious::Lite;    # Создаем массив с тестовыми данными    # В нашем примере при создании/изменении используется  # только один параметр: URI видео файла для скачивания  my $downloads = [];    foreach my $id (0..10) {    $downloads->[$id] =      { 'id'   => $id,        'uri'  => "http://site.com/download_$id",        'name' => "Video $id",        'size' => (int(rand(10000)) + 1) * 1024      };  }       # Непосредственное описание методов веб-сервиса    # Создание (create)  post '/downloads/' => sub {    my $self   = shift;      # Мы получаем от Rails все параметры в JSON    # Поэтому, их надо распарсить    my $params =  Mojo::JSON->decode($self->req->body)->{'download'};      # При создании в качестве уникального id выступает    # последний индекс нашего тестового массива    my $id     = $#{ $downloads } + 1;    my $uri    = $params->{'uri'}  || 'http://localhost/video.mp4';    my $name   = $params->{'name'} || "Video $id";    my $size   = $params->{'size'} || (int(rand(10000)) + 1) * 1024;      $downloads->[$id] =      { 'id'   => $id,        'uri'  => $uri,        'name' => $name,        'size' => $size      };      # Отправляем в качестве ответа созданный объект    $self->render_json($downloads->[$id]);  };    # Список всех объектов (index)  get '/downloads' => sub {    my $self = shift;    $self->render_json($downloads);  };    # Поиск и получение информации объекта (find/show)  get '/downloads/:id' => sub {    my $self = shift;    my $id   = $self->param('id');      if (!exists($downloads->[$id])) {            # Если нет такого объекта - 404      $self->render_not_found;    } else {        # Иначе - отдаем объект      $self->render_json($downloads->[$id]);    }  };    # Редактирование (update)  put '/downloads/:id' => sub {    my $self   = shift;    my $params =  Mojo::JSON->decode($self->req->body)->{'download'};      my $id     = $self->param('id');    my $uri    = $params->{'uri'}  || 'http://localhost/video.mp4';    my $name   = $params->{'name'} || "Video $id";    my $size   = $params->{'size'} || (int(rand(10000)) + 1) * 1024;      if (!exists($downloads->[$id])) {      $self->render_not_found;    } else {      $downloads->[$id] =        { 'id'   => $id,          'uri'  => $uri,          'name' => $name,          'size' => $size        };        $self->render_json($downloads->[$id]);    }  };    # Удаление (delete)  del '/downloads/:id' => sub {    my $self = shift;    my $id   = $self->param('id');      if (!exists($downloads->[$id])) {      $self->render_not_found;    } else {      delete $downloads->[$id];        # Посылаем HTTP 200 OK - объект успешно удален      $self->rendered;    }  };      # Пример нестандартной функции - старт загрузки  post '/downloads/:id/start' => sub {    my $self = shift;    my $id   = $self->param('id');      if (!exists($downloads->[$id])) {      $self->render_not_found;    } else {      $self->rendered;    }  };       # Непосредственный запуск сервера  app->start;  


Запускаем сервис. Используем порт 3001, так как стандартный 3000, скорее всего, будет конфликтовать с вашей инсталляцией Rails:
./restful-server.pl daemon --listen=http://*:3001


На стороне Rails

В рамках этого примера весь Rails сведется к проверке работоспособности класса ActiveResource с нашим RESTful Perl-сервером.

Создаем нужный класс:
class Download < ActiveResource::Base	       # Адрес Perl-сервера    self.site = 'http://localhost:3001'  end


Теперь мы можем выполнять с этим классом обычные для Rails действия.

Поиск всех объектов:
> Download.find(:all)  => [#<Download:0x00000004b77060 @attributes={"name"=>"Video 0", "id"=>"0", "size"=>7654400, "uri"=>"http://site.com/download_0"}, @prefix_options={}, @persisted=true>, #<Download:0x0000000446f740 @attributes={"name"=>"Video 1", "id"=>"1", "size"=>8672256, "uri"=>"http://site.com/download_1"}, @prefix_options={}, @persisted=true>, #<Download:0x0000000446d300 @attributes={"name"=>"Video 2", "id"=>"2", "size"=>5931008, "uri"=>"http://site.com/download_2"}, @prefix_options={}, @persisted=true>, #<Download:0x0000000446c888 @attributes={"name"=>"Video 3", "id"=>"3", "size"=>2273280, "uri"=>"http://site.com/download_3"}, @prefix_options={}, @persisted=true>, #<Download:0x000000045c7c50 @attributes={"name"=>"Video 4", "id"=>"4", "size"=>8466432, "uri"=>"http://site.com/download_4"}, @prefix_options={}, @persisted=true>, #<Download:0x000000045c6ee0 @attributes={"name"=>"Video 5", "id"=>"5", "size"=>7057408, "uri"=>"http://site.com/download_5"}, @prefix_options={}, @persisted=true>, #<Download:0x000000045c5d60 @attributes={"name"=>"Video 6", "id"=>"6", "size"=>2351104, "uri"=>"http://site.com/download_6"}, @prefix_options={}, @persisted=true>, #<Download:0x00000004116058 @attributes={"name"=>"Video 7", "id"=>"7", "size"=>5640192, "uri"=>"http://site.com/download_7"}, @prefix_options={}, @persisted=true>, #<Download:0x00000004114320 @attributes={"name"=>"Video 8", "id"=>"8", "size"=>9701376, "uri"=>"http://site.com/download_8"}, @prefix_options={}, @persisted=true>, #<Download:0x0000000411b080 @attributes={"name"=>"Video 9", "id"=>"9", "size"=>9717760, "uri"=>"http://site.com/download_9"}, @prefix_options={}, @persisted=true>, #<Download:0x00000004a46330 @attributes={"name"=>"Video 10", "id"=>"10", "size"=>6734848, "uri"=>"http://site.com/download_10"}, @prefix_options={}, @persisted=true>]


Поиск конкретного объекта:
> Download.find(5)  => #<Download:0x00000004aa5420 @attributes={"name"=>"Video 5", "id"=>"5", "size"=>7057408, "uri"=>"http://site.com/download_5"}, @prefix_options={}, @persisted=true> 


Поиск несуществующего объекта. Обратите внимание, как срабатывает render_not_found:
  > Download.find(100)  ActiveResource::ResourceNotFound: Failed.  Response code = 404.  Response message = Not Found.


Создание объекта:
> download = Download.new  => #<Download:0x00000004802380 @attributes={}, @prefix_options={}, @persisted=false>  > download.name = "New Video"  => "New Video"  > download.uri = "http://site.com/video.mp4"  => "http://site.com/video.mp4"  > download.size = 23452363  => 23452363  > download.save  => true  > Download.last  => #<Download:0x000000049408f0 @attributes={"name"=>"New Video", "id"=>11, "size"=>23452363, "uri"=>"http://site.com/video.mp4"}, @prefix_options={}, @persisted=true> 


Изменение объекта:
> download = Download.find(5)  => #<Download:0x0000000473ee30 @attributes={"name"=>"Video 5", "id"=>"5", "size"=>7057408, "uri"=>"http://site.com/download_5"}, @prefix_options={}, @persisted=true>   > download.name = "New Video 5"  => "New Video 5"   > download.save  => true   > Download.find(5)  => #<Download:0x000000043dade8 @attributes={"name"=>"New Video 5", "id"=>"5", "size"=>7057408, "uri"=>"http://site.com/download_5"}, @prefix_options={}, @persisted=true> 


Удаление объекта:
> Download.find(5).destroy  => #<Net::HTTPOK 200 OK readbody=true>   > Download.find(5)  ActiveResource::ResourceNotFound: Failed.  Response code = 404.  Response message = Not Found.


Вызов нестандартной функции:
> Download.find(1).post(:start)  => #<Net::HTTPOK 200 OK readbody=true> 


Что дальше?


Этот пример можно развить в следующих направлениях:
  • ActiveResource не обладает жесткой моделью данных, но ее по желанию можно задать с помощью schema. Это позволит, к примеру, исключить ручное присвоение id строкового значения.
  • Perl как RESTful-клиент с использованием модуля Mojo::UserAgent
  • Аутентификация/авторизация


Использованные версии


  • CentOS Linux 6.2
  • Perl 5.10.1
  • Mojolicious 2.97
  • Ruby 1.9.3p125
  • Rails 3.2.1

No comments:

Post a Comment