Skip to content

Commit

Permalink
Реализация веб-сокетов (#45)
Browse files Browse the repository at this point in the history
* Рефакторинг и поддержка веб сокета

---------

Co-authored-by: Nikita Ivanchenko <[email protected]>
  • Loading branch information
Nivanchenko and Nikita Ivanchenko authored Mar 10, 2023
1 parent 8398fda commit 850ef7b
Show file tree
Hide file tree
Showing 29 changed files with 1,404 additions and 47 deletions.
128 changes: 126 additions & 2 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ opm install winow
- Отдавать статичные файлы(картинки, архивы и т.д.)
- Работать с шаблонами ответов (Синтаксис шаблона чем-то похож на jinja2, но сильно упрощен).
- Базовая авторизация и управление доступом к страницам по ролям.
- Поддержка протокола WebSocket

## Ограничения ?

Expand Down Expand Up @@ -973,14 +974,137 @@ app/КонтролСУправлениемДоступом.os
Ответ.ТелоТекст = "Админы и пользователи";
КонецПроцедуры
```

# Работа с протоколом WebSocket

В winow в экспериментальном виде реализована поддержка веб-сокетов. Спека не полная, поддерживаются пока только текстовые сообщения. Разберем работу протокола на примере онлайн чата. Я не буду углубляться в часть фронта. Вот [пример](example/hwapp/view/chat.html) реализации на фронте. Основная суть такая - когда мы заходим на контрол ```/chat``` осуществляется проверка, залогинился пользователь или нет. Если нет, переадресуем на страницу ввода логина.

После ввода логина мы попадаем на страницу с чатом, где осуществляется подключение вебсокета, и начинается обмен сообщениями.

А теперь разберем пример того, что происходит на стороне сервера. Обработкой входящих сообщений занимается такой же контроллер с точками маршрута, к которым мы привыкли. Каждая точка маршрута является "топиком" в рамках которого общается одно соединение веб сокета. В примере ниже у нас контроллер по адресу ```chat``` и точкой маршрута ```message```. У нас одно соединение, которое обменивается сообщениями в топике ```/chat/message```. Точка маршрута по обработке сообщений может принимать несколько параметров:
- Идентификатор - идентификатор сессии, в которой можно хранить разные данные.
- Топик - имя топика, в рамках которого происходит общение
- Сообщение - расшифрованное сообщение, которое пришло от клиента.

Для того, что бы отправлять сообщения, нужно получить желудь ```БрокерСообщенийВебСокетов```. Который умеет следующие действия с сообщениями:

- ОтправитьСообщение(Топик, Сообщение, Идентификатор) - Отправляет определенному клиенту сообщение в указанный топик.
- ОтправитьСообщениеВсем(Топик, Сообщение) - Отправляет сообщение всем клиентам, подключенным к указанному топику.
- ОтправитьСообщениеСписку(Топик, Сообщение, СписокИдентификаторов) - Отправляет сообщение массиву клиентов, подписанных на указанный топик.
- ОтправитьСообщениеВсемКроме(Топик, Сообщение, СписокИсключенийИдентификаторов) - Отправляет сообщение всем, клиентам подписанным на указанный топик, кроме массива, переданного как параметр.

Так же контроллер может иметь методы, помеченные аннотациями ```&ПриПодключенииВебСокета("/имя/топика")``` и ```ПриОтключенииВебСокета("/имя/топика")```. Которые будут вызваны, после соответствующих событий, и принимать ```Идентификатор``` клиента, с которым произошло событие.

Вот полный пример с комментариями.

```
ВебСокетЧат.os
```

```bsl
&Пластилин Перем БрокерСообщенийВебСокетов; // Инжектим желудь, который управляет отправкой сообщений
Перем КешИменПользователей;
&Контроллер("/chat") // помечаем контроллер и инициализируем кеш.
Процедура ПриСозданииОбъекта()
КешИменПользователей = Новый Соответствие();
КонецПроцедуры
&ТочкаМаршрута("message") // Обработчик входящего сообщения
Процедура ВходящееСообщение(Идентификатор, Топик, Сообщение) Экспорт
// клиент понимает два вида сообщений, которые он отправил сам, и которые отправлены другими клиентами. Они по разному отображаются на фронте.
// Тут мы получаем из кеша имя пользователя по идентификатору соединения и отправляем клиентам.
ИмяПользователя = КешИменПользователей.Получить(Идентификатор);
Сообщить(СтрШаблон("Получено сообщение %1 от %2", Сообщение, ИмяПользователя));
Массив = Новый Массив();
Массив.Добавить(Идентификатор);
ТекстПолучения = ФорматированноеСообщение(ИмяПользователя, Сообщение, Истина);
ТекстОтправки = ФорматированноеСообщение(ИмяПользователя, Сообщение, Ложь);
БрокерСообщенийВебСокетов.ОтправитьСообщениеВсемКроме(Топик, ТекстПолучения, Массив);
БрокерСообщенийВебСокетов.ОтправитьСообщениеСписку(Топик, ТекстОтправки, Массив);
КонецПроцедуры
&Отображение("./hwapp/view/chat.html")
&ТочкаМаршрута("") // точка маршрута, которая отдаем страницу клиента.
Процедура Главная(Сессия, Ответ) Экспорт
// если пользователь не закеширован, перенаправляем на страницу логина.
Имя = КешИменПользователей.Получить(Сессия.Идентификатор());
Если Имя = Неопределено Тогда
Ответ.Перенаправить("/chat/login");
КонецЕсли;
КонецПроцедуры
&ТочкаМаршрута("login") // страница ввода логина.
&Отображение("./hwapp/view/chatlogin.html")
Процедура Логин() Экспорт
КонецПроцедуры
&ТочкаМаршрута("loginprocess") // обработка введенного логина, с кешированием имени.
Процедура ОбработкаЛогина(ИмяПользователя, Сессия, Ответ) Экспорт
Если НЕ ЗначениеЗаполнено(ИмяПользователя) Тогда
ГенераторСлучайныхЧисел = Новый ГенераторСлучайныхЧисел(ТекущаяУниверсальнаяДатаВМиллисекундах());
ИмяПользователя = "Noname" + Строка(ГенераторСлучайныхЧисел.СлучайноеЧисло(1, 999));
КонецЕсли;
Сообщить(СтрШаблон("Регистрация пользователя %1", ИмяПользователя));
КешИменПользователей.Вставить(Сессия.Идентификатор(), ИмяПользователя);
Ответ.Перенаправить("/chat");
КонецПроцедуры
&ПриПодключенииВебСокета("/chat/message") // подписка на подключение пользователя
Процедура ПриПодключенииПользователя(Идентификатор) Экспорт
// сообщим всем, что зашел новый пользователь.
ИмяПользователя = КешИменПользователей.Получить(Идентификатор);
Сообщить(СтрШаблон("Подключился %1", ИмяПользователя));
ТекстСообщенияГостю = ФорматированноеСообщение("Оракул", "Привет " + ИмяПользователя + " !", Истина);
БрокерСообщенийВебСокетов.ОтправитьСообщениеВсем("/chat/message", ТекстСообщенияГостю);
КонецПроцедуры
&ПриОтключенииВебСокета("/chat/message") // подписка на отключение пользователя.
Процедура ПриОтключенииПользователя(Идентификатор) Экспорт
// сообщим всем, что пользователь вышел.
ИмяПользователя = КешИменПользователей.Получить(Идентификатор);
Сообщить(СтрШаблон("Отключился %1", ИмяПользователя));
ТекстСообщения = ФорматированноеСообщение("Оракул", ИмяПользователя + " покинул чат", Истина);
БрокерСообщенийВебСокетов.ОтправитьСообщениеВсем("/chat/message", ТекстСообщения);
КонецПроцедуры
Функция ФорматированноеСообщение(Автор, Текст, Получен)
ОбъектДляПарсинга = Новый Структура("Author, Time, Text, rcv", Автор, Формат(ТекущаяДата(), "ДФ=ЧЧ:мм"), Текст, Получен);
Запись = новый ЗаписьJSON;
Запись.УстановитьСтроку();
ЗаписатьJSON(Запись, ОбъектДляПарсинга);
Возврат Запись.Закрыть();
КонецФункции
```

Вот результат наших трудов.

![ws](docs/ws-chat.gif)

# Контейнеризация

Приложение на winow можно, конечно, запустить в контейнере.

В вот небольшой пример, как это сделать.

Нужно в один каталог положить
* Само приложение [app](docker/app/), которое содержит скрипт запуска, контролы, файлы и картинки, ``autumn-properties.json``` со всеми настройками и т.д.
* [Dockerfile](docker/Dockerfile) для того, что бы собрать образ, и прокинуть в него все файлы.
* Само приложение [app](docker/app/), которое содержит скрипт запуска, контролы, файлы и картинки, ```autumn-properties.json``` со всеми настройками и т.д.
* [Dockerfile](example/hwapp/view/chat.html) для того, что бы собрать образ, и прокинуть в него все файлы.
* Скрипт запуска сервера [docker-entrypoint.sh](docker/docker-entrypoint.sh).
* И все это дело удобно собирать одним скриптом [start.sh](docker/start.sh).
Binary file added docs/ws-chat.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 850ef7b

Please sign in to comment.