Как настроить небольшой пример клиент-сервера веб-сокета с помощью nim/prologue?

Я использую структуру пролога языка программирования nim для своего веб-сервера и хочу поиграть с веб-сокетами.

В документации пролога есть раздел о веб-сокетах, но в основном он рассказывает мне, как настроить обработчик для создания веб-сокета:

      import prologue
import prologue/websocket


proc hello*(ctx: Context) {.async.} =
  var ws = await newWebSocket(ctx)
  await ws.send("Welcome to simple echo server")
  while ws.readyState == Open:
    let packet = await ws.receiveStrPacket()
    await ws.send(packet)

  resp "<h1>Hello, Prologue!</h1>"

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

1 ответ

Клиент

Жизнеспособный клиент на стороне JS на самом деле не намного сложнее, чем просто написать:
          const url = "ws://localhost:8080/ws"
    const ws = new WebSocket(url);
    ws.addEventListener("open", () => ws.send("Connection open!"));
    ws.addEventListener("message", event => console.log("Received: " event));

Это будет записывать сообщение в консоль браузера каждый раз при получении сообщения и первоначально отправлять сообщение на сервер при установке соединения.

Однако давайте для экспериментов напишем немного более сложный клиент, который покажет вам обмен сообщениями между вами и сервером:

      <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Websocket Prototype</title>
</head>
<body>
  <h1> Hyper client !</h1>
  <input type="text">
  <button> Send Message </button>
  <h3> Conversation </h3>
  <ul></ul>
  <script>
    const list = document.querySelector("ul");
    function addMessage (sender, message){
      const element = document.createElement("li");
      element.innerHTML = `${sender}: ${message}`;
      list.appendChild(element);
    }
    
    const url = "ws://localhost:8080/ws"
    const ws = new WebSocket(url);
    ws.addEventListener("open", event => ws.send("Connection open!"));
    ws.addEventListener("message", event => addMessage("server", event.data));
    
    const input = document.querySelector("input");
    
    function sendMessage(){
      const clientMsg = input.value;
      ws.send(clientMsg);
      addMessage("user", clientMsg);
      input.value = null;
    }
    
    document.querySelector("button").addEventListener("click", sendMessage);
    document.querySelector('input').addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        sendMessage(event);
      }
    });
  </script>
</body>
</html>

Сервер

Серверу необходимо сделать 2 вещи:
  1. Обработка создания + получения сообщений веб-сокета
  2. Обслуживать клиента

1. Обработка создания и получения сообщений веб-сокета.

Вот как вы можете обрабатывать сообщения (Пролог использует древовидные формыwsбиблиотека под капотом):

      import std/options
import prologue
import prologue/websocket

var connections = newSeq[WebSocket]()

proc handleMessage(ctx: Context, message: string): Option[string] =
  echo "Received: ", message
  return some message

proc initialWebsocketHandler*(ctx: Context) {.async, gcsafe.} =
  var ws = await newWebSocket(ctx)
  {.cast(gcsafe).}:
    connections.add(ws)
  await ws.send("Welcome to simple echo server")
  
  while ws.readyState == Open:
    let message = await ws.receiveStrPacket()
    let response = ctx.handleMessage(message)
    if response.isSome():
      await ws.send(response.get())

  await ws.send("Connection is closed")
  resp "<h1>Hello, Prologue!</h1>"

Пролог продолжает ждать внутрицикл, пока веб-сокет открыт. Функция будет запускаться каждый раз при получении сообщения.

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

The Прагма в обработчике сообщает компилятору, что этот процесс должен быть безопасным для сборки мусора (нет доступа к памяти, которая потенциально может быть собрана мусором во время работы этого процесса). Это приведет к ошибке компиляции, поскольку доступ к глобальным изменяемым переменным, таким какникогда не является безопасным для gc, поскольку теоретически может исчезнуть. В этом сценарии этого не произойдет, поскольку глобальная переменная будет существовать в течение всего времени выполнения программы. Поэтому мы должны сообщить компилятору, что все в порядке, используя.

Примечание. Этот сервер не реализует механику сердцебиения (пакет websocket ее предоставляет) и не занимается закрытыми соединениями ! Итак, в настоящее время ваша последовательность подключений будет только заполняться.

2. Обслуживание клиента

Что касается обслуживания клиента, вы можете просто прочитать HTML-файл во время компиляции и использовать эту HTML-строку в качестве ответа:

      proc client*(ctx: Context) {.async, gcsafe.} =
  const html = staticRead("./client.html")
  resp html

Остальная часть сервера

Затем ваш реальный сервер может использовать эти два процесса-обработчика (также известных как контроллеры), как вы обычно настраиваете приложение-пролог. Оба можно сделать довольно быстро:
      #server.nim
import prologue
import ./controller # Where the 2 handler/controller procs are located

proc main() =
  var app: Prologue = newApp()
  app.addRoute(
    route = "/ws",
    handler = initialWebsocketHandler,
    httpMethod = HttpGet
  )
  
  app.addRoute(
    route = "/client",
    handler = client,
    httpMethod = HttpGet
  )
  app.run()

main()
Другие вопросы по тегам