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("Welcome to simple echo server")
while ws.readyState == Open:
let packet = await ws.receiveStrPacket()
await ws.send(packet)
resp "<h1>Hello, Prologue!</h1>"
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?
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));
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:
<!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>
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 "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>"
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.
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("./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()