🔥 0
0
Lesson 3 of 5 22 min +250 XP

Client & Server Implementation

Time to build! We'll create a real-time chat system with a Node.js server and browser client.

Server Setup

First, install the ws library:

npm install ws

Basic Server

// server.js
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

console.log('WebSocket server running on ws://localhost:8080');

wss.on('connection', (ws, request) => {
  console.log('New client connected');
  console.log('Client IP:', request.socket.remoteAddress);

  // Send welcome message
  ws.send(JSON.stringify({
    type: 'welcome',
    message: 'Connected to server!'
  }));

  // Handle incoming messages
  ws.on('message', (data) => {
    console.log('Received:', data.toString());

    // Echo back
    ws.send(JSON.stringify({
      type: 'echo',
      message: data.toString()
    }));
  });

  // Handle disconnect
  ws.on('close', (code, reason) => {
    console.log(`Client disconnected: ${code} - ${reason}`);
  });

  // Handle errors
  ws.on('error', (error) => {
    console.error('WebSocket error:', error);
  });
});

Client Setup

Browser Client

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>WebSocket Chat</title>
  <style>
    body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; }
    #messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 1rem; }
    .message { padding: 0.5rem; margin: 0.25rem 0; border-radius: 4px; }
    .received { background: #e3f2fd; }
    .sent { background: #e8f5e9; text-align: right; }
    .system { background: #fff3e0; font-style: italic; }
    #input-area { display: flex; gap: 0.5rem; margin-top: 1rem; }
    #message-input { flex: 1; padding: 0.5rem; }
    button { padding: 0.5rem 1rem; cursor: pointer; }
    #status { padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; }
    .connected { background: #c8e6c9; }
    .disconnected { background: #ffcdd2; }
  </style>
</head>
<body>
  <h1>WebSocket Chat</h1>
  <div id="status" class="disconnected">Disconnected</div>
  <div id="messages"></div>
  <div id="input-area">
    <input type="text" id="message-input" placeholder="Type a message..." />
    <button onclick="sendMessage()">Send</button>
  </div>

  <script>
    const messagesDiv = document.getElementById('messages');
    const statusDiv = document.getElementById('status');
    const input = document.getElementById('message-input');

    let ws;

    function connect() {
      ws = new WebSocket('ws://localhost:8080');

      ws.onopen = () => {
        statusDiv.textContent = 'Connected';
        statusDiv.className = 'connected';
        addMessage('Connected to server', 'system');
      };

      ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        addMessage(data.message, 'received');
      };

      ws.onclose = (event) => {
        statusDiv.textContent = 'Disconnected';
        statusDiv.className = 'disconnected';
        addMessage(`Disconnected (${event.code})`, 'system');

        // Reconnect after 3 seconds
        setTimeout(connect, 3000);
      };

      ws.onerror = (error) => {
        console.error('WebSocket error:', error);
      };
    }

    function sendMessage() {
      const message = input.value.trim();
      if (message && ws.readyState === WebSocket.OPEN) {
        ws.send(message);
        addMessage(message, 'sent');
        input.value = '';
      }
    }

    function addMessage(text, type) {
      const div = document.createElement('div');
      div.className = `message ${type}`;
      div.textContent = text;
      messagesDiv.appendChild(div);
      messagesDiv.scrollTop = messagesDiv.scrollHeight;
    }

    // Send on Enter key
    input.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') sendMessage();
    });

    // Connect on page load
    connect();
  </script>
</body>
</html>

Broadcasting to All Clients

Send a message to everyone connected:

// server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

// Broadcast to all connected clients
function broadcast(data) {
  const message = JSON.stringify(data);
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

wss.on('connection', (ws) => {
  // Notify everyone of new user
  broadcast({
    type: 'notification',
    message: 'A new user joined!'
  });

  ws.on('message', (data) => {
    // Broadcast the message to all clients
    broadcast({
      type: 'chat',
      message: data.toString()
    });
  });

  ws.on('close', () => {
    broadcast({
      type: 'notification',
      message: 'A user left'
    });
  });
});

Implementing Chat Rooms

// server.js with rooms
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

// Track rooms and their members
const rooms = new Map();

wss.on('connection', (ws) => {
  // Store user data on the WebSocket object
  ws.userId = Math.random().toString(36).substring(7);
  ws.rooms = new Set();

  ws.on('message', (rawData) => {
    const data = JSON.parse(rawData);

    switch (data.type) {
      case 'join':
        joinRoom(ws, data.room);
        break;

      case 'leave':
        leaveRoom(ws, data.room);
        break;

      case 'message':
        sendToRoom(data.room, {
          type: 'message',
          room: data.room,
          userId: ws.userId,
          text: data.text
        });
        break;
    }
  });

  ws.on('close', () => {
    // Leave all rooms on disconnect
    ws.rooms.forEach((room) => leaveRoom(ws, room));
  });
});

function joinRoom(ws, roomName) {
  // Create room if doesn't exist
  if (!rooms.has(roomName)) {
    rooms.set(roomName, new Set());
  }

  const room = rooms.get(roomName);
  room.add(ws);
  ws.rooms.add(roomName);

  // Notify room
  sendToRoom(roomName, {
    type: 'notification',
    message: `User ${ws.userId} joined ${roomName}`
  });

  // Send room info to user
  ws.send(JSON.stringify({
    type: 'joined',
    room: roomName,
    userCount: room.size
  }));
}

function leaveRoom(ws, roomName) {
  const room = rooms.get(roomName);
  if (!room) return;

  room.delete(ws);
  ws.rooms.delete(roomName);

  // Clean up empty rooms
  if (room.size === 0) {
    rooms.delete(roomName);
  } else {
    sendToRoom(roomName, {
      type: 'notification',
      message: `User ${ws.userId} left ${roomName}`
    });
  }
}

function sendToRoom(roomName, data) {
  const room = rooms.get(roomName);
  if (!room) return;

  const message = JSON.stringify(data);
  room.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

Client for Rooms

// Client-side room handling
const ws = new WebSocket('ws://localhost:8080');

// Join a room
function joinRoom(roomName) {
  ws.send(JSON.stringify({
    type: 'join',
    room: roomName
  }));
}

// Leave a room
function leaveRoom(roomName) {
  ws.send(JSON.stringify({
    type: 'leave',
    room: roomName
  }));
}

// Send message to room
function sendToRoom(roomName, text) {
  ws.send(JSON.stringify({
    type: 'message',
    room: roomName,
    text: text
  }));
}

// Handle incoming messages
ws.onmessage = (event) => {
  const data = JSON.parse(event.data);

  switch (data.type) {
    case 'joined':
      console.log(`Joined ${data.room} (${data.userCount} users)`);
      break;

    case 'message':
      console.log(`[${data.room}] ${data.userId}: ${data.text}`);
      break;

    case 'notification':
      console.log(`System: ${data.message}`);
      break;
  }
};

// Example usage
ws.onopen = () => {
  joinRoom('general');
  joinRoom('random');
  sendToRoom('general', 'Hello everyone!');
};

Message Protocol Design

Best Practice: Structured Messages

{
  "type": "chat_message",    // Action type
  "payload": {               // Action data
    "room": "general",
    "text": "Hello!",
    "timestamp": 1704067200
  },
  "meta": {                  // Optional metadata
    "userId": "abc123",
    "requestId": "req_456"
  }
}
// Server message handler with type routing
const handlers = {
  ping: (ws, payload) => {
    ws.send(JSON.stringify({ type: 'pong', payload: {} }));
  },

  join_room: (ws, payload) => {
    joinRoom(ws, payload.room);
  },

  chat_message: (ws, payload) => {
    sendToRoom(payload.room, {
      type: 'chat_message',
      payload: {
        userId: ws.userId,
        text: payload.text,
        timestamp: Date.now()
      }
    });
  }
};

ws.on('message', (rawData) => {
  try {
    const { type, payload } = JSON.parse(rawData);
    const handler = handlers[type];

    if (handler) {
      handler(ws, payload);
    } else {
      console.warn('Unknown message type:', type);
    }
  } catch (error) {
    console.error('Invalid message:', error);
    ws.send(JSON.stringify({
      type: 'error',
      payload: { message: 'Invalid message format' }
    }));
  }
});

Authentication

Authenticate during the handshake or first message:

// Option 1: Token in URL query string
// Client
const ws = new WebSocket('ws://localhost:8080?token=abc123');

// Server
const url = require('url');

wss.on('connection', (ws, request) => {
  const params = new URLSearchParams(url.parse(request.url).query);
  const token = params.get('token');

  // Verify token
  const user = verifyToken(token);
  if (!user) {
    ws.close(1008, 'Invalid token');
    return;
  }

  ws.user = user;
  console.log(`User ${user.name} connected`);
});
// Option 2: Auth message after connection
ws.on('connection', (ws) => {
  ws.isAuthenticated = false;

  // Set auth timeout
  const authTimeout = setTimeout(() => {
    if (!ws.isAuthenticated) {
      ws.close(1008, 'Authentication timeout');
    }
  }, 5000);

  ws.on('message', (rawData) => {
    const data = JSON.parse(rawData);

    if (!ws.isAuthenticated) {
      if (data.type === 'auth') {
        const user = verifyToken(data.token);
        if (user) {
          ws.isAuthenticated = true;
          ws.user = user;
          clearTimeout(authTimeout);
          ws.send(JSON.stringify({ type: 'auth_success' }));
        } else {
          ws.close(1008, 'Invalid credentials');
        }
      }
      return;
    }

    // Handle other messages only if authenticated
    handleMessage(ws, data);
  });
});

Complete Chat Server Example

// complete-server.js
const WebSocket = require('ws');
const http = require('http');

// Create HTTP server (for health checks, etc.)
const server = http.createServer((req, res) => {
  if (req.url === '/health') {
    res.writeHead(200);
    res.end('OK');
  }
});

// Attach WebSocket server
const wss = new WebSocket.Server({ server });

const rooms = new Map();
let connectionId = 0;

wss.on('connection', (ws, request) => {
  ws.id = ++connectionId;
  ws.rooms = new Set();
  ws.isAlive = true;

  console.log(`Client ${ws.id} connected`);

  // Respond to pings
  ws.on('pong', () => { ws.isAlive = true; });

  ws.on('message', (rawData) => {
    try {
      const { type, payload } = JSON.parse(rawData);

      switch (type) {
        case 'set_name':
          ws.username = payload.name;
          send(ws, 'name_set', { name: ws.username });
          break;

        case 'join':
          joinRoom(ws, payload.room);
          break;

        case 'leave':
          leaveRoom(ws, payload.room);
          break;

        case 'message':
          if (ws.rooms.has(payload.room)) {
            broadcast(payload.room, 'message', {
              room: payload.room,
              from: ws.username || `User ${ws.id}`,
              text: payload.text,
              timestamp: Date.now()
            });
          }
          break;

        case 'ping':
          send(ws, 'pong', {});
          break;

        default:
          send(ws, 'error', { message: `Unknown type: ${type}` });
      }
    } catch (error) {
      send(ws, 'error', { message: 'Invalid JSON' });
    }
  });

  ws.on('close', () => {
    console.log(`Client ${ws.id} disconnected`);
    ws.rooms.forEach((room) => leaveRoom(ws, room));
  });
});

function send(ws, type, payload) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type, payload }));
  }
}

function broadcast(room, type, payload, exclude = null) {
  const members = rooms.get(room);
  if (!members) return;

  const message = JSON.stringify({ type, payload });
  members.forEach((ws) => {
    if (ws !== exclude && ws.readyState === WebSocket.OPEN) {
      ws.send(message);
    }
  });
}

function joinRoom(ws, room) {
  if (!rooms.has(room)) rooms.set(room, new Set());
  rooms.get(room).add(ws);
  ws.rooms.add(room);

  send(ws, 'joined', { room, members: rooms.get(room).size });
  broadcast(room, 'user_joined', {
    room,
    user: ws.username || `User ${ws.id}`
  }, ws);
}

function leaveRoom(ws, room) {
  const members = rooms.get(room);
  if (!members) return;

  members.delete(ws);
  ws.rooms.delete(room);

  if (members.size === 0) {
    rooms.delete(room);
  } else {
    broadcast(room, 'user_left', {
      room,
      user: ws.username || `User ${ws.id}`
    });
  }
}

// Heartbeat to detect dead connections
const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', () => clearInterval(interval));

const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Key Takeaways

  • ws library - Standard Node.js WebSocket server
  • JSON messages - Stringify to send, parse on receive
  • Broadcast - Loop through wss.clients
  • Rooms - Use Map/Set to track membership
  • Heartbeats - Ping/pong to detect dead connections

Next up: Scaling - Learn how to scale WebSocket servers with Redis pub/sub.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What Node.js library is commonly used for WebSocket servers?

2

How do you broadcast a message to all connected clients?

3

How do you send JSON data over WebSocket?

Connection Lifecycle & Upgrade