Posts Tagged ‘ruby’

Кое-что из моей работы

Вторник, Январь 17th, 2012

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

О чем я хочу рассказать:

  • Как использовать state_machine в моделях ActiveRecord
  • Как использовать counter_cache “с условиями”

Итак, распишем задачу более подробно:

  • Будет две модели, Consultor и ConsultorQuestion
  • ConsultorQuestion может быть опубликованным и неопубликованным. Используем state_machine.
  • По-умолчанию показываются только опубликованные вопросы. Используем default_scope.
  • У Consultor будет счетчик counter_cache, с количеством опубликованных вопросов

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

Модель Consultor (консультант):

# Консультант наследуется от Пользователя (STI).
class Consultor < User
  has_many :questions, :class_name => 'ConsultorQuestion', :dependent => :destroy

  default_scope where('type = ?', 'Consultor')
end

Модель Consultor использует STI (Single Table Inheritance) - наследуется от модели User, так как консультант это такой же пользователь, как и все остальные. Я создал отдельную модель для консультантов потому, что у консультантов есть вопросы (has_many :questions), а у обычных пользователей их нет. Незачем засорять модель User ненужными связями.

Я также определил default_scope, чтобы всегда по-умолчанию выбирались только консультанты, исключая других пользователей. С default_scope сразу начинаются проблемы, так как этот скоуп добавляет условие в каждый SQL-запрос к таблице consultors. И счетчик counter_cache перестаёт работать. Но об этом позже.

Об использовании state_machine

Давайте посмотрим на модель ConsultorQuestion. Я приведу сразу полную и законченную версию модели, конечно убрав оттуда то, что к делу не относится:

class ConsultorQuestion < ActiveRecord::Base
  state_machine :state, :initial => :unpublished do
    # Состояния
    state :unpublished
    state :published

    # Коллбэки переходов
    after_transition :unpublished => :published,   :do => :inc_counter_cache
    after_transition :published   => :unpublished, :do => :dec_counter_cache

    # События
    event :publish do
      transition :unpublished => :published
    end

    event :unpublish do
      transition :published => :unpublished
    end
  end

  # Перед удалением снимаем с публикации. Если состояние изменится
  # на unpublished, то автоматически произойдет декремент counter_cache
  before_destroy { unpublish! }

  belongs_to :consultor
  attr_protected :state, :consultor_id

  validates :questioner_name, :questioner_email, :question,  :presence => true
  default_scope with_state(:published).order('created_at DESC')

  def inc_counter_cache
    Consultor.unscoped.increment_counter('questions_count', self.consultor.id)
  end

  def dec_counter_cache
    Consultor.unscoped.decrement_counter('questions_count', self.consultor.id)
  end
end

Первое, на что обращаешь внимание, это state_machine. У вопроса могут быть два состояния: опубликован или не опубликован. state_machine очень облегчает переходы от состояния к состоянию, хотя это можно сделать и без неё. У вас получится странный код с кучей условий. Я попытаюсь изобразить, что может получиться, если не использовать state_machine:

class ConsultorQuestion < ActiveRecord::Base
  scope :published, where('state = ?', :published)
  scope :unpublished, where('state = ?', :unpublished)

  default_scope published.order('created_at DESC')

  before_save   :set_counter_cache
  after_destroy :set_counter_cache_for_destroy

  belongs_to :consultor
  attr_protected :state, :consultor_id

  validates :questioner_name, :questioner_email, :question,  :presence => true

  def published?
    state == 'published'
  end

  def unpublished?
    state == 'unpublished'
  end

  def state_was?(value)
    state_was == value.to_s
  end

  def set_counter_cache
    # Для добавляемых записей со статусом published
    if new_record? && published?
      # инкрементируем счетчик
      method = :increment_counter
    end

    # Для изменяемых записей.
    # Если запись была снята с публикации, декрементируем счетчик
    # Здесь можно воспользоваться aasm и переходом состояния (transition)
    if persisted? && state_was?('published') && unpublished?
      method = :decrement_counter
    end

    # Если запись была опубликована, декрементируем счетчик
    if persisted? && state_was?('unpublished') && published?
      method = :increment_counter
    end

    # Обновляем счетчик
    Consultor.unscoped.send(method, 'questions_count', self.consultor.id) if method
  end

  # Для удаляемых записей, которые были опубликованы
  def set_counter_cache_for_destroy
    Consultor.unscoped.decrement_counter('questions_count', self.consultor.id) if published?
  end
end

Ужасно, не правда ли? Зацените, сколько условий в коде и сравните это с предыдущим листингом, где нет ни одного условия и все реализуется при помощи коллбэков! В этом и есть сила state_machine - в вызове коллбэков при смене состояния. Чтобы сменить состояние, нам нужно всего лишь вызвать событие publish! или unpublish! в нужный момент. В нашем же случае мы используем коллбэки чтобы менять значение счетчика counter_cache. Помните, в условии было сказано, что нам нужно подсчитывать только опубликованные вопросы?

О счётчике counter_cache

Вы уже заметили в коде вызовы Consultor.unscoped.decrement_counter и Consultor.unscoped.increment_counter. Так мы меняем значение счетчика counter_cache вручную. Почему? Это стало необходимо потому, что мы используем default_scope в модели и этот скоуп добавляет своё условие к запросу, который обновляет счетчик counter_cache. В результате, счетчик не то что не обновляется, а даже происходит ошибка при выполнении SQL-запроса. Чтобы это исправить, мы вызываем через unscoped стандартные методы increment_counter и decrement_counter. Unscoped отменяет действие default_scope и запрос выполняется без ошибки.

Еще хочется заметить, что метод sizе берет значение из колонки counter_cache. Чтобы получить реальное количество записей в таблице, можно использовать метод length или метод count.

Немного о default_scope

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

Рефакторинг с использованием rails_best_practices

Четверг, Сентябрь 29th, 2011

Думаешь, твой рельсовый проект идеален в плане кода? Нет? Хорошо, что можешь это признать. Я вот знал, что в моём маленьком проекте contemplate, который я мееедленно но верно разрабатываю, были проблемы. В частности - бизнес-логика в контроллере, не DRY-код в некоторых местах и другие проблемы. Теперь я избавился почти от всех них, и помог мне в этом гем rails_best_practices.

Установить и использовать его просто. Добавляешь в Gemfile в группу development строку:

gem "rails_best_practices"

И делаешь bundle install. Затем, в корневой директории проекта запускаешь:

$ rails_best_practices -g

Это сгенерирует конфиг-файл для rails_best_practices. Для начала работы больше настраивать ничего не нужно. Теперь нужно сгенерировать чеклист:

$ rails_best_practices -f html .

После запуска ты получишь файл rails_best_practices_output.html в корне проекта. Просто открываешь его в браузере и смотришь список. Тебе повезет, если он будет пуст. В моем случае было не так - я получил 48 ошибок.

Например, у меня был такой экшен:

def create
  anchor = :new_comment

  # Anti-robot protection
  is_robot = !params[:comment][:name].blank?
  params[:comment].delete :name

  @comment = Comment.new(params[:comment])
  @comment.ip = IPAddr.new(request.env["REMOTE_ADDR"]).to_i
  @comment.user_agent = request.env["HTTP_USER_AGENT"]
  @comment.commentable = @post

  if !is_robot
    if @comment.save
      flash[:notice] = 'Ваш комментарий опубликован.'
      flash.now[:comment] = nil
      anchor = view_context.dom_id(@comment)
    else
      flash[:comment] = @comment
    end
  else
    flash[:comment] = nil
  end

  redirect_to polymorphic_path(@post, :anchor => anchor)
end

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

def create
  anchor = :new_comment

  # Anti-robot protection
  is_robot = !params[:comment][:name].blank?
  params[:comment].delete :name

  @comment = @commentable.comments.build(params[:comment])

  if !is_robot
    if @comment.publish(request.env)
      flash[:notice] = 'Ваш комментарий опубликован.'
      flash.now[:comment] = nil
      anchor = view_context.dom_id(@comment)
    else
      flash[:comment] = @comment
    end
  else
    flash[:comment] = nil
  end

  redirect_to polymorphic_path(@commentable, :anchor => anchor)
end

Вот как выглядит метод Comment#publish

def publish(env)
  raise 'Cannot find associated object for this comment!' if self.commentable.nil?
  require 'ipaddr'

  self.ip = IPAddr.new(env["REMOTE_ADDR"]).to_i
  self.user_agent = env["HTTP_USER_AGENT"]

  save
end

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

В общем, отличная вещь, рекомендую всем, кто еще не пробовал.

BundleWatcher - следите за апдейтами гемов, используемых в проекте

Среда, Июнь 22nd, 2011

Наткнулся на маленький замечательный проект - BundleWatcher.

Давно искал нечто подобное, так как всегда хочется использовать самые новые версии гемов в своем проекте. Чтобы начать пользоваться BundleWatcher, вы просто заливаете ему Gemfile.lock из своего проекта и он генерирует вам страничку, где указаны ваши версии гемов, а так же самые последние версии используемых гемов. Кроме того, генерируется RSS лента, которую стоит добавить в свой ридер.

В общем, всячески рекомендую, и сам буду пользоваться.

Обработка ActionController::RoutingError в Rails 3

Среда, Март 2nd, 2011

Раньше, до третьих рельс, исключение маршрутизатора можно было красиво и спокойно перехватить, чтобы показать страницу с ошибкой 404. Достаточно было поместить в ApplicationController следующее:

rescue_from ActionController::RoutingError, :with => :error_not_found

Ну, и определить метод error_not_found, который бы рендерил страницу с 404 ошибкой. Но в Rails 3 произошло изменение - стал использоваться гем rack-mount, который используется в Rails как middleware, то есть как прослойка между вашим приложением и веб-сервером. И исключение RoutingError переехало именно в middleware, так что теперь его не получится отловить в контроллере, так как это исключение отлавливается и рендерится еще до вызова контроллера.

Разработчик Rails, а именно Хосе Валим, назначил этой проблеме статус low и поместил тикет в milestone 3.1, что означает, что решение разработчики родят только в версии Rails 3.1. На момент написания этой статьи текущая версия - 3.0.5, так что ждать еще достаточно долго. Сейчас разработчики думают над тем, как сделать обработку ошибок маршрутизации по-нормальному. Но Хосе предложил временное решение - использовать globbing в маршрутах. Нужно просто в вашем routes.rb в самом конце прописать это:

match "*path", :to => "application#error_not_found"

И добавить метод home#routing_error по вашему вкусу. Мой таков:


class ApplicationController < ActionController::Base
  def error_not_found
    render :template => "/error/404.html.erb", :status => 404
  end
end

Остальные эксепшены, вроде ActionController::UnknownController перехватываются нормально при помощи rescue_from и их можно преспокойно обрабатывать в ApplicationController, например.

Да, еще есть вот такое решение. С использованием гема vidibus-routing_error можно работать с исключением RoutingError как прежде, через rescue_from.

Шаблоны приложений в Rails 3

Вторник, Март 1st, 2011

Rails 3 вышел несколько месяцев назад, и одним из многих изменений было обновление API для генераторов и шаблонов приложений. Если вы еще не знаете, то теперь в Rails 3 используется Thor для этих целей, что даёт большую модульность и настраиваемость.

Но зачем использовать шаблоны при создании приложения, если в Rails и так достаточно легко все настраивается? Причина здесь в том, что один раз настроив приложение так, как вам нужно, вам не придется в дальнейшем настраивать каждое новое приложение, вы сможете воспользоваться готовым шаблоном и это избавит вас от рутины.

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

Вот так, например, выглядит шаблон, подготовленный Аароном Самнером:

# Создание rvmrc файла
create_file ".rvmrc", "rvm gemset use #{app_name}"

gem "haml-rails"
gem "sass"
# hpricot и ruby_parser используются гемом haml
gem "hpricot", :group => :development
gem "ruby_parser", :group => :development
gem "nifty-generators"
gem "simple_form"
gem "jquery-rails"

# Аутентификация и авторизация
gem "devise"
gem "cancan"

# rspec, factory girl, webrat, autotest для тестирования
gem "rails3-generators", :group => [ :development ]
gem "rspec-rails", :group => [ :development, :test ]
gem "factory_girl_rails", :group => [ :development, :test ]
gem "webrat", :group => :test
gem "ffaker", :group => :test
gem "autotest", :group => :test

run 'bundle install'

rake "db:create", :env => 'development'
rake "db:create", :env => 'test'

generate 'nifty:layout --haml'
remove_file 'app/views/layouts/application.html.erb' # вместо этого используется nifty layout
generate 'simple_form:install'
generate 'nifty:config'
remove_file 'public/javascripts/rails.js' # jquery-rails заменит стандартный prototype-ujs
generate 'jquery:install --ui'
generate 'rspec:install'
inject_into_file 'spec/spec_helper.rb', "\nrequire 'factory_girl'", :after => "require 'rspec/rails'"
inject_into_file 'config/application.rb', :after => "config.filter_parameters += [:password]" do
  <<-eos

    # Настройка генераторов
    config.generators do |g|
      g.stylesheets false
      g.form_builder :simple_form
      g.fixture_replacement :factory_girl, :dir => 'spec/factories'
    end
  eos
end
run "echo '--format documentation' >> .rspec"

# Настройка аутентификации и авторизации
generate "devise:install"
generate "devise User"
generate "devise:views"
run "db:migrate"
generate "cancan:ability"

# Удаляем ненужные файлы, копируем конфиг БД и добавляем его в игнор
remove_file 'public/index.html'
remove_file 'rm public/images/rails.png'
run 'cp config/database.yml config/database.example'
run "echo 'config/database.yml' >> .gitignore"

# Создаем git-репозиторий и делаем первый коммит
git :init
git :add => "."
git :commit => "-a -m 'create initial application'"

say <<-eos
  ============================================================================
  Your new Rails application is ready to go.

  Don't forget to scroll up for important messages from installed generators.
eos

Достаточно просто, не правда ли? Воспользоваться таким шаблоном можно так:

$ wget https://gist.github.com/848735.txt
$ rails new appname -m 848735.txt

Я сейчас создаю специальный гем, который поможет упростить создание новыйх приложений Rails 3 - этот гем называется playmo-rails, и уже можно посмотреть некоторые наработки. Мой гем так же использует Thor для генерации различных файлов. Этот гем создан для быстрого старта - можно создать новое приложения, добавляя в него нужные библиотеки и расширения, просто отвечая на вопросы установщика. И это очень удобно.

Пока playmo-rails умеет не так уж много:

  • Создает приложение и использованием Compass
  • Лайот приложения использует html5 boilerplate
  • Можно выбрать установку JQuery либо Mootools
  • Удаляет ненужные файлы из нового rails-приложения

Планов по playmo-rails достаточно много, например, добавить поддержку других фреймворков, полезных общеиспользуемых хелперов, таких как title, динамические страницы 500, 404, 422. В общем, в скором времени, playmo-rails будет той штукой, которая избавит вас от всех манипуляций которые вы делаете с каждым новым rails-приложением.

bundle exec - чтобы не забыть

Вторник, Февраль 22nd, 2011

Очень помогли сегодня в группе ror2ru.
Я хотел подправить один гем, он у меня лежал на диске, склонированный с гитхаба. Но, чтобы установить этот гем (при установке он генерирует несколько файлов, которые-то и нужно было мне подправить), нужно использовать консольную программу, в моем случае compass, которой указываем название гема, который нужно установить:

compass init rails -r html5-boilerplate ...

Но, так как гем у меня в виде склонированной репы, то естественно, программа compass его “не видит”. Выход нашелся в следующем.

Сначала подключаем в Gemfile требуемый гем локально:

gem 'html5-boilerplate', :path => '~/sandbox/gems/html5-boilerplate'

А затем, запускаем нужную команду через bundle exec:

bundle exec compass init rails -r html5-boilerplate ...

Это запустит вашу команду в контексте текущего Gemfile, поэтому, для программы compass гем html5-boilerplate будет прекрасно “виден”. Полезная штука.

Уведомлялка о новых твитах для Ubuntu на Ruby

Пятница, Декабрь 24th, 2010

Мне надоело постоянно лазать в твиттер, чтобы посмотреть, что там новенького пишут. Поэтому, потакая своей лени, я решил автоматизировать это дело :)
Сразу код:

require 'twitter'
last_created_at = 0

Twitter.configure do |config|
  config.consumer_key       = 'your key here'
  config.consumer_secret    = 'your key here'
  config.oauth_token        = 'your token here'
  config.oauth_token_secret = 'your token here'
end

client = Twitter::Client.new

loop do
  tweet = client.home_timeline.first

  if DateTime.parse(tweet.created_at) > last_created_at
    system("notify-send -u normal -t 5000 -i info '#{tweet.user.screen_name}' '#{tweet.text}'")
    last_created_at = DateTime.parse(tweet.created_at)
  end

  sleep 60
end

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

Как это юзать? Для начала нужно установить:

  • sudo apt-get install libnotify-bin
  • gem install twitter
  • Зарегистрировать свое приложение тут http://twitter.com/apps/new и вставить в скрипт в конфигурацию полученные ключи и токены

Дальше сохранить предоставленный код в файл и запустить его из консоли:

$ ruby twitter.rb&

Результат выглядит так:
2iho

UPD 13.01.2011

Кирилл Никитин прислал мне исправленный вариант, который я реквестовал выше, за что ему решпект.

require 'twitter'

last_id = nil
tweets = []

Twitter.configure do |config|
  config.consumer_key       = 'your key here'
  config.consumer_secret    = 'your key here'
  config.oauth_token        = 'your token here'
  config.oauth_token_secret = 'your token here'
end

client = Twitter::Client.new

loop do
  if last_id
    tweets = client.home_timeline :since_id => last_id
  else
    tweets << client.home_timeline.first
  end

  last_id = tweets.first.id unless tweets.empty?

  tweets.reverse.each do |tweet|
    system("notify-send -u normal -t 5000 -i info '#{tweet.user.screen_name}' '#{tweet.text}'")
  end

  tweets.clear
  sleep 60
end

Jabber via Websockets: создаём прокси-сервер

Понедельник, Ноябрь 1st, 2010

Продолжу размышления по моей предыдущей заметке и попытаюсь что-нибудь написать.

Я посмотрел существующие Jabber-серверы и ни у одного не обнаружил поддержку вебсокетов. Думаю, это просто вопрос времени, когда разработчики допишут поддержку вебсокетов в свои jabber-серверы. Можно написать свой джаббер-сервер, если совсем делать нефиг, можно ждать, когда разработчики добавят поддержку вебсокетов. Но мы не будем их ждать и сделаем эту поддержку сами. Но как? Первое что приходит в голову - создать прокси-сервер, который будет уметь получать вебсокет-запросы и перенаправлять их jabber-серверу.

Для тестов я поставил jabber-сервер ejabberd на локальную машину, хотя можно было воспользоваться и многочисленными узлами, типа jabber.ru. Также я поставил клиент Psi и зарегистрировался при помощи него на локальном сервере.

Итак. Давайте для начала создадим html5 документ, который будет посылать запрос на наш jabber-прокси-сервер:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Jabber Websocket Proxy Test</title>
    <script src="https://ajax.googleapis.com/ajax/libs/mootools/1.3.0/mootools-yui-compressed.js"></script>
  </head>
  <body>
<script>
var socket = new WebSocket('ws://localhost:6222/');
socket.onopen = function() {
  var xml  = "<?xml version='1.0' encoding='UTF-8'?>"
      xml += "<stream:stream to='localhost' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' xml:lang='ru' version='1.0'>";
  socket.send(xml);
  socket.onmessage = function(e) {
    var data = e.data
    alert(data)
  }
}
</script>
  </body>
</html>

Как видно из примера, мы будем посылать кусок xml на сервер (начало xml-потока), что означает, что мы готовы авторизоваться на сервере. В ответ jabber-сервер должен нам вернуть некий ответ, в данном случае - список поддерживаемых механизмов аутентификации на сервере. Мы просто выведем полученный с сервера ответ при помощи js-функции alert() на экран. Этого нам будет достаточно,чтобы убедиться, что наш прокси работает.

Теперь приведу код jabber-via-websocket сервера на Ruby:

require 'em-websocket'
options = {
  :port        => 6222,
  :remote_host => 'localhost',
  :remote_port => 5222
}
EventMachine.run {
  class Server < EventMachine::Connection
      def initialize(input, output, server_close, client_close)
        @input        = input
        @output       = output
        @server_close = server_close
        @client_close = client_close
        @input_sid = @input.subscribe { |msg| send_data msg }
        @client_close_sid = @client_close.subscribe { |msg| close_connection }
      end
      def receive_data(data)
        @output.push(data)
      end
      def unbind
        @input.unsubscribe(@input_sid)
        @client_close.unsubscribe(@client_close_sid)
      end
  end

  EventMachine::WebSocket.start(:host => "0.0.0.0", :port => options[:port]) do |ws|
    ws.onopen {
      output       = EM::Channel.new
      input        = EM::Channel.new
      server_close = EM::Channel.new
      client_close = EM::Channel.new
      output_sid = output.subscribe { |msg| ws.send msg }
      server_close_sid = server_close.subscribe { |msg| ws.close_connection }
      EventMachine::connect options[:remote_host], options[:remote_port], Server, input, output, server_close, client_close
      ws.onmessage { |msg| input.push(msg)}
      ws.onclose {
        output.unsubscribe(output_sid)
        server_close.unsubscribe(server_close_sid)
      }
    }
  end
}

Не буду подробно описывать код. Скажу лишь, что он перенаправляет все поступившие запросы с 6222 порта на порт 5222 (стандартный порт jabber).
Остается запустить из консоли наш прокси-сервер (ruby proxy.rb) и html5 пример в google chrome и убедиться, что все работает. Мы должны увидеть алерт со следующим содержимым:

<?xml version='1.0'?>
<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='749528783' from='localhost' version='1.0' xml:lang='en'>
    <stream:features>
        <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
            <mechanism>PLAIN</mechanism>
            <mechanism>DIGEST-MD5</mechanism>
        </mechanisms>
        <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://www.process-one.net/en/ejabberd/' ver='8P/XuMtKq0lNk50DLBC8v+TXoAU='/>
        <register xmlns='http://jabber.org/features/iq-register'/>
    </stream:features>

Это был первый шаг в сторону создания инструмента, позволяющего общаться через jabber прямо с веб-страницы. Продолжение следует.

Собираюсь писать клиентскую библиотеку для работы с XMPP на JS

Воскресенье, Октябрь 31st, 2010

Да, есть такое желание. Объясню, зачем мне захотелось такого извращения.

Я посмотрел все существующие библиотеки на JS для работы с XMPP. Все эти библиотеки стремятся отсылать запросы через BOSH и похожие технологии, но ни в одной из них не реализованы вебсокеты.

Я же хочу простого: пусть у меня будет имплементация jabber-мессенджера на сайте, которая будет работать в реальном времени. Я не хочу отсылать xmpp-запросы с сайта прямо на Jabber-сервер. Вместо этого я хочу написать ретранслятор, который был бы посредником между клиентом (то есть веб-страницей) и jabber-сервером. Эта штука всего лишь будет передавать запросы от клиента jabber-серверу, а от jabber-сервера получать ответы и передавать их клиенту. Мой ретранслятор должен быть выполнен в виде вебсокет-сервера, потому что это обеспечивает наивысшую скорость и убирает весь оверхед, в отличие от того же BOSH.

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

Вернемся обратно к клиентской части на JS. От нее мне хочется простого - чтобы она умела парсить XMPP, а так же формировать его. Она не будет ничего передавать - только парсить и генерировать. Как дополнение - можно сделать обсервер, который бы реагировал на определенные события, чтобы можно было удобно обновлять интерфейс клиента - но это в виде отдельного компонента.

Я планирую все обдумать и, если мое решение не изменится - открыть проект на Github. Посему, мне требуются помощники, возможно - контрибуторы. Люди нужны такие - те, кто разбираются в XMPP, javascript-разработчики, ruby-разработчики, понимающие в создании socket-серверов.

Немного расскажу о том, почему та штука, которую я хочу сделать, будет прикольна. Достаточно в недалеком будущем мы увидим нечто новое в веб: веб-приложения начнут себя вести совсем как десктопные и даже круче - интерфейс будет обновляться в реальном времени без перезагрузки страницы. Добавил кто-то товар - хоп - в списке товаров появился новый. И так далее. Я считаю, что XMPP подходит для этого весьма и весьма хорошо. Достаточно посмотреть вон там, чтобы убедиться, как будет в скором времени круто.

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

Ссылки:

  1. RFC 3920 — Протокол XMPP: Ядро
  2. XMPP Libraries

Параллельная обработка запросов в Ruby

Пятница, Октябрь 22nd, 2010

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

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

Что такое треды?
Треды - это программные потоки, которые выполняются внутри одного процесса, они обеспечивают параллельное выполнение кода. Это очень важно. Именно потому что треды выполняются внутри одного процесса, расходуется меньше памяти, да и создать новый тред и переключиться в него быстрее, чем создать новый процесс. Поэтому, все современные веб-серверы и другие серверы используют в своей работе треды.
Возможность создавать треды предоставляет нам операционная система, поэтому, нужен низкоуровневый язык (например C), чтобы работать с тредами. Однако, Ruby предоставляет нам интерфейс к тредам, поэтому, мы можем использовать их и в Ruby.

Давайте добавим в веб-сервер парралельности, тогда он сможет обрабатывать большее количество соединений за единицу времени. Я уберу старые комментарии из кода и добавлю туда поддержку тредов.

require 'socket'
server = TCPServer.open('localhost', 8000)

loop do
  # Как только сервер принимает соединение с клиентом,
  # тут же создается новый тред. И так будет для каждого
  # соединения с веб-сервером. Каждому соединению - отдельный тред.
  Thread.start(server.accept) do |client|
    client_headers = []

    while line = client.gets
      client_headers << line
      break if line == "\r\n"
    end

    client.print "HTTP/1.1 200/OK\n"
    client.print "Content-type: text/html\n\n"
    client.print client_headers.join("<br/>")
    client.close
  end
end

Итак, мы получили парралельную обработку запросов и увидели, как обрабатывается запрос.