RSS Мои друзья Контакты

Введение Ruby::Rack

Если вдруг кто не знает, у меня есть свой Open-Source проект - бесплатный AJAX файловый менеджер. Начав изучать Ruby, первое что пришло мне в голову - это написать для него backend адаптер (и для проекта хорошо и для меня практика). Конечно же, пришлось зарефакторить JavaScript код, но об этом в другой статье.

Как оказалось создание web приложения на Ruby без использования фреймворков, вроде Rails или Sinatra, не так уж просто. Для этого необходимо полностью реализовать поддержку протокола HTTP, т.е. написать парсер заголовков и тела запроса, а также отдавать результат клиенту при помощи обычной функции print. Но все же, не все так плохо и есть дорожка выстеленная благими намерениями - это Rack.

Rack? Не, не слышал

Rack - это интерфейс, который создан, чтобы обеспечить минимальное API для подключения веб-серверов поддерживающих Ruby (WEBrick, Mongrel и т.д.) и веб-фреймворками (Rails, Sinatra и др.). В нем реализован базовый функционал для работы с HTTP протоколом: утилиты для парсинга, классы Response, Request, Session и многое другое.

Вернемся к абстрактным вещам. Если проанализировать взаимодействие сервера и клиента, то можно обнаружить 3 ключевых аспекта: статус запроса, заголовки и тело. Именно это и стало фундаментом Rack Application.

Что же такое Rack Application? Это самый обычный Ruby объект, который отвечает на метод call. В этот метод передаются переменные среды (environment variables) и он должен возвратить массив состоящий из 3-х элементов: числового статуса, хеша заголовков и тела ответа. Последний должен отвечать на метод each, которому передается блок.

Поскольку в Ruby нет интерфейсов и все держится на честном слове, то единственный способ, которым осуществляется проверка наличия метода - это method_defined? и respond_to?. Т.е., Ruby не может проверить сделали ли Вы что-то с переданным блоком или знает ли Ваш код об этом вообще, по-этому нужно быть предельно внимательным. Достаточно поясничать, давайте посмотрим на пример

class Application
  def call(env)
    [200, {'Content-Type' => 'text/html'}, ["This is Rack!"]]
  end
end

run Application.new

Сохраняем пример в файл config.ru (стандартное конфигурационное имя файла для rackup) и в командной строке запускаем сервер:

rackup config.ru

По логам можно определить на каком порту запустился сервер (в моем случае 9292). Открываем броузер http://localhost:9292 и видим строку "This is Rack!". Заголовок Content-Type является обязательным, если Ваше приложение не возвращает такого заголовка, то Rack выбросит эксепшен.

Отойдя немного от темы, расскажу о существовании сервера shotgun, который работает точно также, как и rackup, но используется для development целей. При запуске сервера Ruby подгружает нужные файлы в память, которые там "живут" все время, т.е. rackup - что-то вроде application server. А это значит, что изменения в любом файле не вступят в силу до тех пор, пока не будет перезагружен сервер. И это становится проблемой во время активной разработки, по этому стоит использовать shotgun, он следит за файлами и автоматически их перегружает. Этот сервер доступен, как обычный Ruby gem, по этому установить его не составит труда.

Полезное в Rack изнутри?

Как я уже упомянул раньше Rack содержит набор полезных утилит и 3 важнейших для любого web приложения класса: Response, Request и Session.

Rack::Response предоставляет интерфейс, который упрощает создание ответа клиенту. Позволяет устанавливать заголовки, куки и создавать тело ответа. Класс достаточно простой, его исходники можно найти на GitHub-е (а чего там нельзя найти?). Несколько примеров:

require 'rack'
require 'rack/response'

class Application
  def call(env)
    response = Rack::Response.new
    
    # Append text to response body
    response.write "This is Rack"
    response.write "!!!"
    # Set Content-Type
    response['Content-Type'] = "text/html"
    # Set cookie
    response.set_cookie("my_cookie", "Hello Rack");
    # [status, headers, body]
    response.finish
  end
end

run Application.new

Класс Rack::Request более интересен, он предоставляет интерфейс для доступа к переменным запроса и упрощает работу с загружаемыми файлами. В качестве единственного параметра для инициализации принимает хеш переменных среды. Умеет делать множество полезных вещей: проверять тип запроса (Head, Delete, Options, Get, Post, Put), парсить QUERY_STRING в хеш, читать куки, возвращать базовый URL, узнать referer или user_agent. Например

# inside call method
request = Rack::Request.new(env)

# hash of all GET & POST parameters
request.params

# return "my_param"
request["my_param"]

# set "my_param"
request["my_param"] = "new value"

# read cookie
request.cookies["my_cookie"]

# file upload
if request.post?
  # file is hash that consists from all necessary information about uploaded file including TempFile object
  file = request["my_file"]
  return [200, {"Content-Type" => "text/html"}, [file[:tempfile].read]]
end

Rack::Session реализован, как функционально-независимая часть и предоставляет 3 адаптера для хранения: в куках, в мемкэш сервере и обычный HashPool. Первый не рекомендуется использовать на продакшен серверах в целях безопасности, но вполне годится для девелопмента. К сессии можно обратится через экземпляр класса Rack::Request при помощи метода session, который представляет собой простой хеш.

Сессия является оберткой для приложения, если говорить более строгими терминами, то класс сессии является декоратором (паттерн декоратор) для rack application-а. Например

myapp = MyRackApp.new
sessioned = Rack::Session::Pool.new(myapp,
  :domain => 'foo.com',
  :expire_after => 2592000
)

run sessioned

Все конечно хорошо, но хотелось бы добавлять новый функционал с минимальным изменением кода и без нагромождений. Для этого в Rack существует понятие middleware (чем по сути и является сам фреймворк). Используя middleware (посредник/фильтр) можно изменить/подготовить запрос перед тем как он попадет в Application, аналогично и с ответом для клиента.

Rack Middleware

Предыдущий пример можно переписать через middleware и метод use rackup сервера

class Application
  def call(env)
    request = Rack::Request.new(env)

    request.session[:user_ip] ||= request.ip
    [200, {'Content-Type' => 'text/html'}, [request.session.inspect]]
  end
end
use Rack::Session::Pool
run Application.new

Также можно эмулировать работу shotgun сервера при помощи стандартного Rack::Reloader middleware.

class Application
  def call(env)
    request = Rack::Request.new(env)

    request.session[:user_ip] ||= request.ip
    [200, {'Content-Type' => 'text/html'}, [request.session.inspect]]
  end
end
use Rack::Session::Pool
use Rack::Reloader
run Application.new

В отличии от метода run, методу use нужно передать класс, а не объект. Выглядит достаточно элегантно и гибко.

Пишем свой middleware

Для примера можно написать простую авторизацию по IP. Если IP пользователя находится в массиве разрешимых, то он получает доступ к приложению, если нет, то отдаем 404 страничку.

class IpAuth
  @@trusted_ips = %w(127.0.0.1 ::1)

  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env);

    if @@trusted_ips.include?(request.ip)
      @app.call(env)
    else
      [404, {'Content-Type' => "text/html"}, ['Not Found']]
    end
  end
end

class Application
  def call(env)
    request = Rack::Request.new(env)

    request.session[:user_ip] ||= request.ip
    [200, {'Content-Type' => 'text/html'}, [request.session.inspect]]
  end
end

use IpAuth
use Rack::Session::Pool
use Rack::Reloader
run Application.new

В метод initialize передается объект application, в соответствии с цепочкой middleware (т.е., вызовов методов use), в конкретном случае IpAuth получит экземпляр класса Rack::Session::Pool. В этом же методе мы просто сохраняем ссылку на приложение, чтобы потом можно было его вызвать в методе call. Вот так просто можно добавить авторизацию для своего приложения.

P.S.: Rack имеет документацию, но это не то, по чему можно было бы нормально изучать фреймворк, по этому чаще смотрю исходники на GitHub.

Добавить комментарий

Комментариев: 3

  • jooshka
    Ответить 8 мая 2017 г., 18:01
    Отличная статья, очень помогла разобраться, спасибо!
  • Вадим
    Ответить 9 мая 2017 г., 14:25
    Да уж... В PHP конечно все намного проще! :)
  • Жека
    Ответить 8 сентября 2017 г., 16:17
    "В РНР легче" - это сарказм?