如何使用Nim/Prologue建立一个小型WebSocket客户端-服务器示例?

huangapple go评论75阅读模式
英文:

How to set up a small websocket client-server example with nim/prologue?

问题

我正在使用nim编程语言prologue框架来开发我的Web服务器,并希望尝试使用Websockets。

prologue文档中有关于Websockets的部分,但它主要告诉我如何设置用于建立Websocket连接的处理程序,如下所示:

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>"

但这并没有完全告诉我它的工作原理,也没有说明客户端需要如何连接。我需要在这里做什么?

英文:

I am using the prologue framework of the nim programming language for my webserver and want to play around with websockets.

There is a section about websockets in the prologue docs but that mostly tells me how to set up a handler for establishing a websocket:

import prologue
import prologue/websocket


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

  resp &quot;&lt;h1&gt;Hello, Prologue!&lt;/h1&gt;&quot;

That doesn't quite tell me how it actually works, nor what the client needs to look like to connect to this. What do I need to do here?

答案1

得分: 2

以下是您要翻译的内容:

客户端


JS端的可行客户端实际上并不比简单地编写如下更复杂:

    const url = "ws://localhost:8080/ws"
    const ws = new WebSocket(url);
    ws.addEventListener("open", () => ws.send("连接已打开!"));
    ws.addEventListener("message", event => console.log("接收到消息:", event));

每次接收到消息时,这将在浏览器控制台中写入一条消息,并在建立连接时最初向服务器发送一条消息。

但是,让我们为实验编写一个稍微复杂一点的客户端,它将向您展示您和服务器之间的消息交换:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebSocket原型</title>
</head>
<body>
  <h1> 超级客户端!</h1>
  <input type="text">
  <button>发送消息</button>
  <h3>会话</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("连接已打开!"));
    ws.addEventListener("message", event => addMessage("服务器", event.data));
    
    const input = document.querySelector("input");
    
    function sendMessage(){
      const clientMsg = input.value;
      ws.send(clientMsg);
      addMessage("用户", 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>

服务器


服务器需要执行两件事:

  1. 处理创建和接收WebSocket消息
  2. 为客户端提供服务

1. 处理创建和接收WebSocket消息


这是如何处理消息的方式(Prologue在底层使用treeforms的ws库):

import std/options
import prologue
import prologue/websocket

var connections = newSeq[WebSocket]()

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

proc initialWebsocketHandler*(ctx: Context) {.async, gcsafe.} =
  var ws = await newWebSocket(ctx)
  {.cast(gcsafe).}:
    connections.add(ws)
  await ws.send("欢迎使用简单的回显服务器")
  
  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("连接已关闭")
  resp "<h1>你好,Prologue!</h1>"

Prologue在while循环中保持等待,只要WebSocket处于打开状态。每次接收到消息时,都会触发handleMessage函数。

如果要将特定消息路由到以不同方式处理不同类型消息的特定过程中,可以从handleMessage开始实施,并根据事件本身决定是否返回响应消息。

处理程序上的{.gcsafe.}指示编译器,该过程应该是垃圾回收安全的(在此过程运行时不会访问可能在此过程运行时被垃圾回收的内存)。这将导致编译错误,因为访问全局可变变量,如connections,在理论上可能会消失,但在这种情况下不会发生,因为全局变量将在程序的整个运行时存在。因此,我们必须使用{.cast(gcsafe).}:告诉编译器是可以的。

注意:此服务器不实现心跳机制(WebSocket包提供了一个),也不处理关闭的连接!因此,您的连接序列当前只会填满。

2. 提供客户端


至于提供客户端,您只需在编译时读取HTML文件并将该HTML字符串作为响应提供:

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

服务器的其余部分


然后,您的实际服务器可以像通常设置Prologue应用程序一样使用这两个处理程序过程(也称为控制器)。两者可以很快完成:

# server.nim
import prologue
import ./controller # 两个处理程序/控制器过程的所在位置

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()
英文:

<h2> The Client </h2>
A viable client on the JS side is in fact not much more complicated than simply writing:

    const url = &quot;ws://localhost:8080/ws&quot;
    const ws = new WebSocket(url);
    ws.addEventListener(&quot;open&quot;, () =&gt; ws.send(&quot;Connection open!&quot;));
    ws.addEventListener(&quot;message&quot;, event =&gt; console.log(&quot;Received: &quot; event));

This will write a message to the browsers console every time a message is received and initially send a message to the server when connection is establish.

However, let's write a slightly more elaborate client for experimentation that will show you the exchange of messages between you and the server:

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

<h2> The Server </h2>
The Server needs to do 2 things:

  1. Handle creating + receiving websocket messages
  2. Serve the client

<h3> 1. Handle creating + receiving websocket messages </h3>

This is how you can handle the messages (Prologue uses treeforms ws library under the hood):

import std/options
import prologue
import prologue/websocket

var connections = newSeq[WebSocket]()

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

proc initialWebsocketHandler*(ctx: Context) {.async, gcsafe.} =
  var ws = await newWebSocket(ctx)
  {.cast(gcsafe).}:
    connections.add(ws)
  await ws.send(&quot;Welcome to simple echo server&quot;)
  
  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(&quot;Connection is closed&quot;)
  resp &quot;&lt;h1&gt;Hello, Prologue!&lt;/h1&gt;&quot;

Prologue keeps waiting inside the while loop as long as the websocket is open. The function handleMessage will get triggered every time a message is received.

If you want to route a given message to specific procs that deal with different kinds of messages in different ways, you can implement it starting from handleMessage and based on the event itself decide to return or not return a response message.

The {.gcsafe.} pragma on the handler informs the compiler that this proc is supposed to be garbage-collection-safe (no access to memory that may potentially be garbage collected while this proc is running). This will cause the compilation to error out because accessing global mutable variables like connections is never gc-safe as it theoretically could disappear. In this scenario that is not going to happen, as the global variable will live for the entire runtime of the program. So we must inform the compiler it's fine by using {.cast(gcsafe).}:.

Note: This server does not implement a heartbeat-mechanic (the websocket package provides one), nor does it deal with closed connections! So currently your connections seq will only fill up.

<h3> 2. Serving the client </h3>

As for serving the client, you can just read in the HTML file at compile time and serve that HTML string as response:

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

<h3> The rest of the server </h3>
Your actual server can then use these 2 handler-procs (aka controllers) as you would normally set up a prologue application
Both can be done pretty quickly:

#server.nim
import prologue
import ./controller # Where the 2 handler/controller procs are located

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

main()

huangapple
  • 本文由 发表于 2023年6月30日 03:00:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/76583938.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定