Comet сервер на эрланге

в 18:44, , рубрики: comet, cowboy, erlang, Erlang/OTP, message queue, Веб-разработка, метки: , , ,

Статья для людей, только начинающих знакомиться с эрлангом: как написать простой comet сервер.

Готовый код здесь: github.com/maxlapshin/comet

Описание

Комет-сервер будет написан с использованием cowboy, tinymq, и куска жабаскрипта.

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

Сама статья пишется параллельно с кодом, что заметно из истории в гите.

Логика такая: по http постим сообщение на сервер, оно попадает в очередь сообщений, откуда его выгребает клиент через long-poll запрос.

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

Всякая всячина чтобы начать работу

Вначале нужна всякая рутина: rebar, зависимости.

<code>
mkdir comet
cd comet
git init
wget https://github.com/basho/rebar/wiki/rebar
chmod +x rebar
</code>

Дальше создаем rebar.config, чтобы установить зависимости.

<code>

{lib_dirs, ["apps", "deps"]}.
{sub_dirs, [
  "apps/comet"
]}.
{erl_opts, [
            debug_info,
            warn_format,
            warn_export_vars,
            warn_obsolete_guard,
            warn_bif_clash
           ]}.

{deps, [
  {cowboy, "0.6.*", {git, "git://github.com/extend/cowboy.git", {tag, "0.6.1"}}},
  {tinymq, ".*", {git, "git://github.com/evanmiller/tinymq.git", "386813add4"}}
]}.
</code>

За время написания статьи в tinymq были внесены изменения в API. Лочить конкретный коммит рекомендуется!

И вытаскиваем зависимости:

<code>
./rebar get-deps
</code>

Сразу же создаем и Emakefile. Многие отказываются от использования emake, а я нет и позже покажу, почему я так делаю.

<code>
{"apps/comet/src/*", [debug_info, {outdir, "apps/comet/ebin"}, {i, "apps/comet/src"}]}.
{"deps/cowboy/src/*", [debug_info, {outdir, "deps/cowboy/ebin"}, {i, "deps/cowboy/src"}]}.
{"deps/tinymq/src/*", [debug_info, {outdir, "deps/tinymq/ebin"}, {i, "deps/tinymq/src"}]}.
</code>

Теперь осталось создать костяк для нашего приложения и всё будет ок.

<code>
mkdir -p apps/comet
cd apps/comet
../../rebar create-app appid=comet
cd -
</code>

Добавляем deps и ebin в .gitignore и компилируем то, что получилось:

<code>
./rebar compile
</code>

Момент отмечен тегом v1.

Запуск

В плане того, как запустить приложение на эрланге есть некоторый разброд и шатание. В том смысле, как именно инфраструктурно оформить запуск.

Я как правило для девелопмента делаю Makefile с правилом run в котором указываю следующее:

<code>
ERL_LIBS=apps:deps erl +K true -name comet@127.0.0.1 -boot start_sasl -s comet_app -sasl errlog_type error
</code>

Важно понять следующее: у эрланга нет девеломпент режима и продакшн. Вся эта билиберда, которой многие фреймворки морочат голову, отсутствует.

Теперь детальнее насчёт опций.

ERL_LIBS=apps:deps указывает эрлангу каталоги для поиска библиотек. Библиотека — это такой каталог, в котором есть ebin.

+K true указывает что можно пользоваться epoll/kqueue

-name comet@127.0.0.1 — длинное имя с явным указанием хоста

-boot start_sasl - запомнить и повторять

-s comet_app — запустить функцию start/0 из модуля comet_app (на данный момент её ещё нет)

-sasl errlog_type error позволяет убрать шквал из ненужных сообщений о том, что всё хорошо.

Теперь для того, чтобы всё запустить, надо добавить функцию start/0 в comet_app.erl:

<code>

-export([start/0]).

start() ->
  application:start(comet).
</code>

После этого компилируем всё:

<code>
> ./rebar compile
> make run

ERL_LIBS=apps:deps erl +K true -name comet@127.0.0.1 -boot start_sasl -s comet_app -sasl errlog_type error
Erlang R15B (erts-5.9) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:true]

Eshell V5.9  (abort with ^G)
(comet@127.0.0.1)1> 
</code>

Т.е. наше пустое приложение запустилось. Выйти из него Ctrl+c или halt(). в консоли.

Метка v2

Запуск веб-сервера

С инфраструктурой пока закончили, перейдем к запуску веб-сервера.

Из старта нашего приложения будем запускать ковбой с несколькими хендлерами, но для этого надо сначала добавить ещё одно приложение mimetypes (https://github.com/spawngrid/mimetypes), чтобы правильно отдавать статические файлики.

Добавляем строчку {mimetypes, ".*", {git, "git://github.com/spawngrid/mimetypes.git"}} в rebar.config и вытаскиваем: ./rebar get-deps

Пишем нужный код в comet_app.erl, добавляем минимальный код в www/index.html и запускаем.
make run и идем на localhost:8080/

Метка v3

Пишем сообщения в очередь

Добавляем jquery, форму и отдельный хендлер sendmessage_handler для урла /sendmessage

Сам модуль пишем четко по инструкции ковбоя.

С ковбоем важно помнить одну очень, очень важную вещь: все функции из API ковбоя возвращают ответ и новое состояние. Это делается для того, чтобы парсить все данные по запросу.
Важно то, что надо всегда использовать новое состояние, иначе есть шанс здорово продолбаться.

Теперь важно рассказать, зачем Emakefile. Есть разные подходы к тому, как релоадить код в девелопменте. Мне нравится только штатный make:all([load]).

Другие варианты — reloader или sync. Все они плохи тем, что релоадят код черти когда, причем черти какой. sync так просто валит в корку из-за каких-то проблем с ssl. Именно для работающего make:all([load]). (эту команду надо запускать в консоли эрланга) и делали Emakefile

Дальше надо включить очередь сообщений. Мы добавим её в супервизор нашего приложения comet_sup.

Теперь можно добавлять непосредственно логику комета:

<code>
  {Post, Req2} = cowboy_http_req:body_qs(Req),
  {<<"body">>, Body} = lists:keyfind(<<"body">>, 1, Post),
  tinymq:push(<<"default_channel">>, Body),
</code>

Метка v4

Вычитываем сообщения из очереди

В комете возможны такие ситуации:
1) пришли, а там уже для нас есть сообщения в очереди
2) пришли, сообщений нет, ждем когда появятся.

Для того, чтобы понять, что мы уже видели сообщения, надо использовать сортируемые идентификаторы. Самый надежный в нашем случае — реальное время. Именно так и работает tinymq.

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

Давайте ещё раз подробнее разберем, что именно происходит.

1) клиент приходит к /comet, имея или не имея последний таймстемп на руках
2) хендлер проверяет с помощью tinymq:poll, есть ли сообщения новее, чем этот таймстемп.
3) если есть, то отвечает простым механизмом через handle/2
4) если таких сообщений нет, то подписывается на доставку сообщений через tinymq:pull и повисает в цикле loop/3
5) когда приходит нужное сообщение, пакует его в JSON и отправляет вместе с новым таймстемпом клиенту

Для упаковки в json добавлен модуль mochijson2

Метка v5

Осталось только HTML для этого приделать.

HTML для комета

Тут есть куча ньюансов, которые все ходят вокруг одного и того же: как не свалить сервер слишком частыми запросами. Т.е. после аварийного отсоединения надо немного подождать. Но мы это всё оставим на потом, сначала просто подпишемся на комет.

Метка v6

Что дальше?

Итак, вчерне минимальный пример работающего комета готов. В таком виде запускать в продакшн нельзя, надо доделать:
1) авторизацию
2) указание канала

После этого описанный здесь код можно выкатывать на продакшн. Да-да, эрланг хорош тем, что как правило прототип можно кидать в бой.

Для начала всё, дописывать пример можно будет по мере поступления вопросов.

Автор: erlyvideo

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js