The Client
A viable client on the JS side is in fact not much more complicated than simply writing:
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>
The Server
The Server needs to do 2 things:
- Handle creating + receiving websocket messages
- Serve the client
1. Handle creating + receiving websocket messages
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.
2. Serving the client
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
The rest of the server
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 = "/ws",
handler = initialWebsocketHandler,
httpMethod = HttpGet
)
app.addRoute(
route = "/client",
handler = client,
httpMethod = HttpGet
)
app.run()
main()