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 Upgrade header. 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:

BASH
npm install ws

Server Implementation (server.js):

JAVASCRIPT
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:

JAVASCRIPT
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:

BASH
npm install socket.io express

Server Implementation (index.js):

JAVASCRIPT
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):

HTML
<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:

  1. Execute any code.
  2. Make changes to the request and the response objects.
  3. End the request-response cycle.
  4. 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:

JAVASCRIPT
const myMiddleware = (req, res, next) => {
    // Logic here
    next(); // Pass control
}

Example: A Logger Middleware:
This middleware logs the time and method of every request.

JAVASCRIPT
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:

JAVASCRIPT
// 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:

JAVASCRIPT
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:

JAVASCRIPT
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:

JAVASCRIPT
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