WebSocket Complete Guide | Real-Time, Socket.io, Chat, Notifications, Games
이 글의 핵심
Hands-on guide to WebSocket and Socket.io: native API, rooms, Redis adapter, and the protocol layer—HTTP upgrade, frames, text vs binary frames, Ping/Pong heartbeats, and production patterns.
At a glance
This guide covers the browser WebSocket API, Socket.io (rooms, broadcast), a chat example, horizontal scaling with Redis, and RFC 6455 internals so you can debug proxies, handshakes, and heartbeats in production.
Field note: Migrating a polling-based chat to WebSocket once cut server load by about 90% and reduced delivery delay from seconds to near real time.
When you need real time
Scenario 1: Polling wastes resources — Calling an API every second loads the server; WebSocket keeps one connection open.
Scenario 2: Stale data — Polling adds latency up to the poll interval; WebSocket pushes immediately.
Scenario 3: Collaboration — Features like live cursors or co-editing map naturally to a persistent bidirectional channel.
1. What is WebSocket?
Core properties
WebSocket is a standardized protocol for full-duplex communication over a long-lived connection.
Benefits:
- Bidirectional — server and client can both send at any time
- Low latency — typically sub-10 ms on a good network
- Efficient — no repeated HTTP headers per message once upgraded
- Built into browsers —
WebSocketAPI
HTTP vs WebSocket:
- HTTP — request/response, often short-lived connections
- WebSocket — persistent session after the upgrade
2. Protocol internals: upgrade, frames, text/binary, heartbeats (RFC 6455)
Libraries (ws, Socket.io, browser WebSocket) hide framing and masking. For production incidents—proxies closing idle connections, 400 on handshake, or invalid UTF-8 closes—you need the RFC 6455 skeleton.
2.1 From HTTP to WebSocket: the upgrade handshake
WebSocket is not a separate TCP port protocol by default. It reuses the TCP connection and switches protocols with one HTTP/1.1 request:
- The client sends
GETwithUpgrade: websocket,Connection: Upgrade,Sec-WebSocket-Version: 13, andSec-WebSocket-Key(16 random bytes, Base64). - The server concatenates the key with the magic string
258EAFA5-E914-47DA-95CA-C5AB0DC85B11, computes SHA-1, Base64-encodes the result, and returns it asSec-WebSocket-Acceptwith status101 Switching Protocols. - After
101, the same socket carries WebSocket frames, not HTTP message bodies.
Reverse proxies (Nginx, Cloudflare) must forward Upgrade and Connection end-to-end; otherwise you never get 101 or the connection drops immediately. For restrictive networks, wss:// on port 443 is usually the most reliable option.
2.2 Frame structure (summary)
A message may be split across one or more frames. The frame header includes:
- FIN — whether this frame ends the message (fragmentation possible when
FINis 0). - Opcode — e.g.
1text,2binary,8close,9ping,10pong. - Payload length — 7-bit length, or extended 16/64-bit fields for larger payloads.
- Masking — frames from client to server must be XOR-masked per RFC (mitigates cache poisoning). Server to client is not masked.
Application code usually sees reassembled messages, but huge payloads may be fragmented—always set a maximum message size in your stack (read_message_max, server limits, etc.).
2.3 Text vs binary frames
| Text (opcode 1) | Binary (opcode 2) | |
|---|---|---|
| Payload | UTF-8 text | Any bytes (Protobuf, chunks, etc.) |
| Browser | Often arrives as a string in onmessage | Blob / ArrayBuffer |
| Pitfalls | Invalid UTF-8 may trigger a protocol error and close | Encoding is your responsibility |
JSON chat and notifications usually use text. Protobuf, MsgPack, or file chunks belong in binary. You can mix both on one connection if your app distinguishes frame types.
2.4 Heartbeats: Ping/Pong vs app-level keepalive
- Wire-level — Ping (9) and Pong (10) control frames generate traffic so NATs and load balancers do not treat the TCP session as idle. Pong typically echoes Ping payload.
- Application-level — JSON
{"type":"ping"}or similar is common. Socket.io has its own heartbeat protocol on top of WebSocket.
If the proxy read_timeout is shorter than your heartbeat interval, the link dies regardless of app health. Align Ping period (e.g. 20–30s) with proxy and ALB idle timeouts.
2.5 Production patterns
- WSS on 443 — best pass-through corporate firewalls and proxies.
- Sticky sessions or shared state — pin a user to an instance when needed; otherwise use Redis Pub/Sub (or streams) to broadcast across nodes.
- Reconnect — exponential backoff, offline queues, last event / cursor for catch-up after reconnect.
- Backpressure — do not unboundedly
sendto slow clients; cap queues and define drop policies. - Observability — metrics for open connections, messages/sec, handshake failures, abnormal closes (e.g. 1006 without a close frame).
3. Native WebSocket API
Server (Node.js)
// server.ts
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (data) => {
console.log('Received:', data.toString());
// Echo
ws.send(`Echo: ${data}`);
});
ws.on('close', () => {
console.log('Client disconnected');
});
ws.send('Welcome!');
});
console.log('WebSocket server running on ws://localhost:8080');
Client (browser)
// client.ts
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('Connected');
ws.send('Hello Server!');
};
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
ws.onerror = (error) => {
console.error('Error:', error);
};
ws.onclose = () => {
console.log('Disconnected');
};
4. Socket.io
Install
# Server
npm install socket.io
# Client
npm install socket.io-client
Server
// server.ts
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST'],
},
});
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('message', (data) => {
console.log('Message:', data);
// All clients
io.emit('message', data);
// Everyone except sender
socket.broadcast.emit('message', data);
// Specific socket — set targetSocketId in your app
// socket.to(targetSocketId).emit('message', data);
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
httpServer.listen(3000, () => {
console.log('Server running on :3000');
});
Client
// client.ts
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
socket.on('message', (data) => {
console.log('Received:', data);
});
socket.emit('message', { text: 'Hello!' });
5. Example: chat app
Server
// server.ts
import { Server } from 'socket.io';
const io = new Server(3000, {
cors: { origin: '*' },
});
interface Message {
user: string;
text: string;
timestamp: string;
}
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('join', (room: string) => {
socket.join(room);
socket.to(room).emit('user-joined', socket.id);
console.log(`${socket.id} joined ${room}`);
});
socket.on('message', (data: { room: string; message: Message }) => {
io.to(data.room).emit('message', data.message);
});
socket.on('typing', (room: string) => {
socket.to(room).emit('typing', socket.id);
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
Client (React)
// Chat.tsx
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
let socket: Socket;
export function Chat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const room = 'general';
useEffect(() => {
socket = io('http://localhost:3000');
socket.emit('join', room);
socket.on('message', (message: Message) => {
setMessages((prev) => [...prev, message]);
});
socket.on('typing', (userId: string) => {
console.log(`${userId} is typing...`);
});
return () => {
socket.disconnect();
};
}, []);
const sendMessage = () => {
if (input.trim()) {
const message: Message = {
user: 'Me',
text: input,
timestamp: new Date().toISOString(),
};
socket.emit('message', { room, message });
setInput('');
}
};
const handleTyping = () => {
socket.emit('typing', room);
};
return (
<div>
<div>
{messages.map((msg, i) => (
<div key={i}>
<strong>{msg.user}:</strong> {msg.text}
</div>
))}
</div>
<input
value={input}
onChange={(e) => {
setInput(e.target.value);
handleTyping();
}}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
/>
<button onClick={sendMessage}>Send</button>
</div>
);
}
6. Redis adapter (scale-out)
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const io = new Server(3000);
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
console.log('Redis adapter connected');
});
// Multiple Node processes can now share Socket.io events
Summary and checklist
Takeaways
- RFC 6455 —
101upgrade, opcodes, masking, Ping/Pong explain most proxy and handshake failures - WebSocket — bidirectional real-time channel
- Socket.io — rooms, namespaces, reconnection helpers
- Redis adapter — horizontal scale for Socket.io
Checklist
- WebSocket or Socket.io server
- Rooms / channels if needed
- Authentication (token during handshake or first message)
- Error handling and logging
- Redis (or equivalent) for multi-instance broadcast
- Deploy with correct proxy timeouts and WSS
FAQ
WebSocket vs Server-Sent Events?
WebSocket is bidirectional. SSE is server → client only. For chat and collaboration, WebSocket (or Socket.io) is the usual choice. See also WebSocket vs SSE vs long polling.
What happens when the connection drops?
Socket.io retries with its own rules. For native WebSocket, implement reconnect with backoff and state resync (last message id, snapshot, etc.).
Is this production-ready?
Yes—many products use WebSocket or Socket.io behind TLS and Redis. Tune heartbeats, limits, and observability as you would for any long-lived TCP service.
Keywords
WebSocket, Real-time, Socket.io, RFC 6455, Node.js, Backend