Node: In-depth Yet Easy to Understand (Shengsi Garden Education) 003 [Study Notes]

WebSocket and SSE Overview WebSocket Basics Definition: WebSocket is a full-duplex connection upgraded after an HTTP handshake, allowing clients and servers to push data bidirectionally over the same TCP channel, eliminating the need for repeated polling. Handshake Process: The client initiates an HTTP request with the Upgrade: websocket header; The server responds with 101 Switching Protocols, and both parties agree...

WebSocket and SSE Overview

WebSocket Basics

  • Definition: WebSocket is a full-duplex connection upgraded after an HTTP handshake, allowing clients and servers to push data bidirectionally over the same TCP channel, eliminating the need for repeated polling.
  • Handshake Process:
  • The client initiates an HTTP request with the Upgrade: websocket header;
  • The server responds with 101 Switching Protocols, and both parties negotiate subprotocols, compression, and other extensions;
  • Subsequent communication switches to WebSocket frames, following the FIN/Opcode/Payload format.
  • Features: Persistent state, minimal headers, support for binary/text frames, and broadcast capabilities segmented by rooms.
  • Applicable Scenarios: IM/chat rooms, collaborative editing, real-time dashboards, online games, and other services requiring low-latency bidirectional communication.

Node.js + Socket.IO Example

socket.io encapsulates details such as handshake, heartbeats, automatic reconnection, and fallback polling. It works through its own protocol, server-side component (socket.io), and client-side component (socket.io-client).

Directory and Dependency Initialization

mkdir websocket-demo && cd websocket-demo
npm init -y
npm install socket.io express

Server Example

// server.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, {
  cors: { origin: '*' },
  pingInterval: 10000, // Heartbeat frequency
});

io.on('connection', (socket) => {
  console.log('client connected:', socket.id);

  // Custom event: client sends chat-message
  socket.on('chat-message', (payload) => {
    // Acknowledgment sent to the current client
    socket.emit('chat-ack', { id: payload.id, status: 'OK' });
    // Broadcast to all other clients
    socket.broadcast.emit('chat-message', {
      ...payload,
      from: socket.id,
    });
  });

  // Server actively broadcasts
  socket.on('join-room', (room) => {
    socket.join(room);
    io.to(room).emit('room-notify', {
      room,
      memberCount: io.sockets.adapter.rooms.get(room)?.size || 0,
    });
  });

  socket.on('disconnect', (reason) => {
    console.log('disconnect:', socket.id, reason);
  });
});

server.listen(3000, () => {
  console.log('socket.io server listening on 3000');
});

Client Example

<!-- client.html -->
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<script>
  const socket = io('http://localhost:3000', { transports: ['websocket'] });

  socket.on('connect', () => {
    console.log('connected', socket.id);
    socket.emit('join-room', 'general');
    socket.emit('chat-message', { id: Date.now(), text: 'Hello everyone!' });
  });

  socket.on('chat-message', (msg) => {
    console.log('[broadcast]', msg);
  });

  socket.on('room-notify', (data) => {
    console.log(`[room ${data.room}] members:`, data.memberCount);
  });

  socket.on('disconnect', (reason) => {
    console.log('connection closed:', reason);
  });
</script>

Custom Events and Broadcast Strategies

  • socket.on('<event>', handler): Listens for custom events, such as chat-message, typing.
  • socket.emit: Sends only to the current connection (often used for acknowledgments, private messages).
  • socket.broadcast.emit: Sends to all clients except the sender, suitable for global broadcasts outside of rooms.
  • io.emit: Sends to all connected clients, including the sender, suitable for system messages.
  • io.to(room).emit: Room-level broadcast, combined with socket.join(room)/socket.leave(room) for managing subscriptions.
  • socket.compress(false).emit etc.: For high-traffic broadcasts, parameters like compression and Acks can be controlled to meet performance requirements.

SSE (Server-Sent Events)

Concepts and Features

  • Based on HTTP/1.1 persistent connections, where the server unidirectionally pushes text events to the browser's EventSource.
  • The event format is text/event-stream, with each event containing fields like event, data, id, separated by \n\n.
  • Browsers natively support automatic reconnection and Last-Event-ID for resuming from breakpoints, suitable for notifications, market data, and log streaming.
  • SSE is unidirectional (server -> client); if client-side messages are needed, it still requires cooperation with POST/AJAX; whereas WebSocket is full-duplex.

Node Server Example

// sse-server.js
const express = require('express');
const app = express();

app.get('/events', (req, res) => {
  res.set({
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });
  res.flushHeaders();

  let counter = 0;
  const timer = setInterval(() => {
    counter += 1;
    res.write(`event: tick\n`);
    res.write(`id: ${counter}\n`);
    res.write(`data: ${JSON.stringify({ counter, ts: Date.now() })}\n\n`);
  }, 2000);

  req.on('close', () => {
    clearInterval(timer);
    res.end();
  });
});

app.listen(4000, () => console.log('SSE server on 4000'));

Browser Client Example

<script>
  const evtSource = new EventSource('http://localhost:4000/events');

  evtSource.addEventListener('tick', (evt) => {
    console.log('SSE tick payload:', JSON.parse(evt.data));
  });

  evtSource.onerror = (err) => {
    console.error('SSE connection lost', err);
  };
</script>

Comparison with WebSocket

  • Communication Direction: SSE is only server -> client; WebSocket supports bidirectional.
  • Protocol/Compatibility: SSE relies on HTTP, making it more proxy and firewall-friendly; WebSocket requires Upgrade, which might have limitations in older environments.
  • Throughput & Binary: SSE only transmits text, suitable for low to medium frequency pushes; WebSocket supports binary frames, more suitable for high frequency/large data.
  • Heartbeat: SSE has built-in reconnection mechanisms; WebSocket requires application-layer heartbeats (Socket.IO already encapsulates this).

In actual projects, the choice can be made based on the scenario: for example, backend configuration and market data pushes can use SSE; real-time collaboration, IM, and control panels would prioritize WebSocket/Socket.IO.

HTTP/2 and Long Connections

Core Features

  • Inherits the persistent connection model of HTTP/1.1, using multiplexing to concurrently handle multiple streams over a single TCP long connection, avoiding head-of-line blocking.
  • Employs a binary frame layer, splitting requests/responses into frames like HEADERS, DATA, etc., distinguished by frame ID to identify streams.
  • HPACK header compression + static tables significantly reduce the transmission overhead of duplicate headers.
  • Supports server push (Server Push, which browsers are gradually deprecating, but still usable in specific clients), and can also achieve streaming responses by maintaining DATA frames for extended periods.

Node.js http2 Demo

mkdir http2-demo && cd http2-demo
npm init -y

Create server.mjs:

import fs from 'node:fs';
import http2 from 'node:http2';

// Browsers require https; you can use mkcert/openssl to generate local certificates; this example uses self-signed certificates
const server = http2.createSecureServer({
  key: fs.readFileSync('./localhost-key.pem'),
  cert: fs.readFileSync('./localhost-cert.pem'),
  allowHTTP1: true, // Compatible with older clients
});

server.on('stream', (stream, headers) => {
  const path = headers[':path'];
  if (path === '/time') {
    // Stream data using DATA frames to achieve real-time updates over a long connection
    const timer = setInterval(() => {
      stream.write(JSON.stringify({ ts: Date.now() }) + '\n');
    }, 2000);
    stream.on('close', () => clearInterval(timer));
    return;
  }

  stream.respond({
    'content-type': 'application/json',
    ':status': 200,
  });
  stream.end(JSON.stringify({ hello: 'http2' }));
});

server.listen(8443, () => {
  console.log('HTTP/2 server running https://localhost:8443');
});

Client Verification

  • Browser: Using fetch('https://localhost:8443/time') requires trusting the self-signed certificate. You can confirm the h2 protocol in the DevTools network panel, and the response will continuously append data.
  • CLI: curl --http2 -k https://localhost:8443/time, where -k skips certificate verification. You will see a line of data every 2 seconds.

You can also write a Node client:

// client.mjs
import http2 from 'node:http2';

const client = http2.connect('https://localhost:8443', {
  rejectUnauthorized: false,
});

const req = client.request({ ':path': '/time' });
req.on('data', (chunk) => {
  process.stdout.write('tick ' + chunk.toString());
});
req.on('close', () => client.close());
req.end();

Trade-offs with WebSocket/SSE

  • HTTP/2 long connections still follow the request-response semantic; the client must send HEADERS before receiving DATA. Once a WebSocket connection is upgraded, there is no longer a concept of requests.
  • For pure browser push scenarios, if HTTP/2 is already upgraded, long responses or SSE can replace an additional WS link, reducing heartbeat management. However, HTTP/2 still does not support client-initiated upstream messages without sending a request.
  • If bidirectional communication or cross-TCP room broadcasting is needed, WebSocket is more direct. If it's a unidirectional notification and the infrastructure already supports HTTP/2, then leveraging its multiplexing/compression can make interfaces more efficient.

Node.js Event Model

Event Loop Concept

  • Node Architecture: Single-threaded JS execution thread + libuv thread pool in the background. All I/O tasks, once completed in the kernel or thread pool, schedule callbacks into the JS main thread via the Event Loop.
  • Event Loop Phases (each tick executes in order):
  • timers: Expired setTimeout/setInterval;
  • pending callbacks: Some system-level callbacks, such as TCP errors;
  • idle/prepare: Internal use;
  • poll: Retrieves new I/O events and executes related callbacks. If empty, it may proceed to the next phase or block and wait;
  • check: Specifically executes setImmediate;
  • close callbacks: For example, socket.on('close', ...).
  • Microtasks: process.nextTick executes preferentially at the end of each phase, followed closely by Promise microtasks; therefore, judicious use of nextTick/queueMicrotask can adjust the execution order of callbacks.

Differences from Browser Event Loop

  • Different Phase Model: The browser follows the HTML standard's Task/Microtask queues (macro tasks like setTimeout, DOM events, fetch callbacks; micro tasks like Promise, MutationObserver). After each macro task, all micro tasks are cleared. Node, driven by libuv, has 6 phases (timers/poll/check, etc.). Microtasks are polled only after each phase, and Promise microtasks are executed after the dedicated process.nextTick queue.
  • API Differences: Browsers do not have setImmediate, process.nextTick, but use methods like MessageChannel, requestAnimationFrame, postMessage to influence scheduling. Node's setImmediate is tied to the check phase, and nextTick is used to interrupt the current phase. The same setTimeout(fn, 0) executes in the browser at least after 4ms (>=5 nested calls), while in Node, it is scheduled in the timers phase as soon as 0ms is reached.
  • Rendering and UI Constraints: The browser event loop is coupled with the rendering pipeline—requestAnimationFrame, layout/paint execute between macro tasks. If JS occupies the main thread for too long, it blocks page rendering. Node does not have a rendering phase; CPU-intensive logic only blocks I/O callbacks from entering the main thread.
  • Multiple Event Loops: In browsers, each window/frame/worker has an independent event loop, interacting via postMessage. Node has only one main event loop; if parallelism is needed, worker_threads/cluster can be used to create additional threads/processes.
  • Microtask Priority: Browser microtasks (Promises, etc.) execute uniformly after macro tasks complete. Node executes in the order of process.nextTick → Promise microtask → next phase. Therefore, excessive use of nextTick might starve I/O, while browsers are mainly concerned about microtasks not being cleared for too long, leading to rendering delays.

Event Loop Example

// event-loop.js
const fs = require('fs');

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));

fs.readFile(__filename, () => {
  console.log('I/O callback');
  setTimeout(() => console.log('timeout in I/O'), 0);
  setImmediate(() => console.log('immediate in I/O'));
});

console.log('sync');

Possible output (depends on libuv poll phase status):

sync
nextTick          // Microtasks execute first
timeout / immediate (order not fixed)
I/O callback
immediate in I/O  // check phase executes immediately after I/O callback phase
timeout in I/O

This script can be used to observe the scheduling order of timers/poll/check in different contexts.

EventEmitter and Custom Events

Many Node internal APIs are based on EventEmitter (http.Server, net.Socket, etc.). We can also send/listen for custom events by inheriting or directly instantiating it.

// custom-event.js
const EventEmitter = require('events');

class TaskBus extends EventEmitter {
  addTask(task) {
    // Simulate triggering a custom event after asynchronous execution completes
    setTimeout(() => {
      this.emit('task:done', {
        id: task.id,
        result: task.work.toUpperCase(),
      });
    }, 100);
  }
}

const bus = new TaskBus();

bus.on('task:done', (payload) => {
  console.log('Task finished:', payload);
});

bus.once('task:done', () => {
  console.log('This runs only for the first completion');
});

bus.addTask({ id: 1, work: 'compile' });
bus.addTask({ id: 2, work: 'test' });

Key points:

  • on/addListener: Add a long-term listener; once: A one-time listener that is automatically removed after being triggered.
  • emit(event, ...args): Synchronously triggers listeners, executing them in registration order. To trigger asynchronously, use setImmediate before or after emit.
  • removeListener/off and removeAllListeners are used to release resources and prevent leaks.

The event-driven pattern facilitates decoupling producers and consumers: I/O completion, business state changes, etc., are all encapsulated as events. Combined with the event loop's scheduling, Node can maintain a single-threaded programming model while fully leveraging the high concurrency capabilities of asynchronous I/O.

Node's Underlying Asynchronous I/O and Thread Pool

libuv Model

  • Node's asynchronous I/O capability comes from the C-layer libuv: it requests asynchronous file/TCP/DNS operations from the operating system and triggers JS callbacks via the event loop upon completion.
  • For truly asynchronous kernel calls (most socket, epoll/kqueue supported file descriptors), libuv only needs to register events and notify upwards when readable/writable, without occupying the thread pool.
  • For operations that cannot provide non-blocking interfaces (such as some file system calls, DNS resolution, compression/encryption, and other CPU-intensive tasks), libuv dispatches these tasks to a fixed-size thread pool for execution. Upon completion, the results are then delivered back to the event loop.

Thread Pool Details

  • The default size is 4, adjustable via the environment variable UV_THREADPOOL_SIZE (up to 128), e.g., UV_THREADPOOL_SIZE=8 node server.js.
  • Node APIs that use the thread pool include: fs.readFile/writeFile, crypto.pbkdf2, zlib, dns.lookup (default), fs.stat, etc.
  • If the thread pool is full, subsequent similar operations will queue up and wait; therefore, in high-concurrency file I/O scenarios, it is recommended to change synchronous read/write to streaming fs.createReadStream/createWriteStream, or appropriately increase the pool size.

Example: Comparing I/O and CPU-intensive Tasks

// threadpool.js
const crypto = require('crypto');
const fs = require('fs');

console.time('fs');
for (let i = 0; i < 6; i++) {
  fs.readFile(__filename, () => {
    console.timeLog('fs', 'read file done', i);
  });
}

console.time('pbkdf2');
for (let i = 0; i < 6; i++) {
  crypto.pbkdf2('secret', 'salt', 1e6, 64, 'sha512', () => {
    console.timeLog('pbkdf2', 'hash done', i);
  });
}

After running, you will find that pbkdf2 callbacks process a maximum of 4 tasks simultaneously because the thread pool defaults to only 4 worker threads; fs.readFile also occupies the same pool. If you have higher demands for parallelism in CPU-intensive tasks, you can increase UV_THREADPOOL_SIZE at startup, but be mindful of the number of machine cores and context switching costs.

Collaboration with the Event Loop

  • After the thread pool completes a task, it places the completion event into the pending callbacks phase queue; the event loop processes them in the next round, thereby invoking the JS callback.
  • If the JS layer processes CPU logic for a long time, the event loop cannot quickly return to the poll phase, and the results queued in the thread pool cannot be consumed. This is the single-threaded blocking problem. Solutions include: splitting tasks, using worker_threads, or migrating to dedicated processes.

Practical Advice

  • Avoid executing heavy CPU operations like crypto.pbkdf2, zlib.gzip directly in the request thread; they can be placed in worker_threads or external services.
  • For large numbers of file operations, use streaming APIs, or adjust UV_THREADPOOL_SIZE combined with batching/rate limiting.
  • For network I/O, try to use truly asynchronous socket/HTTP clients. With libuv's epoll/kqueue capabilities, tens of thousands of connections can be managed concurrently in a single thread.

Overall, Node's "asynchronous I/O + thread pool" model separates CPU and I/O work: I/O events are notified by the kernel, CPU-intensive tasks are handled by the thread pool, and the JS main thread is only responsible for driving the state machine and business logic, thereby achieving high concurrency and high throughput.

Node Connecting to MySQL / MongoDB / Redis

MySQL: Transactional Business

Suitable for strong consistency scenarios like e-commerce orders and inventory. mysql2 is recommended, supporting Promises and connection pools.

npm install mysql2
// mysql.js
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: 'localhost',
  user: 'app_user',
  password: 'secret',
  database: 'shop',
  waitForConnections: true,
  connectionLimit: 10,
});

async function createOrder(userId, items) {
  const conn = await pool.getConnection();
  try {
    await conn.beginTransaction();
    const [orderResult] = await conn.execute(
      'INSERT INTO orders(user_id, status) VALUES(?, ?)',
      [userId, 'pending']
    );
    for (const item of items) {
      await conn.execute(
        'INSERT INTO order_items(order_id, sku, qty) VALUES(?,?,?)',
        [orderResult.insertId, item.sku, item.qty]
      );
    }
    await conn.commit();
    return orderResult.insertId;
  } catch (err) {
    await conn.rollback();
    throw err;
  } finally {
    conn.release();
  }
}

Practical tips:

  • Use ? placeholders for all parameters + bind arrays to prevent SQL injection;
  • Centralize long connections in a pool, set waitForConnections to avoid running out of connections;
  • For reports and read-only interfaces, configure connections to replica databases to achieve master-write, replica-read;
  • Combine with pool.on('connection', conn => conn.query('SET SESSION sql_mode=...')) for unified settings.

ORM Example: Sequelize

In medium to large projects, ORMs can be used to abstract models, relationships, and migrations.

npm install sequelize mysql2
// sequelize.js
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('shop', 'app_user', 'secret', {
  host: 'localhost',
  dialect: 'mysql',
  logging: false,
  pool: { max: 10, idle: 10000 },
});

const User = sequelize.define('User', {
  email: { type: DataTypes.STRING, unique: true },
  nickname: DataTypes.STRING,
});

const Order = sequelize.define('Order', {
  status: {
    type: DataTypes.ENUM('pending', 'paid', 'shipped'),
    defaultValue: 'pending',
  },
  totalPrice: DataTypes.DECIMAL(10, 2),
});

User.hasMany(Order, { foreignKey: 'userId' });
Order.belongsTo(User, { foreignKey: 'userId' });

async function init() {
  await sequelize.sync({ alter: true }); // In production, migrations are recommended
  const user = await User.create({ email: 'demo@test.com', nickname: 'Demo' });
  await Order.create({ userId: user.id, totalPrice: 199.99 });
  const orders = await Order.findAll({
    include: [{ model: User, attributes: ['email'] }],
  });
  console.log(JSON.stringify(orders, null, 2));
}

init().catch(console.error);

Sequelize key points:

  • Supports advanced features like model hooks, validation, scopes, optimistic locking (version);
  • In production, use sequelize-cli to maintain migrations, avoiding uncontrolled changes from sync({ alter: true }) in multi-node environments;
  • When SQL optimization is needed, sequelize.query() can be used to issue raw statements while retaining model definitions.

ORM Example: Prisma

Prisma uses Schema files to define models and generates type-safe clients, suitable for TypeScript projects.

npm install prisma @prisma/client
npx prisma init

prisma/schema.prisma:

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id       Int      @id @default(autoincrement())
  email    String   @unique
  nickname String?
  orders   Order[]
}

model Order {
  id        Int      @id @default(autoincrement())
  status    String   @default("pending")
  total     Decimal
  user      User     @relation(fields: [userId], references: [id])
  userId    Int
  createdAt DateTime @default(now())
}

Execute npx prisma migrate dev --name init to generate tables, then:

// prisma-demo.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

async function main() {
  const user = await prisma.user.create({
    data: {
      email: 'dev@test.com',
      nickname: 'dev',
      orders: {
        create: [{ total: 299.9 }, { total: 399.9, status: 'paid' }],
      },
    },
    include: { orders: true },
  });
  console.log(user);
}

main().finally(() => prisma.$disconnect());

Prisma automatically generates type definitions based on the Schema, supporting middleware (e.g., audit logs), connection pool reuse, query event hooks, etc., ensuring development efficiency while maintaining SQL control.

MongoDB: Document-oriented Scenarios

Content management, social feeds, logs, or flexible schemas are more convenient with MongoDB. You can use the official driver or mongoose.

npm install mongoose
// mongo.js
const mongoose = require('mongoose');

await mongoose.connect('mongodb://localhost:27017/forum', {
  maxPoolSize: 20,
});

const PostSchema = new mongoose.Schema({
  title: String,
  content: String,
  tags: [String],
  createdAt: { type: Date, default: Date.now },
});

PostSchema.index({ tags: 1, createdAt: -1 });

const Post = mongoose.model('Post', PostSchema);

async function listPosts(tag) {
  return Post.find({ tags: tag }).sort({ createdAt: -1 }).limit(20).lean();
}

Key points:

  • Use maxPoolSize to limit the number of connections, combined with connection monitoring;
  • Call lean() at the end of the query chain to reduce Mongoose's getter/setter overhead;
  • Create compound indexes for high-frequency fields (e.g., tag + createdAt) to avoid full table scans;
  • Utilize replica set multi-node reads (readPreference=secondaryPreferred) for reports or real-time recommendations.

Redis: Cache + Messaging

Redis excels at caching, sessions, counters, and Pub/Sub. ioredis supports Cluster/Sentinel and automatic reconnection.

npm install ioredis
// redis.js
const Redis = require('ioredis');
const redis = new Redis({ host: '127.0.0.1', port: 6379 });

async function cacheProfile(userId, profile) {
  await redis.set(`user:${userId}`, JSON.stringify(profile), 'EX', 3600);
}

async function getProfile(userId) {
  const cached = await redis.get(`user:${userId}`);
  return cached ? JSON.parse(cached) : null;
}

// Distributed lock: prevent duplicate orders
async function acquireLock(key, ttl = 5000) {
  const lockId = Date.now() + Math.random().toString(16).slice(2);
  const ok = await redis.set(key, lockId, 'NX', 'PX', ttl);
  return ok ? lockId : null;
}

// Pub/Sub: order status notifications
const sub = new Redis();
sub.subscribe('order-status');
sub.on('message', (_, payload) => {
  console.log('Order update:', payload);
});

const pub = new Redis();
pub.publish('order-status', JSON.stringify({ orderId: 1, status: 'shipped' }));

Practical advice:

  • Cache must have a TTL, combined with random expiration (EX + Math.random()) to reduce cache avalanche;
  • For persistent data, use AOF/RDB mechanisms, or Redis Cluster for higher availability;
  • Pub/Sub only guarantees best-effort delivery; for reliable consumption, use Redis Streams (XADD/XREADGROUP);
  • Global auto-incrementing IDs can use INCR, and leaderboards use ZADD/ZREVRANGE.

By reasonably combining MySQL's strong transactions, MongoDB's flexible documents, and Redis's high-speed caching, Node applications can achieve a 3-layer division of labor: write operations first persist to MySQL/MongoDB, reads prioritize hitting Redis, falling back to the source if missed; real-time events are pushed to WebSocket/SSE via Redis Pub/Sub or Stream, collaborating with the real-time communication section above to build end-to-end high-concurrency services.

Node Main Process, Child Processes, and Cluster

child_process Basics

Node defaults to single-threaded JS execution. To utilize multiple cores or call system commands, the child_process module can be used to create child processes:

  • spawn(command, args, options): Most commonly used, returns streamed stdout/stderr, suitable for long-running tasks;
  • exec(command, options, callback): Buffers output in memory, suitable for short commands;
  • fork(modulePath, args, options): Specifically designed to run Node child scripts, automatically establishing an IPC channel between parent and child processes.
// child.js
setInterval(() => {
  process.send({ type: 'tick', ts: Date.now() });
}, 1000);

process.on('message', (msg) => {
  if (msg === 'stop') process.exit(0);
});
// master.js
const { fork } = require('child_process');
const child = fork('./child.js');

child.on('message', (msg) => {
  console.log('Child message:', msg);
  if (msg.type === 'tick' && Math.random() > 0.8) {
    child.send('stop');
  }
});

child.on('exit', (code) => console.log('child exit', code));

The pipe created by fork is IPC-based, and objects passed via process.send and child.on('message') are automatically serialized. In spawn/exec scenarios, streamed communication can also be achieved via child.stdin.write and child.stdout.on('data').

Shared Ports and Cluster

A single Node process cannot utilize multiple cores. The cluster module encapsulates child_process.fork, allowing multiple Workers to share the same server port; the Master is responsible for listening to the port and distributing connections to Workers (on Linux via SO_REUSEPORT or internal Round-Robin).

// cluster-server.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');

if (cluster.isMaster) {
  const cpuCount = os.cpus().length;
  for (let i = 0; i < cpuCount; i++) {
    cluster.fork();
  }
  cluster.on('exit', (worker, code) => {
    console.log(`Worker ${worker.process.pid} died`, code);
    cluster.fork(); // Auto-restart
  });
} else {
  http
    .createServer((req, res) => {
      res.end(`Handled by worker ${process.pid}\n`);
    })
    .listen(3000);
}

The master process can listen for events like cluster.on('online'), cluster.on('message'), etc. Workers can also interact with the master process via process.send, for example, to report load or health status.

Cluster and Child Process Communication

  • Master -> Worker: worker.send(payload);
  • Worker -> Master: process.send(payload); Master listens for cluster.on('message', (worker, msg) => {});
  • Inter-worker communication requires the Master to act as a relay, or use Redis/Message Queue;
  • For CPU-intensive tasks, worker_threads is a lighter choice and shares memory; Cluster focuses on port sharing + process isolation.

Practical Advice

  • Child processes typically share the same lifecycle as the master process; ensure listening for exit/error for restarts or graceful degradation;
  • IPC messages are serialized, avoid transmitting huge objects, and consider using shared memory files or Sockets;
  • Workers in Cluster mode are independent processes, their states (cache, connections) are not shared, requiring external storage for sessions (e.g., Redis);
  • In container environments, consider using process management tools like PM2 or forever to encapsulate the cluster, providing logging, restart policies, and health checks.

Through child_process + cluster, Node can reuse multiple cores, isolate crashes, and collaborate with system commands or other Node child scripts while maintaining a single-threaded model, building more reliable services.

Express and Koa Project Setup

Express: Classic MVC Framework

Express is one of the most widely used web frameworks on Node, providing routing and middleware mechanisms, making it easy to organize MVC/REST projects.

Initialization Steps

mkdir express-demo && cd express-demo
npm init -y
npm install express dotenv morgan

Example directory structure:

express-demo/
├─ app.js            # Creates an express instance
├─ routes/
│  ├─ index.js
│  └─ users.js
├─ controllers/
│  └─ user.controller.js
├─ models/
│  └─ user.model.js  # Can use ORMs like Sequelize/Prisma
└─ middlewares/
   └─ auth.js

app.js:

const express = require('express');
const morgan = require('morgan');
require('dotenv').config();

const app = express();
app.use(express.json());
app.use(morgan('dev'));

const userRouter = require('./routes/users');
app.use('/api/users', userRouter);

app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status || 500).json({ message: err.message });
});

app.listen(process.env.PORT || 3000, () => {
  console.log('Express server running');
});

routes/users.js:

const router = require('express').Router();
const userController = require('../controllers/user.controller');
const auth = require('../middlewares/auth');

router.get('/', auth.optional, userController.list);
router.post('/', auth.required, userController.create);

module.exports = router;

controllers/user.controller.js:

const User = require('../models/user.model');

exports.list = async (req, res, next) => {
  try {
    const users = await User.findAll();
    res.json(users);
  } catch (err) {
    next(err);
  }
};

The Express model layer is typically provided by an ORM/ODM (Sequelize/Prisma/Mongoose, etc.). Express introduces a middleware stack, making it easy to insert authentication, logging, rate limiting, and other logic, suitable for REST APIs and SSR applications.

Koa: Lighter Middleware Composition

Koa, created by the Express team, uses async/await + an onion model for middleware, with a lighter core, suitable for on-demand composition.

Initialization Steps

mkdir koa-demo && cd koa-demo
npm init -y
npm install koa koa-router koa-body koa-logger dotenv

Example directory structure:

koa-demo/
├─ app.js
├─ routes/
│  └─ user.route.js
├─ controllers/
│  └─ user.controller.js
├─ services/
│  └─ user.service.js
└─ models/         # Can contain ORM definitions

app.js:

const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-body');
const logger = require('koa-logger');
require('dotenv').config();

const app = new Koa();
const router = new Router({ prefix: '/api' });

app.use(logger());
app.use(bodyParser());

const userRoute = require('./routes/user.route');
router.use('/users', userRoute.routes(), userRoute.allowedMethods());

app.use(router.routes()).use(router.allowedMethods());

app.on('error', (err, ctx) => {
  console.error('server error', err, ctx);
});

app.listen(process.env.PORT || 4000, () => {
  console.log('Koa server running');
});

routes/user.route.js:

const Router = require('koa-router');
const controller = require('../controllers/user.controller');
const authGuard = require('../middlewares/auth');

const router = new Router();
router.get('/', controller.list);
router.post('/', authGuard, controller.create);

module.exports = router;

Koa controller example:

const userService = require('../services/user.service');

exports.list = async (ctx, next) => {
  const users = await userService.findAll();
  ctx.body = users;
};

Koa's recommended "model" organization typically involves a Service/Repository layer to encapsulate business logic. Combined with the async/await onion model, it can perform streaming processing before and after requests (e.g., response compression, error capture). With middleware like koa-compose, koa-jwt, etc., it can quickly build GraphQL, REST, or BFF services.

Onion Model Principle

Koa's middleware is essentially an array of async functions (middleware stack). It uses await next() to pass control to the next middleware, and then continues execution outwards after next returns, forming a "forward then backward" onion model. The core implementation of koa-compose is as follows (simplified version):

function compose(middlewares) {
  return function (ctx) {
    return dispatch(0);
    function dispatch(i) {
      const fn = middlewares[i];
      if (!fn) return Promise.resolve();
      return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
    }
  };
}

Example:

app.use(async (ctx, next) => {
  console.log('A before');
  await next();
  console.log('A after');
});

app.use(async (ctx, next) => {
  console.log('B before');
  await next();
  console.log('B after');
});

app.use(async (ctx) => {
  console.log('handler');
  ctx.body = 'OK';
});

Output order: A before -> B before -> handler -> B after -> A after. Therefore, to perform cleanup tasks after a request (logging, transactions, response wrapping), simply write the logic after await next(); to preprocess (authentication, parsing) before entering the next layer, write it before await next(). This model can be implemented via Promise/async recursion without complex state machines.

Express vs Koa

  • Middleware Mechanism: Express is based on a callback stack; Koa is based on an async/await onion model, where middleware can process after returning from await next().
  • Ecosystem: Express has a vast middleware ecosystem; Koa's core is lightweight but requires manual selection of middleware to build functionality.
  • Default Features: Express comes with many convenient methods (res.json, express.static); Koa only provides the core, requiring additional packages.
  • Error Handling: Koa's try/catch combined with app.on('error') can catch exceptions in async functions; Express passes errors to error-handling middleware via next(err).

Whether Express or Koa, both can integrate the aforementioned database layers (MySQL/MongoDB/Redis) and real-time capabilities (WebSocket/SSE). By following a layered design of routes -> controllers -> services -> models, highly maintainable Node Web projects can be built.

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/4767

(0)
Walker的头像Walker
上一篇 Mar 10, 2026 00:00
下一篇 Mar 8, 2026 15:40

Related Posts

  • Go Engineer System Course 013 [Study Notes]

    Order transactions, whether deducting inventory first or later, will both affect inventory and orders. Therefore, distributed transactions must be used to address business issues (e.g., unpaid orders). One approach is to deduct inventory only after successful payment (e.g., an order was placed, but there was no inventory at the time of payment). Another common method is to deduct inventory when the order is placed, but if payment isn't made, the order is returned/released upon timeout.

    Transactions and Distributed Transactions
    1. What is a transaction?
    A transaction is an important concept in database management systems. It is a collection of database operations, which either all execute successfully, or all...

    Personal Nov 25, 2025
    28500
  • Go Engineer Systematic Course 008 [Study Notes]

    Orders and Shopping Cart
    First, copy the service code framework of 'srv' from the inventory service, then find and replace the corresponding name (order_srv).

    Fundamentals of Encryption Technology
    Symmetric Encryption
    Principle:
    Uses the same key for encryption and decryption.
    Like a single key that can both lock and unlock a door.
    Fast encryption speed, suitable for large data transfers.
    Use cases:
    Local file encryption
    Database content encryption
    Content encryption during large data transfers
    Fast communication between internal systems...

    Personal Nov 25, 2025
    26500
  • In-depth Understanding of ES6 006 [Study Notes]

    Symbol and Symbol properties The 6th primitive data type: Symbol. Private names were originally designed to allow developers to create non-string property names, but general techniques cannot detect the private names of these properties. Creating a Symbol let firstName = Symbol(); let person = {} person[firstName] = "Nicholas"; cons…

    Personal Mar 8, 2025
    1.3K00
  • Deep Dive into ES6 010 [Study Notes]

    Improved array functionality. The peculiar behavior of `new Array()`: when a single numeric value is passed to the constructor, the array's `length` property is set to that value; if multiple values are passed, regardless of whether they are numeric or not, they all become elements of the array. This behavior is confusing, as it's not always possible to pay attention to the type of data passed in, thus posing a certain risk. `Array.of()`, regardless of how many arguments are passed, has no special case for a single numeric value (one argument and numeric type); it always returns an array containing all arguments...

    Personal Mar 8, 2025
    1.3K00
  • TS Mount Everest 002 [Study Notes]

    Generics /* * @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git &a…

    Personal Mar 27, 2025
    1.6K00
EN
简体中文 繁體中文 English