Previous Entry Share Next Entry
Чат на Erlang и Web Sockets
elisitsky
Данный чат в первую очередь задумывался как демонстрационный пример для моего доклада про веб сокеты на РИТ-2010. То есть в первую очередь он создавался чтобы протестировать веб сокеты, проверить как будет работать event-driven система, состоящая из брауера, сервера и среды передачи данных. То, что это все хорошо работает по отдельности - уже давно известно и не интересно.
Выбор Эрланга для написания сервера обусловлен двумя вещами:
- во-первых, он мне очень интересен и хотелось написать несложную программу в самообразовательных целях :)
- во-вторых, сама модель работы процессов и асинхронных коммуникаций в Эрланге как нельзя лучше подходит для реализации событийно-ориентированных вещей.
Поскольку пример демонстрационный, то дизайн сделан довольно спартанский - ровно такой, чтобы не мешал. Аналогично не делалось никакой оптимизации, защиты от хаксоров и всего прочего, что необходимо в бою.

Посмотреть можно вот тут: http://chat.websockets.ru

Как это все работает? При заходе на страницу открывается веб сокетное подключение к серверу (Если на Земле еще остались люди, которым я не прожужжал уши про эту технологию, то на том же сайте есть введение в веб сокеты ;-).

Приняв подключение, сервер добавляет процесс, отвечающий за коммуникацию с клиентом, в свой список клиентов, а потом присылает его пользователю. Для этого он посылает (в эрланговском смысле: Pid ! Msg) сообщение процессу клиента с подготовленным списком.

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

Вам не кажется странным, что в две первые вещи, которые нужны пользователю для начала работы в чате - список других юзеров и архив, присылаются по-разному? В одном случае их явно просит клиент, в другом случае - сервер просто их присылает. Так сделано по двум соображениям: при любом изменении списка клиентов все равно эту информацию сервер должен разослать по всем, а запрос архива идет с клиента, потому что клиент может и не затребовать архив, либо клиент может переключиться в другую комнату, да и просто чтобы можно было опробовать такой вариант.

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

Если пользователь не авторизован, то система не принимает от него сообщения. Состояние авторизации хранится в record`е - пока это еще рано называть FSM`ом. Изначально мы находимся в состоянии, в котором отказываем пользователю в отправке сообщения - возвращаем ошибку. Это можно легко проверить если с помощью FireBug или DevTools сделать видимым слой #messagediv и отправить сообщение.

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


Код выложен на гитхабу: http://github.com/lisitsky/wisechat/blob/master/src/wisechat.erl
Я буду очень благодарен нашим гуру, если они устроят жесткую немецкую небольшую "порку".

  • 1
> -define(ub(X), unicode:characters_to_binary(X)).

Этих вот unicode:XYZ функций не надо вовсе. Смысла в них ноль.

> Conn_Manager = spawn(fun() -> conn_manager() end),

Conn_manager = spawn(fun conn_manager/0),

> register(conn_mgr, Conn_Manager),

erl -man gen_server, уверяю, это просто для головы, и лучше для дела.

> continue_work() ->
> receive

Этот процессинг не имеет таймаута (after timeout), что есть плохо. Кроме того, при hot code upgrade процесс, крутящий этот цикл, будет грязно падать.

> process_flag(trap_exit, true),

Гораздо корректнее использовать erlang:monitor, и ловить 'DOWN', чем ловить 'EXIT' после выставления trap_exit. Так как erlang:monitor внизу и делается, рекомендую выбросить process_flag(trap_exit, true) вовсе из кода.

> conn_manager(CurrentState) ->

Опять же, дешевле, нагляднее и лучше использовать gen_server.

> Res = lists:keytake(Pid, 2, CurrentClients),

Дикая позиционщина. Что за "2"? Я бы весь блок внизу переписал так:
{set_opts, Pid, Session} ->
  NewClients = lists:map(fun
    (#client{pid = P} = Client) when P == Pid ->
        Client#client{name=Session#session.name, color=Session#session.color};
    (E) -> E end, CurrentClients)
  clients_send_list(all, NewClients),
  conn_manager(CurrentState#cm_state{clients=NewClients});

> {'DOWN', Ref, process, Pid, Reason} ->

erlang:demonitor(Ref) ниже не обязателен.

> TakeNum = case Num of
> _ when Num > 0 ->

Не люблю эрланговский if, но здесь явно просится:

TakeNum = if Num > 0 -> Num; true -> 0 end,

или даже

TakeNum = case Num > 0 of true -> Num; false -> 0 end,


> NewList = List ++ [Message],

Можно хранить историю в развёрнутом виде ([Message | List]), чтобы заменить O(N) на O(1), либо даже воспользоваться erl -man queue.


Огромное спасибо, Лев!

> Этих вот unicode:XYZ функций не надо вовсе. Смысла в них ноль.
Я попробую по вашей рекомендации все перенести на бинари. Здесь у меня просто часть операций делалась со списками, а часть с бинари, поэтому приходилось конвертировать.

> Conn_Manager = spawn(fun() -> conn_manager() end),
> Conn_manager = spawn(fun conn_manager/0),
В первом случае мы забиваем память анонимной функцией?

> erl -man gen_server, уверяю, это просто для головы, и лучше для дела.
Что-то он у меня не лезет в голову :)
Непонятен вот такой момент - у меня будет некое глобальное состояние сервера (State), которое будут получать мои callback`и. То есть, все, что сейчас раскидано и хранится в отдельных процессах будет храниться в единой переменной. Не станет ли от этого программа хуже работать? По идее должна испортиться асинхронность - т.к. все запросы должны будут работать с этой переменно и они будут вынуждены выстраиваться в очередь. Параллельно тоже будет обрабатваться один запрос?
Можно ли эту переменную разобраться на отдельные состояния? Например, если я добавлю поддержку нескольких комнат в чат, то мне нужно будет заводить несколько таких менеджеров клиентов. Мне как-то хочется держать их состояния отдельно, пусть в gen_server, но не в общей куче.
Почему-то я жду жутких тормозов, когда все процессы начнут копаться в этой куче ради изменения пары полей.

> continue_work() ->
> receive
Это чудовищный хак - мне нужно было как-то демонизировать процесс, иначе он постоянно завершался. Я это сделал загнав его в вечный цикл с выходом по сигналу.
как это делается по человечески?

> Гораздо корректнее использовать erlang:monitor, и ловить 'DOWN', чем ловить 'EXIT' после выставления trap_exit. Так как erlang:monitor внизу и делается, рекомендую выбросить process_flag(trap_exit, true) вовсе из кода.
Ясно. идея была в том, чтобы при ошибке в нижележащих процессах conn_manager не упал. В принципе link не используется, поэтому действительно trap_exit стоит выкинуть.

> Дикая позиционщина. Что за "2"? Я бы весь блок внизу переписал так:
Согласен, потом вспоминать все индексы и править их по всей проге станет ужасом. От этого необходимо избавляться.
Скажите, а насколько быстр будет этот код? почему-то я жду, что он будет пробегать по всему списку проверять все элементы - O(N), а значит жду тормозов при больших списках.
Вот здесь мне как-то очень хочется использовать хеш или аналог, чтобы обратиться адресно к нужному элементу и получитить трудоемкость O(1).

> Не люблю эрланговский if, но здесь явно просится:.....
Ясно

> ...воспользоваться erl -man queue....
Изучу.

P.S.
После ваших комментариев я понял одну простую вещь и смешную вещь - я упорно пытаюсь писать на Эрланге императивно. "взять элемент" - "сделать то" - "положить на место".

Вобщем это все надо перепродумать и переписать с чистого листа.


я форкнул проект на гитхабе, поковыряю на предмет "включения" otp :) как будет что интересного, дам знать...

Обязательно расскажите.
Я тоже решил двигаться в сторону OTP, пока некоторые детали не ясны, но будем разбираться.

А как нужно изменить скрипт, чтобы сделать возможным одключение БД на postgreSQL? Вы все еще занимаетесь Erlangом? как успехи с OTP?

Добрый день!
Поздно заметил ваш комментарий - сейчас вообще не бываю в ЖЖ.
Нет, сейчас не занимаюсь Erlang - все же слишком узкая и специфичная ниша. Лично мне сейчас Golang видится более перспективным.

(Deleted comment)
(Deleted comment)
Какая версия оперы? В виндовой 10.51 есть проблема - она не выдерживает потока событий при загрузке истории чата. Вероятно проблема в кешировании ДОМ.
Есть мысль как обойти, но буду это делать уже в новой версии.
А что у вас показывается? Какие ошибки?

Тестировал во все ослах - все отлично даже в 6.

(Deleted comment)
У меня маковская 10.10 - она просто тупит, но работает. А у народа с вин 10.51 были проблемы с загрузкой истории, часть сообщений не доходила.
Тестировал на 8 осле Вынь7 - все четко.

(Deleted comment)
(Deleted comment)
Закиньте темку на erlanger.ru или в рассылку
erlang-russian % googlegroups.com
там думаю че-нибудь хорошее предложат.

  • 1
?

Log in

No account? Create an account