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

в 5:53, , рубрики: mojolicious, perl, ruby on rails, webservices, Веб-разработка, метки: , , ,

При разработке современных веб-сервисов зачастую появляется вопрос, каким образом обеспечить простое и прозрачное взаимодействие нескольких разнородных систем. Благо, выбор большой: здесь и 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 = [];

for (my $id = 0; $id <= 10; $id++) {
  $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

Автор: vponomarev

* - обязательные к заполнению поля