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: websocketheader; - 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/Payloadformat. - 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 aschat-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 withsocket.join(room)/socket.leave(room)for managing subscriptions.socket.compress(false).emitetc.: 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 likeevent,data,id, separated by\n\n. - Browsers natively support automatic reconnection and
Last-Event-IDfor 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 theh2protocol in the DevTools network panel, and the response will continuously append data. - CLI:
curl --http2 -k https://localhost:8443/time, where-kskips 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.nextTickexecutes preferentially at the end of each phase, followed closely by Promise microtasks; therefore, judicious use ofnextTick/queueMicrotaskcan 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,fetchcallbacks; 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 dedicatedprocess.nextTickqueue. - API Differences: Browsers do not have
setImmediate,process.nextTick, but use methods likeMessageChannel,requestAnimationFrame,postMessageto influence scheduling. Node'ssetImmediateis tied to the check phase, andnextTickis used to interrupt the current phase. The samesetTimeout(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/clustercan 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 ofnextTickmight 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, usesetImmediatebefore or after emit.removeListener/offandremoveAllListenersare 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 callbacksphase 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.gzipdirectly in the request thread; they can be placed inworker_threadsor external services. - For large numbers of file operations, use streaming APIs, or adjust
UV_THREADPOOL_SIZEcombined 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, setwaitForConnectionsto 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-clito maintain migrations, avoiding uncontrolled changes fromsync({ 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
maxPoolSizeto 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 useZADD/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 streamedstdout/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 forcluster.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_threadsis 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/errorfor 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 vianext(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