Node.js WebSocket Basics

When building modern real-time applications, the traditional HTTP request-response model often proves insufficient. Implementing features such as instant messaging, online game state synchronization, or stock market push notifications requires establishing full-duplex communication channels. Node.js has become the preferred solution for building WebSocket servers thanks to its non-blocking I/O model and the built-in ws module (or third-party libraries installed via npm).

The WebSocket protocol is built upon TCP, allowing clients and servers to exchange data bidirectionally immediately after connection establishment, without needing to wait for server responses or initiate new requests like HTTP does. This mechanism significantly reduces latency and enhances user experience. For Node.js developers, understanding its core concepts is fundamental to writing efficient backend code: the handshake phase negotiates an upgrade from HTTP to WebSocket via the HTTP protocol; once the connection is successful, both parties can send text or binary data streams of arbitrary length.
Before beginning to code, ensure that your project environment has the necessary dependency libraries installed. While Node.js 18+ includes the ws module by default, it is generally recommended to explicitly manage it via npm in production environments and complex projects for better maintainability and feature support. Execute the following commands in your terminal:

npm init -y
npm install ws express cors

In this setup, express is used to build a lightweight HTTP server, while cors (Cross-Origin Resource Sharing) addresses common cross-origin restriction issues when browsers call WebSocket endpoints. Once dependencies are configured, you can create the main entry file server.js to initialize the service logic.

Implementing Basic WebSocket Connection Management

Core business logic typically revolves around "establishing a connection," "sending messages," and "handling disconnections." Below is a complete example code based on the Express framework:

const express = require('express');
const http = require('http');
const { Server } = require("ws"); // Note import method, ensure ws@^8.0.0+ is installed
require('cors')(); // Assume CORS is already configured
// ... Omit other initialization code ...

const app = express();
app.use(express.json());

const server = http.createServer(app);

// Instantiate WebSocket Server instance
const wss = new Server({ server }, {
    maxPayload: 1024 * 1024, // Set maximum message size to 1MB to prevent large payload attacks
});

wss.on('connection', (ws) => {
    console.log(`User connected: ${ws.readyState}`);
    
    ws.on('message', (data) => {
        const message = data.toString();
        
        // Example logic: Broadcast received message to all online clients (simulating a chat room)
        wss.clients.forEach((client) => {
            if (client.readyState === 1 || client.readyState === 2) { // OPEN or CLOSING
                client.send(message);
            }
        });

        console.log(`Received message from ${ws.url}: ${message}`);
    });

    ws.on('close', () => {
        console.log('User disconnected');
    });
});

server.listen(8080, 'localhost', () => {
    console.log("WebSocket Server running at http://localhost:8080");
});

This code demonstrates the most basic handshake and message processing flow. The Server constructor accepts two parameters: an underlying HTTP server instance and a WebSocket configuration object. Once a client initiates a connection, the event listener automatically triggers the connection callback function. Here, we register listeners for the message (received data) and close (disconnection) events, thereby gaining control over the communication state.

Advanced: Message Validation and Error Handling Mechanisms

In actual production environments, directly forwarding all client messages poses security risks and resource waste risks. Therefore, introducing simple message filtering logic is crucial. For instance, one can restrict passing through only specific format JSON data or perform permission checks on sensitive operations. Additionally, abnormal disconnection scenarios must be handled properly to prevent the server from entering an infinite loop or experiencing memory leaks.

ws.on('message', (data) => {
    try {
        const payload = JSON.parse(data.toString()); // Attempt to parse as object
        
        if (!payload || typeof payload !== 'object') {
            ws.send(JSON.stringify({ error: "Invalid message format" }));
            return;
        }

        // Simulate business logic: Check if user ID exists in the whitelist
        const userId = payload.userId; 
        if (userId === null) {
             throw new Error("No valid user identifier provided");
        }

        ws.send(JSON.stringify({ status: "success", message: `Message processed for ${userId}` }));
    } catch (err) {
        console.error(`Parse error: ${err.message}`, err);
        // Log the error or send an error notification, but do not close the connection directly unless it is a malicious attack
    }
});

// Listen for abnormal disconnections (e.g., network interruption)
ws.on('error', (err) => {
    console.error("WebSocket exception:", err);
});

Wrapping the parsing logic within a try-catch block effectively prevents process crashes caused by format errors. Simultaneously, validating key business fields enhances system robustness. For unexpected connection interruptions (such as heartbeat timeouts), logs should be recorded to facilitate troubleshooting the root cause rather than blindly closing the connection immediately.