Как стать автором
Обновить

Асинхронный ruby-фреймворк Cramp: архитектура и использование

Время на прочтение6 мин
Количество просмотров2.9K
Cramp ‒ полностью асинхронный фреймворк реального времени, написанный Pratik Naik, разработчиком в 37signals и членом Rails core-team. Этот фреймворк предназначен прежде всего для организации двунаправленного общения между клиентом и сервером и имеет встроенную поддержку WebSockets и Server-Sent Events. В этой статье мы разберем основные вещи, касающиеся использования данного инструмента, а также попытаемся разобраться в его архитектуре и понять, как это работает.

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

Архитектура


Cramp основан на EventMachine, ActiveSupport и Thor и запускается на сервере Thin или Rainbows.

Thor используется для генерации приложения — в бинарнике вызывается Cramp::Generators::Application.start, который последовательно выполняет все публичные методы класса Application, названия которых: create_root, create_config, create_models, говорят сами за себя.

Далее, при запуске сервера стартует цикл EventMachine, который и отлавливает все события, запуская привязанные к ним коллбэки. Именно поэтому Cramp работает только на Rainbows или Thin(по умолчанию), которые построены на EM.

Ключевым в философии Cramp является понятие Action(классы Action и Abstract). Action является чем-то средним между контроллером и собственно экшеном в Rails(если вообще уместно сравнивать асинхронный и синхронный фреймворки). Каждый action обрабатывает один запрос или, правильнее будет сказать, одну точку входа, ведь к примеру соединение через веб-сокет одним запросом не назовешь.

Обычный запрос условно делится на 4 стадии:
  • Инициализация запроса ‒ вызываются коллбэки before_start.
    На этой стадии заголовки еще не отправлены, можно перенаправить пользователя куда-нибудь или прервать обработку запроса. В документации написано, что рекомендуется использовать before_start, чтобы проверять запрос или права доступа пользователя. На before_start можно вешать несколько методов, которые будут выполнены последовательно, но каждый из них должен вызывать yield или halt, чтобы продолжить или прервать обработку запроса соответственно.
  • Инициализация ответа ‒ соответствует функции respond_with.
    Служит для формирования заголовков и обычно используется стандартный заголовок с кодом 200:

    def build_headers
         status, headers = respond_to?(:respond_with, true) ? respond_with.dup : [200, {'Content-Type' => 'text/html'}]
         headers['Connection'] ||= 'keep-alive'
         [status, headers]
     end
  • Запрос запущен ‒ выполняется on_start.
    Здесь и должно происходить формирование ответа. На on_start можно так же вешать несколько коллбэков, которые будут выполняться одновременно, при этом каждый из них может вызывать метод render и не по одному разу.
  • Запрос завершен ‒ вызывается on_finish.
    Выполняется после формирования ответа и служит, чтобы что-то подчистить, закрыть открытые соединения и т.п.

Киллер-фичей данного фреймворка являются WebSockets. Они поддерживаются из коробки и не требуют отдельного сервера. Чтобы использовать веб-сокеты достаточно в своем action’е создать метод-обработчик on_data, все остальные вопросы решит сервер, реализующий EM-метод receive_data, срабатывающий при получении сообщения из соединения и вызывающий пользовательский обработчик:

Thin
callback = @request.env[Thin::Request::WEBSOCKET_RECEIVE_CALLBACK]
callback.call(data) if callback


Rainbows
callback = @env[WEBSOCKET_RECEIVE_CALLBACK]
callback.call(data) if callback


На этом закончим с архитектурой и перейдем к использованию.

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


Для работы с Cramp рекомендуется использовать Ruby ветки 1.9 для использования файберов, что при наличие таких инструментов как rvm или rbenv не должно стать проблемой даже если вы до сих пор сидите на 1.8.7.

Установка Cramp’а никаких трудностей вызывать не должна:
gem install cramp


Далее запускаем
cramp new project_name

и вышеупомянутый Thor генерирует скелет вашего приложения.
Затем, находясь в папке проекта, выполняем bundle install, который подтянет все зависимости.

У нас уже создан один action — app/actions/home_action.rb, со следующим содержимым:
class HomeAction < Cramp::Action
 def start
    render "Hello World!"
    finish
 end
end


Как несложно понять из этого кода, обработчик start формирует ответ в виде строки “Hello World!” и завершает обработку запроса. Проверяем это предположение, запустив сервер:
bundle exec thin start

и действительно, все работает, как ожидалось. Насколько я понимаю, Thin сам подхватывает файл config.ru, лежащий в папке проекта, но для верности можно запускать эту команду в таком виде:
bundle exec thin --max-persistent-conns 1024 --timeout 0 -R config.ru start

или
bundle exec thin --max-persistent-conns 1024 --timeout 0 -V -R config.ru start

для отладки в режиме “verbose”.

Естественно нам не нужно рендерить статичесикй текст и html-разметку напрямую, для этого должны использоваться представления(views), находящиеся в отдельных файлах. Создаем папку app/views и в ней файл index.erb со следующим нехитрым содержимым:
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>   
    <link rel="stylesheet" type="text/css" href="stylesheets/styles.css" />
  </head>
<body>
    <h1>Hello World!</h1>
</body>
</html>


А в папке public/stylesheets создаем файл styles.css всего с одной строкой:
h1{color:red}


Как видно по расширению, в этом примере используется движок Erb, но вы конечно же можете использовать Haml, Slim или все, что вашей душе угодно, предварительно включив соответствующую запись в Gemfile.

Чтобы action рендерил содержимое из erb-файла, для начала подключим сам движок:
require ‘erb’


Можно добавить эту строку в сам action, но чтобы использовать Erb во всем поекте удобнее подключить его в application.rb.

Далее изменяем home_action.rb:
class HomeAction < Cramp::Action
 def start
    page = ERB.new(File.read(ExampleProject::Application.root('app/views/index.erb')))
    render page.result(binding)
    finish
 end
end


Здесь ExampleProject — имя самого проекта. Но где же наш css? Чтобы css-файлы были доступны, нужно разрешить раздачу статического контента из содержащей их папки в файле config.ru:
use Rack::Static, :urls => ["/javascripts", "/stylesheets"], :root => CrampArticle::Application.root(:public)


Ну вот, заголовок стал красным, как и ожидалось.
Теперь пойдем дальше и создадим новый action WsAction, который будет общаться с клиентом через веб-сокеты. Для начала установим, на каком сервере будут работать веб-сокеты:
Cramp::Websocket.backend = :thin


Опять же, вместо Thin можно использовать Rainbows, лично я предпочитаю Thin только потому, что он лежит на Github и доков по нему больше.
Далее в папке app/actions создаем файл ws_action.rb и добавляем в него такой код:
class WsAction < Cramp::Action
 self.transport = :websocket

 on_data :process_data

 def process_data(data)
    render "Hello" if /^hello/i =~ data
 end
end


Достаточно добавить в текст класса строку self.transport = :websocket, чтобы он принимал и отсылал сообщения через WebSockets. Затем мы устанавливаем обработчик сообщения и отсылаем приветствие, если было получено сообщение, начинающееся с “hello”.

Чтобы попробовать это в действии, во-первых нужно добавить маршрут(route) для класса WsAction (файл config/routes.rb):
get('socket/').to(WsAction)


А во-вторых добавляем в index.erb клиентский код примерно такого вида:
<script type="text/javascript">
       var ws = new WebSocket("ws://localhost:3000/socket/");
       ws.onopen = function() {
           ws.send("Hello!");
       };
       ws.onmessage = function(e) {
           alert(e.data);
       };
       ws.onclose = function() {
           alert("closed");
       };
</script>


Для Firefox’а последних версий WebSocket заменяем на MozWebSocket. Клиентский код очень прост и представлен исключительно для демонстрации(как собственно и серверный), поэтому условие для класса веб-сокетов или бэкграунд на флэше в него не входят, сделайте это сами.

На этом все, если вы захотите более углубленно изучить Cramp, есть еще несколько тем, не затронутых в данной статье: Server-Side Events, файберы, работа с ActiveRecord. Ну и конечно же вы можете помочь развитию проекта с помощью bug report’ов и pull request’ов на гитхабе.

Дополнительные ссылки


Код проекта
github.com/lifo/cramp

Сайт проекта(документация, ссылки на примеры)
cramp.in

Что такое EventMachine и как его использовать
github.com/eventmachine/eventmachine/wiki

Websockets with Cramp
vinsol.com/blog/2011/05/09/websockets-with-cramp

Еще одна небольшая статья про Cramp и WebSockets
boldr.net/html5-websockets-cramp
Теги:
Хабы:
Всего голосов 43: ↑41 и ↓2+39
Комментарии12

Публикации

Работа

Программист Ruby
6 вакансий
Ruby on Rails
5 вакансий

Ближайшие события