Lesson 05-WebSocket Communication and Security

Communication Mechanisms

Detailed Explanation of the WebSocket Handshake Process (HTTP Upgrade Header)

WebSocket Handshake Process:

  1. The client initiates an HTTP Upgrade request.
  2. The server responds with a 101 Switching Protocols status.
  3. A full-duplex communication channel is established.

Detailed Handshake Flow:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com

Server Response:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Handshake Source Code Implementation (Node.js):

const http = require('http');
const crypto = require('crypto');

const server = http.createServer((req, res) => {
  if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') {
    // Validate WebSocket handshake request
    if (req.headers['sec-websocket-key']) {
      // Calculate Sec-WebSocket-Accept
      const acceptKey = calculateAcceptKey(req.headers['sec-websocket-key']);

      // Send 101 response
      res.writeHead(101, {
        'Upgrade': 'websocket',
        'Connection': 'Upgrade',
        'Sec-WebSocket-Accept': acceptKey
      });
      res.end();

      // Handshake successful, upgrade to WebSocket connection
      handleWebSocketConnection(req.socket);
    } else {
      res.writeHead(400);
      res.end('Missing Sec-WebSocket-Key');
    }
  } else {
    res.writeHead(400);
    res.end('Not a WebSocket request');
  }
});

function calculateAcceptKey(key) {
  const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  const sha1 = crypto.createHash('sha1').update(key + magic).digest('base64');
  return sha1;
}

function handleWebSocketConnection(socket) {
  // WebSocket connection handling logic
  console.log('WebSocket connection established');
}

server.listen(8080, () => {
  console.log('WebSocket server running at ws://localhost:8080');
});

WebSocket Frame Parsing and Encapsulation

WebSocket Frame Format:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

Frame Types (opcode):

  • 0x0: Continuation frame
  • 0x1: Text frame
  • 0x2: Binary frame
  • 0x8: Connection close
  • 0x9: Ping
  • 0xA: Pong

Frame Parsing Source Code Implementation:

class WebSocketFrame {
  constructor(buffer) {
    this.buffer = buffer;
    this.fin = (buffer[0] & 0x80) !== 0;
    this.rsv1 = (buffer[0] & 0x40) !== 0;
    this.rsv2 = (buffer[0] & 0x20) !== 0;
    this.rsv3 = (buffer[0] & 0x10) !== 0;
    this.opcode = buffer[0] & 0x0F;
    this.mask = (buffer[1] & 0x80) !== 0;
    this.payloadLength = buffer[1] & 0x7F;
    this.maskingKey = null;
    this.payloadData = null;

    this.parse();
  }

  parse() {
    let offset = 2;

    // Handle extended length
    if (this.payloadLength === 126) {
      this.payloadLength = (buffer[offset] << 8) | buffer[offset + 1];
      offset += 2;
    } else if (this.payloadLength === 127) {
      this.payloadLength = (buffer[offset] << 56) | (buffer[offset + 1] << 48) |
                          (buffer[offset + 2] << 40) | (buffer[offset + 3] << 32) |
                          (buffer[offset + 4] << 24) | (buffer[offset + 5] << 16) |
                          (buffer[offset + 6] << 8) | buffer[offset + 7];
      offset += 8;
    }

    // Handle missing data
    if (this.mask) {
      this.maskingKey = buffer.slice(offset, offset + 4);
      offset += 4;
    }

    // Extract payload data
    this.payloadData = buffer.slice(offset, offset + this.payloadLength);

    // Unmask payload if masked
    if (this.mask && this.maskedData) {
      this.payloadData = this.unmaskPayload(this.payload, this.maskedData);
    }
  }

  unmaskPayload(payload, maskingKey) {
    const unmasked = new Uint8Array(payload.length);
    for (let i = 0; i < payload.length; i++) {
      unmasked[i] = payload[i] ^ maskingKey[i % 4];
    }
    return unmasked;
  }
}

Message Boundary Handling (Message Fragmentation and Reassembly)

Message Fragmentation Mechanism:

  1. Fragmentation Types:
    • Intermediate fragment (Opcode=0x0)
  2. – Start fragment (Opcode=0x1 or 0x2)
  3. – End fragment (FIN=1)

Fragmentation Reassembly Process:

  • Receive start fragment, initialize buffer
  • Receive intermediate fragments, append to buffer
  • Receive end fragment, complete reassembly

Fragmentation Reassembly Source Code Implementation:

class WebSocketMessageAssembler {
  constructor() {
    this.fragments = new Map(); // clientId -> { opcode, fragments }
    this.maxFragmentSize = 16 * 1024 * 1024; // 16MB maximum fragment size
  }

  processFrame(frame, clientId) {
    if (!this.fragments.has(clientId)) {
      this.fragments.set(clientId, {
        opcode: null,
        fragments: []
      });
    }

    const assembler = this.fragments.get(clientId);

    if (frame.fin) {
      // End fragment, complete reassembly
      if (assembler.opcode === null) {
        // Single frame message
        return frame.payloadData;
      } else {
        // Fragmented message
        assembler.fragments.push(frame.payloadData);
        const completeMessage = this.concatFragments(assembler.fragments);
        this.fragments.delete(clientId);
        return completeMessage;
      }
    } else {
      // Intermediate or start fragment
      if (assembler.opcode === null) {
        // Start fragment
        assembler.opcode = frame.opcode;
        assembler.fragments.push(frame.payloadData);
      } else {
        // Intermediate fragment
        assembler.fragments.push(frame.payloadData);

        // Check if fragment size exceeds limit
        const totalSize = assembler.fragments.reduce((sum, frag) => sum + frag.length, 0);
        if (totalSize > this.maxFragmentSize) {
          this.fragments.delete(clientId);
          throw new Error('Message fragment size exceeds limit');
        }
      }

      return null; // Message incomplete
    }
  }

  concatFragments(fragments) {
    let totalLength = fragments.reduce((sum, frag) => sum + frag.length, 0);
    const result = new Uint8Array(totalLength);
    let offset = 0;

    for (const frag of fragments) {
      result.set(frag, offset);
      offset += frag.length;
    }

    return result;
  }
}

Connection State Management (Timeout, Reconnection)

Connection State Management Strategies:

  1. Heartbeat mechanism: Periodic Ping/Pong to check connection health
  2. Timeout settings:
    • Handshake timeout
    • Message response timeout
    • Idle connection timeout
  3. Reconnection strategies:
    • Exponential backoff algorithm
    • Maximum retry limit

Heartbeat Detection Implementation:

class WebSocketHeartbeat {
  constructor(ws, options = {}) {
    this.ws = ws;
    this.interval = options.interval || 30000; // 30 seconds
    this.timeout = options.timeout || 5000; // 5 seconds
    this.pingTimeout = null;
    this.pongTimeout = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;

    this.start();
  }

  start() {
    this.schedulePing();
  }

  stop() {
    clearTimeout(this.pingTimeout);
    clearTimeout(this.pongTimeout);
  }

  schedulePing() {
    this.pingTimeout = setTimeout(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.ping();
        this.schedulePongTimeout();
      }
    }, this.interval);
  }

  schedulePongTimeout() {
    this.pongTimeout = setTimeout(() => {
      console.error('Pong response timeout, closing connection');
      this.ws.close(1001, 'Pong timeout');
    }, this.timeout);
  }

  handlePong() {
    clearTimeout(this.pongTimeout);
    this.schedulePing();
  }

  handleOpen() {
    this.reconnectAttempts = 0;
    this.schedulePing();
  }

  handleClose() {
    this.stop();

    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
      this.reconnectAttempts++;

      console.log(`Will attempt to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
      setTimeout(() => {
        // Implement reconnection logic
        this.reconnect();
      }, delay);
    } else {
      console.log('Maximum reconnect attempts reached');
    }
  }

  reconnect() {
    // Implement reconnection logic
    // Typically requires creating a new WebSocket instance
  }
}

// Usage example
const ws = new WebSocket('ws://example.com/socket');
const heartbeat = new WebSocketHeartbeat(ws, {
  interval: 30000,
  timeout: 5000
});

ws.onopen = () => heartbeat.handleOpen();
ws.onclose = () => heartbeat.handleClose();
ws.onpong = () => heartbeat.handlePong();

Multi-Client Communication Modes (Broadcast, Unicast, Multicast)

Communication Modes Implementation:

  1. Unicast: Point-to-point message delivery
  2. Broadcast: Send message to all clients
  3. Multicast: Send message to specific group of clients

Multi-Client Management Implementation:

class WebSocketManager {
  constructor() {
    this.clients = new Map(); // clientId -> WebSocket
    this.clientGroups = new Map(); // groupId -> Set<clientId>
  }

  addClient(clientId, ws) {
    this.clients.set(clientId, ws);
  }

  removeClient(clientId) {
    this.clients.delete(clientId);

    // Remove client from all groups
    for (const [groupId, clientIds] of this.clientGroups) {
      clientIds.delete(clientId);

      // Delete group if empty
      if (clientIds.size === 0) {
        this.clientGroups.delete(groupId);
      }
    }
  }

  joinGroup(clientId, groupId) {
    if (!this.clientGroups.has(groupId)) {
      this.clientGroups.set(groupId, new Set());
    }
    this.clientGroups.get(groupId).add(clientId);
  }

  leaveGroup(clientId, groupId) {
    if (this.clientGroups.has(groupId)) {
      this.clientGroups.get(groupId).delete(clientId);

      // Delete group if empty
      if (this.clientGroups.get(groupId).size === 0) {
        this.clientGroups.delete(groupId);
      }
    }
  }

  sendToClient(clientId, message) {
    const ws = this.clients.get(clientId);
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(message);
    }
  }

  broadcast(message) {
    for (const [clientId, ws] of this.clients) {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(message);
      }
    }
  }

  multicast(groupId, message) {
    if (this.clientGroups.has(groupId)) {
      const clientIds = this.clientGroups.get(groupId);
      for (const clientId of clientIds) {
        this.sendToClient(clientId, message);
      }
    }
  }
}

// Usage example
const manager = new WebSocketManager();

// Add clients
manager.addClient('client1', ws1);
manager.addClient('client2', ws2);

// Clients join group
manager.joinGroup('client1', 'group1');
manager.joinGroup('client2', 'group1');

// Send messages
manager.sendToClient('client1', 'Private message'); // Unicast
manager.broadcast('Broadcast message'); // Broadcast
manager.multicast('group1', 'Group message'); // Multicast

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Share your love