Unit 3 - Notes
INT222
Unit 3: Socket Services in Node.js & Creating middlewares
Part 1: Socket Services in Node.js
1. Understanding Network Sockets
A Network Socket is an internal endpoint for sending or receiving data within a node on a computer network. In the context of web development, we primarily focus on WebSockets, which provide a distinct alternative to the traditional HTTP Request-Response model.
- The HTTP Limit: Standard HTTP is stateless and unidirectional. The client requests, the server responds, and the connection closes. To get new data, the client must request again (polling).
- The WebSocket Solution: The WebSocket Protocol (RFC 6455) provides full-duplex communication channels over a single TCP connection.
- Persistent: The connection stays open.
- Bi-directional: Both client and server can send data independently at any time.
- Low Latency: Minimal overhead compared to HTTP headers.
- The Handshake: A WebSocket connection begins as a standard HTTP request with an
Upgradeheader. If the server approves, the protocol switches from HTTP to WebSocket.
2. Creating a Basic WebSocket Server
While Node.js has a built-in net module for raw TCP sockets, the ws library is the industry standard for implementing the WebSocket protocol specifically.
Installation:
npm install ws
Server Implementation (server.js):
const WebSocket = require('ws');
// Create a WebSocket Server on port 8080
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('New client connected!');
// Send a message to the connected client immediately
ws.send('Welcome to the WebSocket server!');
});
console.log("Server started on port 8080");
3. Sending and Receiving Messages
Once a connection is established, communication relies on event listeners.
ws.on('message', callback): Triggered when data is received from the client.ws.send(data): Sends data to the specific client instance.ws.on('close', callback): Triggered when the client disconnects.
Enhanced Echo Server Example:
wss.on('connection', (ws) => {
// Event: Receiving a message
ws.on('message', (message) => {
console.log(`Received: ${message}`);
// Logic: Echo the message back to the client in uppercase
ws.send(`Server says: ${message.toString().toUpperCase()}`);
});
// Event: Client disconnects
ws.on('close', () => {
console.log('Client has disconnected');
});
// Handling errors
ws.on('error', (error) => {
console.error('WebSocket error observed:', error);
});
});
4. A Socket.IO Chat Server
Socket.IO is a library built on top of WebSockets. It is often preferred over raw ws because it provides:
- Fallbacks: If WebSockets are not supported by the browser/network, it falls back to HTTP Long-Polling.
- Auto-reconnection: Automatically tries to reconnect if the line drops.
- Broadcasting: Easier syntax to send messages to all connected clients.
- Rooms/Namespaces: Logic to group sockets together.
Installation:
npm install socket.io express
Server Implementation (index.js):
const express = require('express');
const http = require('http');
const { Server } = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = new Server(server);
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', (socket) => {
console.log('A user connected: ' + socket.id);
// Listen for 'chat message' events from client
socket.on('chat message', (msg) => {
// Broadcast the message to ALL connected clients (including sender)
io.emit('chat message', msg);
// To broadcast to everyone EXCEPT sender:
// socket.broadcast.emit('chat message', msg);
});
socket.on('disconnect', () => {
console.log('User disconnected');
});
});
server.listen(3000, () => {
console.log('Listening on *:3000');
});
Client Implementation (index.html snippet):
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
// Sending
form.addEventListener('submit', function(e) {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});
// Receiving
socket.on('chat message', function(msg) {
var item = document.createElement('li');
item.textContent = msg;
messages.appendChild(item);
});
</script>
Part 2: Creating Middleware in Express
5. Introduction to Middleware
In Express.js, middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle.
Think of middleware as a processing pipeline. When a request hits the server, it passes through a series of middleware functions.
Middleware can perform the following tasks:
- Execute any code.
- Make changes to the request and the response objects.
- End the request-response cycle.
- Call the next middleware function in the stack.
6. Implementing Basic Middleware
If the current middleware function does not end the request-response cycle (e.g., by sending a response via res.send()), it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.
Syntax:
const myMiddleware = (req, res, next) => {
// Logic here
next(); // Pass control
}
Example: A Logger Middleware:
This middleware logs the time and method of every request.
const express = require('express');
const app = express();
// Defining the middleware
const requestLogger = (req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} request to ${req.url}`);
next(); // Crucial: Move to the next function/route handler
};
// Mounting the middleware globally
app.use(requestLogger);
app.get('/', (req, res) => {
res.send('Home Page');
});
app.listen(3000);
7. app.use() vs app.all()
app.use([path,] callback)
- Purpose: Mounts specified middleware function(s) at the specified path.
- Behavior: It matches the path prefix.
- Example:
app.use('/api', ...)will match/api,/api/users,/api/products, etc. - Defaults: If no path is specified, it executes for every request.
app.all(path, callback)
- Purpose: Handles all HTTP methods (GET, POST, PUT, DELETE, etc.) for a specific route.
- Behavior: It requires an exact path match (unless regex is used).
- Usage: Useful for global logic specific to a single endpoint, such as requiring authentication for any action on
/secret.
Comparison Example:
// Runs for /admin, /admin/dashboard, /admin/users
app.use('/admin', (req, res, next) => {
console.log('Admin Request');
next();
});
// Runs ONLY for /secret, regardless of whether it is GET or POST
app.all('/secret', (req, res, next) => {
console.log('Secret accessed via any Method');
next();
});
8. cookie-parser
Express does not parse cookies by default. cookie-parser is a third-party middleware that parses the Cookie header and populates req.cookies.
Installation: npm install cookie-parser
Usage:
const cookieParser = require('cookie-parser');
app.use(cookieParser()); // Can pass a secret string for signed cookies
app.get('/set-cookie', (req, res) => {
// Set a cookie (name, value, options)
res.cookie('username', 'JohnDoe', { maxAge: 900000, httpOnly: true });
res.send('Cookie set');
});
app.get('/read-cookie', (req, res) => {
// Access the cookie
console.log(req.cookies.username);
res.send(`User is ${req.cookies.username}`);
});
9. cookie-session
cookie-session is a middleware for storing session data purely on the client-side (in the browser cookie).
- Mechanism: The entire session object is serialized and stored in the cookie.
- Pros: No server-side database required; very fast.
- Cons: Cookie size limit (4KB); data is visible to user (unless encrypted); cannot invalidate sessions from server easily.
Installation: npm install cookie-session
Usage:
const cookieSession = require('cookie-session');
app.use(cookieSession({
name: 'session',
keys: ['key1', 'key2'], // Used to sign/encrypt the cookie
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}));
app.get('/', (req, res) => {
// Update session data
req.session.views = (req.session.views || 0) + 1;
res.send(`You have visited this page ${req.session.views} times`);
});
10. express-session
express-session stores session data on the server-side. The client only receives a unique Session ID in the cookie.
- Mechanism: The browser sends the Session ID. The server looks up the ID in a store (Memory, Redis, MongoDB) to retrieve the data.
- Pros: Can store large amounts of data; more secure (data not exposed to client); sessions can be deleted by the server.
- Cons: Requires server resources (RAM or DB connection).
Installation: npm install express-session
Usage:
const session = require('express-session');
app.use(session({
secret: 'mySecretKey', // Signs the session ID cookie
resave: false, // Don't save session if unmodified
saveUninitialized: false, // Don't create session until something stored
cookie: { secure: false } // Set true if using HTTPS
}));
app.get('/login', (req, res) => {
// Store data on server linked to this client's ID
req.session.user = "Alice";
res.send('Logged in');
});
app.get('/profile', (req, res) => {
if(req.session.user) {
res.send(`Hello ${req.session.user}`);
} else {
res.send('Please login first');
}
});
Summary Table: cookie-session vs express-session
| Feature | cookie-session | express-session |
|---|---|---|
| Storage Location | Client (Browser Cookie) | Server (Memory/Database) |
| Data in Cookie | Full data payload | Session ID only |
| Data Size Limit | ~4KB | Limited only by server storage |
| Security | Depends on encryption keys | High (data hidden from client) |
| Use Case | Lightweight, ephemeral data | Auth, Shopping carts, Sensitive data |