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.