Node.js WebSockets
Introduction to WebSockets
WebSockets provide a persistent connection between client and server, allowing for real-time, bidirectional communication.
This is different from traditional HTTP, which follows a request-response model.
Key Benefits of WebSockets
- Real-time updates: Instantly push data to clients
- Efficient: No need for repeated HTTP requests
- Bidirectional: Both client and server can send messages
- Low latency: Messages are sent immediately
WebSockets vs HTTP
Understanding the difference between WebSockets and HTTP is crucial for building real-time applications effectively.
Feature | WebSockets | HTTP |
---|---|---|
Connection | Persistent, single connection | New connection per request |
Communication | Bidirectional, full-duplex | Unidirectional, request-response |
Overhead | Minimal after handshake | Headers with every request |
Use Case | Real-time applications | Traditional web pages, APIs |
Example | Chat apps, live feeds | Loading web pages, form submissions |
Pro Tip: WebSockets begin with an HTTP handshake (status code 101) before upgrading to the WebSocket protocol (ws:// or wss://).
Setting Up WebSockets
1. Install the ws Module
First, create a new directory for your project and initialize it:
mkdir websocket-demo
cd websocket-demo
npm init -y
Then, install the ws
package:
npm install ws
Note: The ws
module is a simple, fast, and thoroughly tested WebSocket client and server implementation.
Creating a WebSocket Server
Let's create a simple WebSocket server that echoes back any message it receives.
Create a new file called server.js
:
Example: WebSocket Echo Server
const WebSocket = require('ws');
// Create a WebSocket server on port 8080
const wss = new WebSocket.Server({ port: 8080 });
console.log('WebSocket server is running on ws://localhost:8080');
// Connection event handler
wss.on('connection', (ws) => {
console.log('New client connected');
// Send a welcome message to the client
ws.send('Welcome to the WebSocket server!');
// Message event handler
ws.on('message', (message) => {
console.log(`Received: ${message}`);
// Echo the message back to the client
ws.send(`Server received: ${message}`);
});
// Close event handler
ws.on('close', () => {
console.log('Client disconnected');
});
});
Understanding the Code
- We import the
ws
module - Create a new WebSocket server on port 8080
- Set up event handlers for connections, messages, and disconnections
- Echo back any received messages to the client
Try It Out
1. Save the code above as server.js
2. Run the server: node server.js
3. The server will start and listen on ws://localhost:8080
Creating a WebSocket Client
Now that we have a WebSocket server, let's create clients to connect to it. We'll create both a Node.js client and a browser client.
1. Node.js Client
Create a new file called client.js
:
const WebSocket = require('ws');
const readline = require('readline');
// Create readline interface for user input
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Connect to the WebSocket server
const ws = new WebSocket('ws://localhost:8080');
// Connection opened
ws.on('open', () => {
console.log('Connected to the WebSocket server');
promptForMessage();
});
// Listen for messages from the server
ws.on('message', (message) => {
console.log(`Server: ${message}`);
});
// Handle errors
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
// Handle connection close
ws.on('close', () => {
console.log('Disconnected from the server');
process.exit(0);
});
// Function to prompt user for messages
function promptForMessage() {
rl.question('Enter a message (or "exit" to quit): ', (message) => {
if (message.toLowerCase() === 'exit') {
ws.close();
rl.close();
return;
}
ws.send(message);
promptForMessage();
});
}
How to Use the Node.js Client
- Save the code above as
client.js
- Make sure the WebSocket server is running
- Run the client:
node client.js
- Type messages and press Enter to send them to the server
- Type "exit" to quit
2. Browser Client
Let's create a simple HTML page with JavaScript to connect to our WebSocket server.
Create a file named index.html
:
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Client</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
#messages {
height: 300px;
border: 1px solid #ccc;
overflow-y: auto;
padding: 10px;
margin-bottom: 10px;
}
.message { margin: 5px 0; }
</style>
</head>
<body>
<h1>WebSocket Client</h1>
<div id="status">Connecting to server...</div>
<div id="messages"></div>
<div>
<input type="text" id="messageInput" placeholder="Type your message">
<button onclick="sendMessage()">Send</button>
</div>
<script>
const status = document.getElementById('status');
const messages = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
// Connect to the WebSocket server
const ws = new WebSocket('ws://localhost:8080');
// Connection opened
ws.onopen = () => {
status.textContent = 'Connected to server';
status.style.color = 'green';
};
// Listen for messages
ws.onmessage = (event) => {
const message = document.createElement('div');
message.className = 'message';
message.textContent = event.data;
messages.appendChild(message);
messages.scrollTop = messages.scrollHeight;
};
// Handle errors
ws.onerror = (error) => {
status.textContent = 'Error: ' + error.message;
status.style.color = 'red';
};
// Handle connection close
ws.onclose = () => {
status.textContent = 'Disconnected from server';
status.style.color = 'red';
};
// Function to send a message
function sendMessage() {
const message = messageInput.value.trim();
if (message) {
ws.send(message);
messageInput.value = '';
}
}
// Send message on Enter key
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>
Try it Yourself »
How to Use the Browser Client
- Save the code above as
index.html
- Make sure the WebSocket server is running
- Open the HTML file in a web browser
- Type messages in the input field and click Send or press Enter
Note: For the browser client to work, you'll need to serve the HTML file through a web server (like http-server
or live-server
) due to browser security restrictions.
3. Testing the Application
- Start the WebSocket server:
node server.js
- Open multiple browser windows with the client HTML page
- Send messages from different clients and see them appear in real-time
- You can also run the Node.js client alongside the browser clients
Understanding the Implementation
- The server maintains a set of all connected clients
- When a message is received from one client, it's broadcast to all others
- The client handles connection, disconnection, and error events
- Messages are displayed in real-time as they're received
WebSocket Events
WebSockets use an event-driven model. Here are the key events:
Event | Description |
---|---|
connection (server) |
Fired when a client connects to the server |
open (client) |
Fired when the connection is established |
message |
Fired when a message is received |
error |
Fired when an error occurs |
close |
Fired when the connection is closed |
Real-world Applications
WebSockets are used in a variety of real-world applications:
- Chat Applications: Instant message delivery
- Live Dashboards: Real-time updates of metrics and data
- Collaborative Tools: Multiple users editing the same document
- Gaming: Multiplayer online games requiring fast interactions
- Financial Platforms: Real-time stock tickers and trading platforms
- IoT Applications: Monitoring and controlling connected devices
Advanced WebSocket Features
1. Binary Data Transfer
WebSockets support sending binary data, which is more efficient for certain types of data:
// Sending binary data (server-side)
const buffer = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // 'Hello' in binary
ws.send(buffer, { binary: true });
// Receiving binary data (client-side)
ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new Uint8Array(event.data);
console.log('Received binary data:', view);
}
};
2. Heartbeats and Connection Monitoring
Implement heartbeats to detect and handle disconnections:
// Server-side heartbeat
function setupHeartbeat(ws) {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
}
// Ping all clients every 30 seconds
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
// Clean up on server close
wss.on('close', () => {
clearInterval(interval);
});
Security Considerations
1. Authentication
Always authenticate WebSocket connections:
const http = require('http');
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const server = http.createServer();
const wss = new WebSocket.Server({ noServer: true });
// Handle upgrade with authentication
server.on('upgrade', (request, socket, head) => {
try {
const token = request.url.split('token=')[1];
if (!token) throw new Error('No token provided');
// Verify JWT token
jwt.verify(token, 'your-secret-key', (err, decoded) => {
if (err) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
// Proceed with WebSocket handshake
wss.handleUpgrade(request, socket, head, (ws) => {
ws.user = decoded; // Attach user data to WebSocket
wss.emit('connection', ws, request);
});
});
} catch (error) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
}
});
2. Rate Limiting
Prevent abuse with rate limiting:
const rateLimit = require('ws-rate-limit');
// Limit to 100 messages per minute per connection
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100,
message: 'Too many messages, please slow down!',
});
wss.on('connection', (ws) => {
limiter(ws);
// ... rest of your connection handler
});
3. Input Validation
Always validate incoming messages:
const Joi = require('joi');
const messageSchema = Joi.object({
type: Joi.string().valid('chat', 'join', 'leave').required(),
username: Joi.string().alphanum().min(3).max(30),
message: Joi.string().max(1000),
room: Joi.string().alphanum().max(50),
});
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
const { error, value } = messageSchema.validate(message);
if (error) {
throw new Error(`Invalid message: ${error.details[0].message}`);
}
// Process valid message...
} catch (err) {
ws.send(JSON.stringify({ error: err.message }));
}
});
Performance Optimization
Compression
Enable per-message deflate to reduce bandwidth usage:
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
// Other options
clientNoContextTakeover: true,
serverNoContextTakeover: true,
concurrencyLimit: 10,
}
});
Best Practice: For production applications, consider using libraries like Socket.IO which provides additional features like fallbacks for browsers that don't support WebSockets.