Backend Development 23 min read

Building Real-Time IM Solutions with WebSocket: A Practical Guide

The guide walks through building a real‑time instant‑messaging system—using a multiplayer monster‑battle demo—to compare polling, long‑polling, SSE and WebSocket, explains the WebSocket handshake and frame format, shows a custom Node.js server implementation, and advises adopting mature IM SDKs for production.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
Building Real-Time IM Solutions with WebSocket: A Practical Guide

This article explores Instant Messaging (IM) solutions for real-time data updates in web applications, using a multi-player monster-battle scenario as a practical example.

Data Update Approaches Comparison:

The article first compares different real-time data update methods: polling (setInterval every 3 seconds), long polling (server holds connection until data is available), SSE (Server-Sent Events for server-to-client push), and WebSocket (full-duplex persistent connection).

Polling Implementation:

// Request ranking interface
const refreshRank = (familyId) => {
    getMonsterDamageRank({ familyId }).then((res) => {
        setRank(res);
    }).catch((err) => {
        Toast.warn(err.message || 'Server busy');
    });
};

// Refresh interface every 3 seconds
setInterval(() => {
    refreshRank(currentFamily.familyId);
}, 3000);

Long Polling Server Example (Node.js):

const http = require('http');
const messages = [];

function waitForNewMessages(response) {
  const intervalId = setInterval(() => {
    if (messages.length > 0) {
      response.writeHead(200, { 'Content-Type': 'application/json' });
      response.end(JSON.stringify(messages));
      clearInterval(intervalId);
    }
  }, 1000);

  setTimeout(() => {
    clearInterval(intervalId);
    response.writeHead(200, { 'Content-Type': 'application/json' });
    response.end(JSON.stringify([]));
  }, 30000);
}

WebSocket vs HTTP: WebSocket provides persistent connections with bidirectional communication, lower latency, cross-domain support, and binary data transmission. Unlike HTTP's request-response pattern, WebSocket requires only one handshake to establish a persistent connection.

WebSocket Handshake Process:

Client sends HTTP request with Upgrade headers:

GET ws://localhost:3000/ HTTP/1.1
Host: localhost:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: nFPKUyeo5Ul58tbe7Dg5lA==

Server responds with 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 89D1tEKizEJHFrVDhswIIpAf4ww=

The Sec-WebSocket-Accept is generated by concatenating Sec-WebSocket-Key with '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', computing SHA1 hash, and encoding as base64.

WebSocket Frame Structure: Frames include FIN (final fragment indicator), RSV1-3 (reserved), opcode (frame type: 0x0=continuation, 0x1=text, 0x2=binary, 0x8=close, 0x9=ping, 0xA=pong), and payload data.

Custom WebSocket Server Implementation:

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

function parseFrame(buffer) {
    const opcode = buffer[0] & 0x0f;
    const payloadLength = buffer[1] & 0x7f;
    const mask = buffer.slice(2, 6);
    const payload = buffer.slice(6);
    let decodedPayload = '';
    if (opcode === 1) {
        for (let i = 0; i < payloadLength; i++) {
            decodedPayload += String.fromCharCode(payload[i] ^ mask[i % 4]);
        }
    } else if (opcode === 8) {
        return { type: 'close' };
    }
    return { type: 'text', data: decodedPayload };
}

function createTextFrame(message) {
    const buffer = Buffer.alloc(2 + message.length);
    buffer[0] = 0x81;
    buffer[1] = message.length;
    for (let i = 0; i < message.length; i++) {
        buffer[i + 2] = message.charCodeAt(i);
    }
    return buffer;
}

server.on('upgrade', (req, socket, head) => {
    const key = req.headers['sec-websocket-key'];
    const sha1 = crypto.createHash('sha1');
    sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
    const accept = sha1.digest('base64');
    
    const headers = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        'Sec-WebSocket-Accept: ' + accept,
        '\r\n'
    ];
    socket.write(headers.join('\r\n'));
    
    socket.on('data', (buffer) => {
        const frame = parseFrame(buffer);
        if (frame.type === 'text') {
            console.log('Received message:', frame.data);
            sendTextMessage(socket, 'Hello, client!');
        } else if (frame.type === 'close') {
            socket.destroy();
        }
    });
});

Production IM Solution: The article recommends using mature IM SDKs like NetEase IM (云信) for production environments. It demonstrates integrating their Web IM SDK for chatroom scenarios, handling message filtering to discard delayed messages based on timestamps.

onmsgs: (data) => {
    const { msgTime } = data;
    const preTime = monster?.msgTime || 0;
    if (msgTime > preTime) {
      monster.remainingHp = remainHp;
      monster.damage = damage;
      monster.msgTime = msgTime;
    }
}
frontend developmentNode.jsWebSocketreal-time communicationnetwork protocolInstant Messaginglong pollingSSE
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.