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

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

Я написал веб-сервер, который ничего не умеет

Октябрь 21st, 2010

И, спрашивается, зачем?

А затем, чтобы рассказать на примере, как обрабатываются GET-запросы. Пока что мой сервер ничего не умеет, кроме обработки GET-запросов и вывода информации о клиенте, он даже не читает никакие файлы с диска. Его задача другая, показать, как происходит обработка запросов. Я это сделал потому, что как оказалось, далеко не все веб-разработчики понимают, как это происходит. Мне вот повезло, давным давно приходилось разбирать аж multipart-запросы и обрабатывать заголовки, приходящие на сервер - в бытность, когда писал свою библиотеку на Perl.

Но сейчас я сделал пример на Ruby и не поймет его только баран (или обезьяна). Я постарался максимально документировать его, чтобы было все понятно.

require 'socket' # Из стандартной библиотеки Ruby

# Открываем TCP сокет (Это можно было сделать и по-другому, но я решил
# не создавать сокет вручную, а воспользоваться более высоким уровнем).
# Как видно, сокет будет ожидать соединения (т.к. он является сервером) на
# localhost на 8000-м порту.
server = TCPServer.open('localhost', 8000)

# Бесконечный цикл.
# После того, как соединение от клиента будет принято - клиенту
# будет возвращен ответ и соединение закроется - программа перейдет
# в начало цикла, чтобы ожидать новый запрос
loop do
  # Принимаем входящее соединение клиента. Пока соединение не будет принято,
  # сокет будет ожидать его, и код ниже этой строки
  # не будет выполняться.
  client = server.accept
  client_headers = []

  # Получаем данные клиента построчно
  while line = client.gets
    client_headers << line

    # Протокол HTTP предписывает \r\n как
    # перевод строки — всегда, независимо от операционной системы.
    # Поэтому, тело запроса нам не нужно (т.к. мы обрабатываем
    # только GET-запросы. Получить тело нам понадобилось бы
    # только в POST-запросе, а в GET-запросе тела просто-напросто нет).
    break if line == "\r\n"
  end

  # Поспим 10 секунд. Я сделал это для того, чтобы проиллюстрировать кое что.
  # Попробуйте одновременно обратиться в 2-х закладках браузера к веб-серверу и
  # вы увидите, что второй запрос будет происходить в два раза дольше
  # первого, т.к. он будет дожидаться, пока обработается первый запрос. Чтобы запросы
  # обрабатывались параллельно (т.е. не стояли в очереди) - можно сделать
  # несколько тредов (воркеров).
  sleep 10

  # Возвращаем ответ клиенту и закрываем соединение. В виде тела ответа
  # показываем клиенту его же заголовки.
  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

Я создал TCP-сервер при помощи объекта TCPServer, но можно было сделать и на чистом объекте Socket. Просто TCPServer, который наследуется от TCPSocket, а тот, в свою очередь, от Socket - уже настроен для того, чтобы быть сервером, то есть он умеет висеть на определенном хосте и порту и ожидать входящие соединения.

Из кода видно, что:

  • Веб-сервер обрабатывает только GET-запрос
  • Он не читает никаких файлов с диска, а просто отображает информацию о клиенте, который к нему присоединился
  • Всегда отвечает со статусом 200

Можно сохранить код в файл server.rb, а затем запустить его:

ruby server.rb

Про диджеев

Октябрь 11th, 2010

Я вообще не уважаю диджеев, которые сводят чужие треки и играют их в клубах. Офигеваю за тех, кто может сказать: “Пошли в клуб N, там будет играть такой-то Вася.”. Это особенно заметно в среде т.н. клабберов. При том все достижения этого Васи заключаются в том, что он научился сводить треки и стоять за пультом с таким видом, как будто управляет АЭС, иногдя отходя покурить или перетереть с местными свистками. Для меня это чмыри.

А уважаю тех, кто пишет и играет свою музыку, особенно trance :)

Озвучивание интерфейсов в HTML5, мысли

Октябрь 4th, 2010

Я тут подумал давеча, что связи с появлением Audio Data API в HTML5, можно будет делать очень клевые штуки. Только представьте: различные меню, всплывающие окна, accordion’ы, notices - все это можно сопровождать различными звуками! Блин, как же классно будет :)

Я сейчас работаю над одним проектом и в будущем хочу внедрить в него озвучку интерфейса. Сходу в гугле не нашел подобных сайтов с озвучкой на HTML5 API, но если вы видели подобное, пишите в комменты, с удовольствием гляну.

Антибот оказался успешен

Октябрь 1st, 2010

Меня тут достали совсем эти блогоспамеры, хоть и ни один их коммент в этом блоге не прошел - я модерирую все комментарии. Но просто достало удалять по 20 спамных комментов в день, когда удаляешь, то краем глаза все равно читаешь их, а я этого терпеть не могу, поэтому поставил антибот плагин для WP, который абсолютно незаметен для человека, пишущего комментарий. Но вот робот отправить коммент уже не может - слишком тупой.
Как ни странно - это решение избавило меня от спам-комментов на 100%. И я доволен.

Прекрасные контроллеры в Rails 3

Сентябрь 3rd, 2010

Зацените, кто еще не видел, насколько меньше кода стало в контроллерах Rails 3! Теперь можно сказать, что контроллеры стали по-настоящему соответствующими принципу DRY. И всё это благодаря респондерам. В общем, мне нравится:

class ProductsController < ApplicationController
  respond_to :html, :xml  

  def index
    respond_with(@products = Product.all)
  end  

  def show
    respond_with(@product = Product.find(params[:id]))
  end  

  def new
    respond_with(@product = Product.new)
  end  

  def create
    @product = Product.new(params[:product])
    flash[:notice] = "Successfully created product." if @product.save
    respond_with(@product)
  end  

  def edit
    respond_with(@product = Product.find(params[:id]))
  end  

  def update
    @product = Product.find(params[:id])
    flash[:notice] = "Successfully updated product."
      if @product.update_attributes(params[:product])

    respond_with(@product)
  end  

  def destroy
    @product = Product.find(params[:id])
    @product.destroy
    flash[:notice] = "Successfully destroyed product."
    respond_with(@product)
  end
end  

В респондерах теперь содержится вся та магия по отдаче ответа клиенту, которая раньше находилась в контроллере, в блоке, передаваемому методу respond_to. Сейчас стало удобно контролировать отдачу типов контента - ну нужно править каждый метод контроллера, если вы захотите добавить или убрать тип контента. Кстати, метод respond_to поддерживает ключи :only и :except, чтобы контролировать типы контента для методов.
Самое главное - это все работает, и работает замечательно! Я сейчас пишу проект на Rails 3 и использую всю эту красоту. В общем - радости нет предела)

Рекомендую почитать вот эту статейку на английском: Controllers in Rails 3

Отличная презентация об улучшениях в коде проекта на Rails 3

Август 31st, 2010

Смотрим, как нужно писать код теперь. Мне презентаха очень понравилась!

Installing Ruby 1.9.2 and Rails 3 stable on Ubuntu

Август 31st, 2010

Let’s install Ruby 1.9.2 and Rails 3 stable on Ubuntu. I’m going to use just one Ruby version so, this installation without RVM (Ruby Version Manager). I’m using Ubuntu 10.04, 32 bit version.

If you have not yet installed the following packages - install them:

$ sudo apt-get install gcc g++ build-essential libssl-dev libreadline5-dev zlib1g-dev linux-headers-generic libsqlite3-dev

Now download Ruby 1.9.2 sources, unpack them and install:

$ wget ftp://ftp.ruby-lang.org//pub/ruby/1.9/ruby-1.9.2-p0.tar.gz
$ tar -xvzf ruby-1.9.2-p0.tar.gz
$ cd ruby-1.9.2-p0/
$ ./configure --prefix=/usr/local/ruby
$ make && sudo make install

Add path to binary Ruby files.

$ sudo gedit /etc/environment

You need to add in the PATH variable that path - /usr/local/ruby/bin, should look something like this:

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/ruby/bin"

Then run the source command for the file /etc/environment to apply changes.

$ source /etc/environment

Now check is Ruby installed properly:

$ ruby -v

You should see something like this: ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]
Now create a symbolic link for ruby and gem program

$ sudo ln -s /usr/local/ruby/bin/ruby /usr/local/bin/ruby
$ sudo ln -s /usr/local/ruby/bin/gem /usr/bin/gem

Ruby 1.9.2 is already includes Rubygems, so you do not have to install it.
Now install the required gem packages, including Rails 3.:

$ sudo gem install tzinfo builder memcache-client rack rack-test erubis mail text-format bundler thor i18n sqlite3-ruby
$ sudo gem install rack-mount --version=0.4.0
$ sudo gem install rails --version 3.0.0

Check Rails version:

$ rails -v

You should see the version number 3.0.0. Otherwise, try to execute command source /etc/environment and enter rails -v command once again.
Now you are ready to create a new Rails 3 application:

$ rails new myproject
cd myproject
rails server

UP: To update rails to latest version (3.0.3 for now), run:

sudo gem update rails

And change rails gem in your Gemfile to gem ‘rails’, ‘3.0.3′