Deno: WebSocket server in Oak
Introduction
A WebSocket is undoubtedly the most common & standard way to serve bidirectional real-time communication over HTTP. Here is a text from Wikipedia:
WebSocket is distinct from HTTP. Both protocols are located at layer 7 in the OSI model and depend on TCP at layer 4. Although they are different, RFC 6455 states that WebSocket “is designed to work over HTTP ports 443 and 80 as well as to support HTTP proxies and intermediaries,” thus making it compatible with HTTP. To achieve compatibility, the WebSocket handshake uses the HTTP Upgrade header to change from the HTTP protocol to the WebSocket protocol.
WebSocket works as an upgrade over HTTP. This way, they are compatible with the underlying core infra.
Oak is a popular middleware framework for Deno’s native HTTP server, Deno Deploy and Node.js 16.5 and later. It also includes a middleware router. At the time of writing, Oak is the best middleware framework available in Deno’s third party packages.
In one of the earlier articles, we’ve seen how to use native WebSockets in Deno.
In this article, we’ll learn how to use WebSockets with Oak middleware framework.
WebSocket server with Oak
A WebSocket server written in Oak is different from the native WebSocket server only for the setup phase. This is because Oak uses similar looking, but different APIs for the WebSocket setup. Once the WebSocket connection is established, the other APIs like onopen, onmessage, onclose, etc. are the same whether it’s Oak or native.
Setup phase
The setup phase consists of two simple steps:
- Check if connection can be upgraded (isUpgradable)
- If upgradable, upgrade it (upgrade)
These two simple APIs are part of the Oak’s Context object. These APIs take care of upgrading the underlying HTTP connection to a two-way WebSocket connection.
Here is a code snippet for the setup phase:
import { Application, Router } from "https://deno.land/x/oak/mod.ts";const app = new Application({ logErrors: false });
const router = new Router();router.get("/wss", (ctx) => {
if (!ctx.isUpgradable) {
ctx.throw(501);
}
const ws = ctx.upgrade();
// Define ws callbacks
});app.use(router.routes());
app.use(router.allowedMethods());app.listen({ port: 8000 });
The above code returns 501 Not Implemented if connection is not upgradable. If the connection is upgradable, the connection gets upgraded. The upgrade API returns a web standard WebSocket object.
Let’s do a round of test using curl:
> deno run --allow-net=:8000 app.ts> > curl http://localhost:8000/wss -v
> GET /wss HTTP/1.1
< HTTP/1.1 501 Not Implemented
< content-type: text/plain; charset=utf-8
< content-length: 15Not Implemented
A test using curl fails with 501 error because curl is trying to make a regular HTTP connection to the /wss endpoint that expects only upgradable connections.
Callbacks
Once the connection is established, we can define a number of callbacks on the returned WebSocket object. There are simple callbacks like:
- onopen: Called whenever the WebSocket connection gets established
- onmessage: Called whenever there is a new message from the other party
- onclose: Called whenever the WebSocket connection gets closed
- onerror: Called whenever the WebSocket connection encounters an error
Here is a code snippet for the callbacks:
ws.onopen = () => console.log('Connection established');
ws.onclose = () => console.log('Connection closed');
ws.onmessage = (m) => console.log('Received message', m.data);
The onmessage callback comes with a MessageEvent object that contains a data attribute which carries the message that came from the other party.
Now, let’s write a simple WebSocket server and a client.
WebSocket example
Here is the code of a WebSocket server that:
- Waits for clients
- Sends a ‘Hello from server’ message whenever a client is connected
- Echoes the first message that comes from client
- Disconnects the connection after echoing
// ws_server.tsimport { Application, Router } from "https://deno.land/x/oak/mod.ts";const app = new Application({ logErrors: false });
const router = new Router();router.get("/wss", (ctx) => {
if (!ctx.isUpgradable) {
ctx.throw(501);
}
const ws = ctx.upgrade();
ws.onopen = () => {
console.log("Connected to client");
ws.send("Hello from server!");
};
ws.onmessage = (m) => {
console.log("Got message from client: ", m.data);
ws.send(m.data as string);
ws.close();
};
ws.onclose = () => console.log("Disconncted from client");
});app.use(router.routes());
app.use(router.allowedMethods());app.listen({ port: 8000 });
Here is the code of a WebSocket client that:
- Initiates connection to the server
- Waits for server’s hello message
- On receiving server’s hello, sends a message with a UUID
- Closes the socket whenever server closes it
// ws_client.tsconst ws = new WebSocket("ws://localhost:8000/wss");
ws.onopen = () => console.log("Connected to server");
ws.onmessage = (m) => {
console.log("Got message from server: ", m.data);
ws.send(`Some message ${crypto.randomUUID()}`);
};
ws.onclose = () => console.log("Disconnected from server");
Here is a sample run of the client and server applications:
> deno run --allow-net=:8000 ws_server.ts
Connected to client
Got message from client: Some message 8757a032-7a6b-4ede-879b-742684d059b8
Disconncted from client> > deno run --allow-net=:8000 ws_client.ts
Connected to server
Got message from server: Hello from server!
Got message from server: Some message 8757a032-7a6b-4ede-879b-742684d059b8
Disconnected from server
The simple WebSocket echo app works fine!