Introduction to the ws Library
The ws library is a fast, standards-compliant WebSocket implementation for Node.js. It's one of the most popular WebSocket libraries with minimal overhead and excellent performance characteristics.
Why ws?
- Lightweight: No external dependencies, minimal abstraction
- Fast: Highly optimized for performance
- Standards-compliant: Fully implements RFC 6455
- Production-ready: Used by thousands of production applications
- Active maintenance: Regularly updated and well-supported
Installing ws
Install the ws library using npm or yarn:
# Using npm
npm install ws
# Using yarn
yarn add ws
# Check installed version
npm list ws
Creating a Basic WebSocket Server
Let's create a simple WebSocket server that echoes messages back to clients:
// server.js
const WebSocket = require('ws');
// Create WebSocket server on port 8080
const wss = new WebSocket.Server({ port: 8080 });
console.log('WebSocket server started on ws://localhost:8080');
// Listen for connections
wss.on('connection', (ws) => {
console.log('New client connected');
// Listen for messages from this client
ws.on('message', (message) => {
console.log('Received:', message.toString());
// Echo the message back
ws.send(`Echo: ${message.toString()}`);
});
// Send welcome message
ws.send('Welcome to the WebSocket server!');
});
Run the server:
node server.js
WebSocket Server Options
The WebSocket.Server constructor accepts various configuration options:
const wss = new WebSocket.Server({
port: 8080, // Server port
host: '0.0.0.0', // Host to bind to (0.0.0.0 for all interfaces)
path: '/socket', // URL path (e.g., ws://host:port/socket)
perMessageDeflate: true, // Enable compression
clientTracking: true, // Track connected clients (default: true)
maxPayload: 100 * 1024 * 1024, // Max message size (100MB)
verifyClient: (info, cb) => { // Custom verification function
// Verify client before accepting connection
const token = new URL(info.req.url, 'http://localhost').searchParams.get('token');
if (token === 'valid-token') {
cb(true); // Accept connection
} else {
cb(false, 401, 'Unauthorized'); // Reject connection
}
}
});
Custom Verification: Use the verifyClient option to implement authentication, rate limiting, or IP filtering before accepting WebSocket connections. This prevents unauthorized clients from establishing connections.
Attaching to an Existing HTTP Server
Instead of creating a standalone server, you can attach the WebSocket server to an existing HTTP/HTTPS server:
const http = require('http');
const WebSocket = require('ws');
const express = require('express');
// Create Express app
const app = express();
// Regular HTTP routes
app.get('/', (req, res) => {
res.send('HTTP server is running');
});
app.get('/api/data', (req, res) => {
res.json({ message: 'REST API endpoint' });
});
// Create HTTP server
const server = http.createServer(app);
// Attach WebSocket server to HTTP server
const wss = new WebSocket.Server({ server, path: '/socket' });
wss.on('connection', (ws) => {
console.log('WebSocket client connected');
ws.on('message', (message) => {
console.log('Received:', message.toString());
});
});
// Start the server
server.listen(8080, () => {
console.log('Server listening on http://localhost:8080');
console.log('WebSocket available at ws://localhost:8080/socket');
});
Shared Port Benefits: Running WebSocket and HTTP on the same port simplifies deployment, works better with firewalls and load balancers, and allows you to share SSL/TLS certificates.
Handling Client Connections
The connection event provides access to the WebSocket instance for each connected client:
wss.on('connection', (ws, request) => {
// ws: WebSocket instance for this connection
// request: HTTP upgrade request object
// Access request information
console.log('Client IP:', request.socket.remoteAddress);
console.log('User-Agent:', request.headers['user-agent']);
console.log('URL:', request.url);
// Parse query parameters
const url = new URL(request.url, `http://${request.headers.host}`);
const userId = url.searchParams.get('userId');
const token = url.searchParams.get('token');
console.log('User ID:', userId);
console.log('Token:', token);
// Store custom properties on the WebSocket instance
ws.userId = userId;
ws.isAuthenticated = token === 'valid-token';
// Send personalized welcome message
ws.send(JSON.stringify({
type: 'welcome',
message: `Hello user ${userId}!`,
timestamp: Date.now()
}));
});
Receiving and Sending Messages
Receiving Messages
ws.on('message', (data, isBinary) => {
// data: Buffer or string
// isBinary: boolean indicating if message is binary
if (isBinary) {
console.log('Received binary data:', data.length, 'bytes');
// Process binary data
const view = new Uint8Array(data);
console.log('First byte:', view[0]);
} else {
// Text message
const message = data.toString();
console.log('Received text:', message);
// Parse JSON if applicable
try {
const json = JSON.parse(message);
console.log('Parsed JSON:', json);
// Handle different message types
switch (json.type) {
case 'chat':
handleChatMessage(ws, json);
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
break;
default:
console.log('Unknown message type:', json.type);
}
} catch (e) {
console.log('Not JSON message');
}
}
});
Sending Messages
// Send text message
ws.send('Hello client!');
// Send JSON
ws.send(JSON.stringify({ type: 'notification', message: 'New update' }));
// Send binary data
const buffer = Buffer.from([1, 2, 3, 4, 5]);
ws.send(buffer);
// Send with callback
ws.send('Important message', (error) => {
if (error) {
console.error('Failed to send:', error);
} else {
console.log('Message sent successfully');
}
});
// Send with options
ws.send('Compressed message', {
compress: true, // Compress this message
binary: false, // Send as text frame
fin: true // Final fragment
}, (error) => {
if (error) console.error('Send error:', error);
});
Broadcasting Messages
Broadcasting sends a message to all connected clients:
// Simple broadcast to all clients
function broadcast(message) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Broadcast except sender
function broadcastExcept(message, sender) {
wss.clients.forEach((client) => {
if (client !== sender && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Broadcast to specific users
function broadcastToUsers(message, userIds) {
wss.clients.forEach((client) => {
if (userIds.includes(client.userId) && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Usage in message handler
ws.on('message', (data) => {
const message = data.toString();
// Broadcast to all clients except sender
broadcastExcept(JSON.stringify({
type: 'chat',
from: ws.userId,
message: message,
timestamp: Date.now()
}), ws);
});
Performance Consideration: Broadcasting to thousands of clients synchronously can block the event loop. For large-scale applications, consider using workers, clustering, or message queues (Redis Pub/Sub) to distribute the load.
Handling Errors
Always handle errors to prevent server crashes:
// Connection-level error handling
ws.on('error', (error) => {
console.error('WebSocket error:', error);
// Log error details
console.error('Error code:', error.code);
console.error('Error message:', error.message);
// Don't try to send on errored connection
// The connection will be closed automatically
});
// Server-level error handling
wss.on('error', (error) => {
console.error('WebSocket Server error:', error);
// Handle server errors (port already in use, etc.)
if (error.code === 'EADDRINUSE') {
console.error('Port is already in use');
process.exit(1);
}
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
// Gracefully shutdown
wss.close(() => {
process.exit(1);
});
});
Connection Lifecycle Events
ws.on('open', () => {
console.log('Connection opened');
// Note: This event is not emitted on the server side
// Only the 'connection' event on wss indicates a new connection
});
ws.on('close', (code, reason) => {
console.log('Connection closed');
console.log('Close code:', code);
console.log('Close reason:', reason.toString());
// Clean up resources
if (ws.userId) {
console.log(`User ${ws.userId} disconnected`);
// Remove from active users list, etc.
}
});
ws.on('ping', (data) => {
console.log('Received ping from client');
// Pong is sent automatically by ws library
});
ws.on('pong', (data) => {
console.log('Received pong from client');
ws.isAlive = true; // Mark connection as alive
});
Ping/Pong Heartbeat
Implement heartbeat mechanism to detect broken connections:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// Heartbeat interval (30 seconds)
const HEARTBEAT_INTERVAL = 30000;
wss.on('connection', (ws) => {
// Mark connection as alive
ws.isAlive = true;
// Reset alive flag when pong is received
ws.on('pong', () => {
ws.isAlive = true;
});
ws.on('message', (message) => {
console.log('Received:', message.toString());
});
});
// Ping all clients periodically
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
// Connection is dead, terminate it
console.log('Terminating dead connection');
return ws.terminate();
}
// Mark as potentially dead
ws.isAlive = false;
// Send ping
ws.ping();
});
}, HEARTBEAT_INTERVAL);
// Clean up interval when server closes
wss.on('close', () => {
clearInterval(interval);
});
Heartbeat Best Practice: Always implement ping/pong heartbeat in production. This detects network failures, proxy timeouts, and client crashes that wouldn't otherwise trigger the close event.
Complete Chat Server Example
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// Store active users
const users = new Map();
console.log('Chat server started on ws://localhost:8080');
wss.on('connection', (ws, request) => {
const clientIp = request.socket.remoteAddress;
console.log('New connection from:', clientIp);
// Initialize connection
ws.isAlive = true;
ws.userId = null;
// Handle pong
ws.on('pong', () => {
ws.isAlive = true;
});
// Handle messages
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
switch (message.type) {
case 'join':
// User joins chat
ws.userId = message.userId;
ws.username = message.username;
users.set(ws.userId, ws);
// Broadcast user joined
broadcast({
type: 'user-joined',
userId: ws.userId,
username: ws.username,
timestamp: Date.now()
});
// Send active users list
ws.send(JSON.stringify({
type: 'users-list',
users: Array.from(users.values()).map(u => ({
userId: u.userId,
username: u.username
}))
}));
break;
case 'chat-message':
// Broadcast chat message
broadcast({
type: 'chat-message',
from: ws.username,
message: message.content,
timestamp: Date.now()
});
break;
case 'typing':
// Broadcast typing indicator
broadcastExcept({
type: 'user-typing',
username: ws.username
}, ws);
break;
default:
console.log('Unknown message type:', message.type);
}
} catch (error) {
console.error('Error processing message:', error);
ws.send(JSON.stringify({
type: 'error',
message: 'Invalid message format'
}));
}
});
// Handle disconnection
ws.on('close', () => {
console.log('Client disconnected:', ws.username);
if (ws.userId) {
users.delete(ws.userId);
// Broadcast user left
broadcast({
type: 'user-left',
userId: ws.userId,
username: ws.username,
timestamp: Date.now()
});
}
});
// Handle errors
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
});
// Broadcast helper
function broadcast(data) {
const message = JSON.stringify(data);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Broadcast except sender
function broadcastExcept(data, sender) {
const message = JSON.stringify(data);
wss.clients.forEach((client) => {
if (client !== sender && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Heartbeat
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', () => {
clearInterval(interval);
});
Exercise: Extend the chat server to include:
- Private messaging between specific users
- Chat rooms/channels that users can join
- Message history stored in memory or database
- User authentication with JWT tokens
- Rate limiting to prevent spam
Summary
The ws library provides a robust foundation for building WebSocket servers in Node.js. Key concepts include creating servers with configuration options, handling connections and messages, broadcasting to multiple clients, implementing error handling, and using ping/pong heartbeat for connection health monitoring. With these building blocks, you can create production-ready real-time applications. In the next lesson, we'll explore Socket.io, a higher-level library that adds features like automatic reconnection and room support.