Building a Raw WebSocket Server

WebSockets & Real-Time Communication — Building a Raw WebSocket Server
WebSockets & Real-Time Communication
Chapter 3 · Building a Raw WebSocket Server

🛠️ Building a Raw WebSocket Server

Chapter 2 covered the protocol itself — the handshake, the frames, the close sequence. None of that needs to be implemented by hand: Node's ws library handles the protocol details and hands you a clean event-based API. This chapter builds a real server with it.

Installing ws

npm install ws

A Minimal Server

import { WebSocketServer } from "ws"; const wss = new WebSocketServer({ port: 8080 }); wss.on("connection", (socket) => { // "socket" here represents this one specific client's connection console.log("A client connected."); });

One Server, Many Sockets

wss is the server itself; socket (often named ws in examples) is created fresh for every individual client that connects — the "connection" handler runs once per client, not once total.

No Manual Framing

The library already did the handshake and frame parsing from Chapter 2 before this handler ever runs — by the time you have a socket, the WebSocket protocol is fully established.

The message Event

wss.on("connection", (socket) => { socket.on("message", (data) => { const text = data.toString(); // data arrives as a Buffer, not a string const parsed = JSON.parse(text); console.log("Received:", parsed); }); });

Every message a client sends fires this event on that client's own socket object — data is a Buffer by default, so it needs .toString() before treating it as text or parsing it as JSON.

Sending Data Back

socket.on("message", (data) => { const parsed = JSON.parse(data.toString()); socket.send(JSON.stringify({ echo: parsed })); // reply to just this one client });

The close and error Events

wss.on("connection", (socket) => { socket.on("close", (code, reason) => { console.log(`Client disconnected: ${code} ${reason}`); // clean up anything tracked for this socket — e.g. remove it from a room }); socket.on("error", (err) => { console.error("Socket error:", err); }); });

👥 Tracking Multiple Clients

A single socket only knows about itself — broadcasting to everyone means keeping your own collection of connected sockets:

const clients = new Set(); wss.on("connection", (socket) => { clients.add(socket); socket.on("message", (data) => { for (const client of clients) { if (client.readyState === client.OPEN) { client.send(data.toString()); // broadcast to everyone, including the sender } } }); socket.on("close", () => clients.delete(socket)); // stop tracking a disconnected client });

This is deliberately minimal — real rooms, presence tracking, and targeted broadcasting (not "everyone") are the focus of Chapter 6.

readyState

Every socket has a readyState: CONNECTING, OPEN, CLOSING, or CLOSED. Checking for OPEN before sending avoids errors from trying to write to a socket that's already on its way out.

Sharing an HTTP Port

A WebSocketServer can attach to an existing http.Server (e.g. an Express app's server) instead of listening on its own port — common in real deployments where regular HTTP and WebSocket traffic share one port.

Raw ws Today vs Socket.IO (Next Chapter)

What ws Gives You

A thin, direct wrapper around the protocol — connection/message/close events, and nothing else. Rooms, reconnection, acknowledgements: all hand-rolled.

What a Library Like Socket.IO Adds

Automatic reconnection, a room/namespace system, message acknowledgements, and fallback transports — at the cost of a heavier abstraction and its own wire protocol on top of WebSockets.

💻 Coding Challenges

Challenge 1: Build an Echo Server

Using ws, write a complete server that listens on port 8080 and sends every message it receives straight back to the same client that sent it (not broadcast to everyone).

Goal: Practice the basic connection/message/send flow before adding multi-client broadcasting.

→ Solution

Challenge 2: Track Connected Clients With Logging

Extend the echo server to maintain a Set of connected clients, logging the current client count every time a client connects or disconnects.

Goal: Practice managing per-connection state that outlives a single message.

→ Solution

Challenge 3: Defend Against a Bad readyState

Write a broadcast(clients, message) helper function that safely sends message to every socket in a Set, skipping any socket that isn't currently OPEN.

Goal: Practice the defensive readyState check this chapter's tip box warns about.

→ Solution

⚠️ Gotcha: Sending to a Socket That Isn't Open Yet (or Anymore)

socket.send() doesn't automatically wait for the connection to be ready, and it doesn't throw a helpful error if the socket has already started closing — depending on timing, a careless call can silently fail or throw an uncaught exception. This bites most often in the broadcast pattern above: by the time a loop reaches a given client, that client may have disconnected moments earlier. Always check readyState === WebSocket.OPEN immediately before calling send() on any socket you didn't just create yourself in the current handler.

🎯 What's Next

With a working raw server in hand, the next chapter steps back to compare this approach against a higher-level library: Socket.IO vs Raw WebSockets — reviewing the Socket.IO coverage from the Node.js and Express courses, and figuring out when raw ws is actually the better choice.