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

Введение в веб-сокеты

Если вы не знаете, что такое веб-сокеты, визуализируйте их как трубу, идущую от сервера к клиенту. Сообщения могут отправляться через этот канал в любом направлении. Что мы подразумеваем под сообщениями? Сообщения представляют собой просто строки.

Вам может быть интересно: чем это отличается от HTTP-запросов? Ах, как я рад, что вы спросили! Запросы HTTP подобны созданию канала, который распадается после отправки сообщения. Следовательно, вам нужно создавать новый канал каждый раз, когда сервер или клиент хотят общаться друг с другом. Звучит неэффективно, не так ли? Вот почему веб-сокеты так привлекательны, поскольку канал сохраняется до тех пор, пока клиент или сервер не решит закрыть его. Еще один недостаток http заключается в том, что сервер не может отправлять данные клиенту, пока клиент не запросит данные.

Создание простого приложения для чата

В этой записи блога мы собираемся создать очень простое приложение для чата. Вы можете увидеть живую демонстрацию здесь. Чтобы получить максимальную отдачу от демонстрации, откройте ссылку в двух окнах браузера и поговорите с собой в чате. Если вы читаете это на мобильном устройстве, картинка ниже даст вам представление о приложении.

Crystal Backend

Мы собираемся использовать Crystal для бэкэнда и Elm для фронтэнда. Если вы никогда не слышали о Crystal, это молодой язык, который называют быстрым, как C, ловким, как Ruby. Мы используем Crystal, а точнее фреймворк Kemal, по трем причинам:

  1. Kemal обладает превосходной производительностью для веб-сокетов.
  2. Язык Crystal имеет красивый Ruby-подобный синтаксис.
  3. Развернуть приложение Kemal на Heroku совсем несложно.

Кристалл: Быстро, как C, ловко, как Рубин

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

  1. Установите последнюю версию Crystal. На момент написания последняя версия - 0.19.4.
  2. Создайте новый проект с именем kemal-chat-elm, используя команду crystal init app kemal-chat-elm в вашем терминале.
  3. Добавьте и установите осколок для Кемаля в свой проект. Осколки похожи на Ruby Gems или модули NPM.

Для этого приложения ниже показан код, который мы помещаем в src/kemal-chat-elm/kemal-chat-elm.cr. Это прямо из связанного сообщения в блоге. Не забудьте добавить Kemal.run в конец файла.

require “kemal”
SOCKETS = [] of HTTP::WebSocket
ws “/chat” do |socket|
  # Add the client to SOCKETS list
  SOCKETS << socket
  # Broadcast each message to all clients
  socket.on_message do |message|
    SOCKETS.each { |socket| socket.send message}
  end
  # Remove clients from the list when it’s closed
  socket.on_close do
    SOCKETS.delete socket
  end
end
Kemal.run

Запустите crystal src/kemal-chat-elm.cr в своем терминале, чтобы запустить сервер Kemal. Если все пойдет хорошо, у вас должен быть сервер, работающий на 0.0.0.0:3000 , что эквивалентноlocalhost:3000.

Вяз Фронтенд

Для интерфейса мы будем использовать Elm, потому что:

  1. Мы собираемся получить нулевые исключения времени выполнения.
  2. Быстро пылает.
  3. Я считаю, что мы можем писать поддерживаемый код, даже не пытаясь.

В этом проекте используется Elm 0.17.1. Я предполагаю, что вы проработали Руководство по Elm до раздела о веб-сокетах, поскольку этот пост не предназначен для введения в Elm.

Создайте новый проект Elm. Я позвонил своему elm-chat. Недавно я обнаружил удобный инструмент под названием create-elm-app, который обрабатывает большую часть шаблонов, используемых при создании нового проекта Elm, и я рекомендую вам попробовать его.

Вы можете создать этот проект в указанном выше каталоге kemal-chat-elm или в его собственном каталоге. Лично я сначала начал создавать его снаружи, а затем переместил в kemal-chat-elm, чтобы мне не приходилось управлять двумя репозиториями git. Просто не забудьте добавить каталог elm-stuff в файл .gitignore.

Затем откройте свой elm-package.json и убедитесь, что у вас есть следующие зависимости.

"dependencies": {
      "elm-lang/core": "4.0.1 <= v < 5.0.0",
      "elm-lang/html": "1.0.0 <= v < 2.0.0",
      "elm-lang/websocket": "1.0.0 <= v < 2.0.0"
    },

Ниже приведен стартовый код Elm для нашего чат-приложения. Это не последний код, который вы видите в живой демонстрации, но он дает нам 90%. Самый простой способ запустить эту программу и увидеть ее в своем браузере - ввести elm-reactor src/Main.elm в терминале.

Получение сообщения веб-сокета

Elm упрощает работу с веб-сокетами. В начале этого поста мы говорили об отправке и получении сообщений через канал. Давайте посмотрим на строку 85, где мы обрабатываем получение сообщения.

WebSocket.listen "ws://0.0.0.0:3000/chat" NewChatMessage

У нас есть этот фрагмент кода в разделе подписки. Elm может подписаться на множество вещей из внешнего мира, таких как время, размер окна и веб-сокеты. Тот сервер, который вы видите там, ws://0.0.0.0:3000/chat, является сервером веб-сокетов, работающим в Кемале. Каждый раз, когда мы получаем новое сообщение с указанного выше адреса, мы обрабатываем его с помощью сообщения обновления NewChatMessage.

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

Отправка сообщения через веб-сокет

Когда пользователь вводит свое сообщение чата и нажимает кнопку «Отправить», мы хотели бы отправить сообщение на наш сервер Kemal, чтобы он мог отправить это сообщение всем клиентам, участвующим в этом чате. Кроме того, мы хотим очистить ввод текста, где написано «Привет», чтобы пользователь мог ввести свое следующее сообщение.

Давайте подробнее рассмотрим код, который делает это в строках с 44 по 48.

PostChatMessage ->
  let
    message = model.userMessage
  in
    { model | userMessage = "" } ! [ WebSocket.send "ws://0.0.0.0:3000/chat" message ]

В блоке let мы сохраняем сообщение чата в переменной message. Затем мы очищаем userMessage в модели, заменяя его пустой строкой - вот почему нам нужно сохранить это значение в message в блоке let, чтобы мы не потеряли его. Наконец, мы отправляем сообщение чата пользователя на сервер Kemal, используя WebSocket.send "ws://0.0.0.0:3000/chat" message.

Это WebSocket.send "ws://0.0.0.0:3000/chat" message - команда, которую мы отправляем Вязу. Сообщение нашего пользователя в чате покидает комфорт нашей программы Elm и уходит в пугающий внешний мир. Но не волнуйтесь, если случится что-нибудь плохое, например потеря связи с сервером websocket, Elm позаботится об этом за нас.

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

Сервер WebSocket, где ты?

В приведенном выше приложении Elm мы жестко запрограммировали адрес сервера веб-сокета как ws://0.0.0.0:3000/chat, и это нормально, если мы собираемся запускать это только на нашем локальном компьютере. Но что происходит, когда мы развертываем наше приложение? Наш сервер websocket не будет находиться по адресу 0.0.0.0:3000. Мы могли бы получить адрес рабочего сервера websocket и вставить его в наше приложение Elm, но это довольно непросто, не правда ли?

Кроме того, если наше приложение Elm обслуживается через https, Chrome не позволит нам использовать обычное соединение с веб-сокетом и выдаст ошибку, настаивая на использовании безопасного подключения к веб-сокету (wss). Как наше приложение с этим справится?

Решение

До сих пор мы запускали нашу программу с использованием Elm Reactor, но мы можем скомпилировать код из Main.elm в файл javascript и импортировать его в файл html. Преимущество этого заключается в том, что мы можем передавать аргументы приложению Elm при инициализации. В нашем случае мы можем передать конечную точку веб-сокета. Еще одно преимущество состоит в том, что мы можем использовать внешние таблицы стилей - подробнее об этом позже.

Мы создаем index.html и elm.js и помещаем их в общую папку в нашем приложении Kemal. Код в index.html - это то, что мы пишем, и он показан ниже. Код для elm.js создается с помощью следующей команды из терминала: elm make src/Main.elm --output elm.js

Мы добавляем немного javascript в тело нашего index.html, который определяет адрес сервера websocket, как показано в строках 10 и 11. Эта информация передается в Elm, когда он инициализируется в строке 14. Чтобы получить доступ к этой информации в нашем Elm. app, мы используем Html.programWithFlag вместо Html.program.

main : Program Flags
main =
  Html.programWithFlags
  { init = init
  , view = view
  , update = update
  , subscriptions = subscriptions
  }
type alias Flags =
  { websocketHost : String }

Обратите внимание, что нам нужно создать псевдоним типа с именем Flags, чтобы Элм знал, какой будет структура Flag. Чтобы получить доступ к информации о флаге, мы обновляем модель и init функцию следующим образом.

type alias Model =
  { chatMessages : List String
  , userMessage : String
  , websocketHost: String
  }
init : Flags -> (Model, Cmd Msg)
init flags =
  ( Model [] "" flags.websocketHost
  , Cmd.none
  )

Последние штрихи

Окончательная версия кода, которую вы видели в живой демонстрации, делает некоторые дополнительные вещи. А именно:

  • Он запрашивает у пользователя имя пользователя и добавляет его в сообщение чата.
  • Он добавляет стиль с использованием skeleton.css - если вы выполняете какую-либо работу со стилем с Elm, которая требует внешних таблиц стилей, я рекомендую вам использовать в разработке что-то вроде elm-live, а не elm -actor, потому что elm-Reaction не обрабатывает внешние таблицы стилей. Еще одним преимуществом является то, что у elm-live есть функция оперативной перезарядки, которой нет у elm-реактора.

Если вы хотите развернуть это приложение Kemal на Heroku, следуйте инструкциям на heroku-buildpack-crystal.

Приятного общения!