WebSocket Complete Guide | Real-Time· Socket.io
이 글의 핵심
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](/en/blog/websocket-vs-sse-vs-polling-guide/.
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
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ WebSocket Complete Guide | Beast Handshake· Frames](/en/blog/cpp-series-30-1-websocket/
- [C++ WebSocket Deep Dive | Handshake· Frames](/en/blog/cpp-series-30-2-websocket/
- [HTTP Protocols Complete Guide | HTTP/1.1· HTTP/2](/en/blog/http-protocols-complete-guide/
이 글에서 다루는 키워드 (관련 검색어)
WebSocket, Real-Time, Socket.io, Chat, Node.js, Backend 등으로 검색하시면 이 글이 도움이 됩니다.