WebSocket 與 SSE 總覽
WebSocket 基礎
- 定位:WebSocket 是一條在 HTTP 握手後升級的全雙工連接,允許客戶端與服務器在同一 TCP 通道上雙向推送數據,省去了反覆輪詢。
- 握手流程:
- 客戶端通過
Upgrade: websocket頭髮起 HTTP 請求; - 服務器響應
101 Switching Protocols,雙方協商子協議、壓縮等擴展; - 後續通信轉爲 WebSocket 幀,遵循
FIN/Opcode/Payload格式。 - 特點:狀態常駐、頭部極小、支持二進制 / 文本幀、可以按房間劃分廣播。
- 適用場景:IM/ 聊天室、協同編輯、實時儀表盤、在線遊戲等需要低延遲雙向通信的業務。
Node.js + Socket.IO 示例
socket.io 封裝了握手、心跳、自動重連、回退輪詢等細節,通過自帶協議 + 服務端組件 (socket.io) 與客戶端 (socket.io-client) 協作。
目錄與依賴初始化
mkdir websocket-demo && cd websocket-demo
npm init -y
npm install socket.io express
服務端示例
// 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, // 心跳頻率
});
io.on('connection', (socket) => {console.log('client connected:', socket.id);
// 自定義事件:客戶端發來 chat-message
socket.on('chat-message', (payload) => {
// 發給當前客戶端的回執
socket.emit('chat-ack', { id: payload.id, status: 'OK'});
// 廣播給所有其他客戶端
socket.broadcast.emit('chat-message', {
...payload,
from: socket.id,
});
});
// 服務器主動廣播
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.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>
自定義事件與廣播策略
socket.on('<event>', handler):監聽自定義事件,如chat-message、typing。socket.emit:只發給當前連接(常用於回執、私聊)。socket.broadcast.emit:發給除自己外的所有客戶端,適合房間外的全局廣播。io.emit:發送給所有連接,包括自己,適合系統消息。io.to(room).emit:房間級廣播,結合socket.join(room)/socket.leave(room)管理訂閱。socket.compress(false).emit等:對大流量廣播可控制壓縮、Acks 等參數以滿足性能需求。
SSE(Server-Sent Events)
概念與特點
- 基於 HTTP/1.1 持久連接,由服務器單向推送文本事件到瀏覽器
EventSource。 - 事件格式是
text/event-stream,每個事件包含event、data、id等字段,以\n\n分隔。 - 瀏覽器原生支持自動重連、
Last-Event-ID斷點續傳,適合通知、行情、日誌流式輸出。 - SSE 是單向的(服務端 -> 客戶端),若需要客戶端上行消息仍要配合 POST/AJAX;而 WebSocket 是全雙工。
Node 服務端示例
// 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'));
瀏覽器客戶端示例
<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>
與 WebSocket 對比
- 通信方向:SSE 只有服務端 -> 客戶端;WebSocket 支持雙向。
- 協議 / 兼容:SSE 依賴 HTTP,受代理與防火牆更友好;WebSocket 需
Upgrade,對舊環境可能有限制。 - 吞吐 & 二進制:SSE 僅傳文本,適合中低頻推送;WebSocket 帶二進制幀,對高頻 / 大數據更合適。
- 心跳:SSE 內置重連機制;WebSocket 需應用層心跳(Socket.IO 已封裝)。
在實際項目中,可按場景選擇:例如後臺配置、行情推送可用 SSE;需要實時協作、IM、控制面板等則優先 WebSocket/Socket.IO。
HTTP/2 與長連接
核心特性
- 繼承 HTTP/1.1 的持久連接模型,通過 多路複用 在一條 TCP 長連接上併發多條 Stream,避免隊頭阻塞。
- 採用二進制幀層(Frame Layer),將請求 / 響應拆成 HEADERS、DATA 等幀以幀 ID 區分流。
- HPACK 頭部壓縮 + 靜態表顯著降低重複 header 的傳輸開銷。
- 支持服務器主動推送(Server Push,現今瀏覽器已逐步棄用,但在特定客戶端仍可用),也可通過長時間保持 DATA 幀實現流式返回。
Node.js http2 Demo
mkdir http2-demo && cd http2-demo
npm init -y
創建 server.mjs:
import fs from 'node:fs';
import http2 from 'node:http2';
// 瀏覽器訪問需 https,可以用 mkcert/openssl 生成本地證書;示例使用自簽證書
const server = http2.createSecureServer({key: fs.readFileSync('./localhost-key.pem'),
cert: fs.readFileSync('./localhost-cert.pem'),
allowHTTP1: true, // 兼容老客戶端
});
server.on('stream', (stream, headers) => {const path = headers[':path'];
if (path === '/time') {
// 以 DATA 幀流式推送,實現長連接下的實時更新
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');
});
客戶端驗證
- 瀏覽器:使用
fetch('https://localhost:8443/time')需信任自簽證書,可在 DevTools 網絡面板確認h2協議,響應會持續追加數據。 - CLI:
curl --http2 -k https://localhost:8443/time,-k跳過證書校驗,可看到每 2 秒一行數據。
也可寫一個 Node 客戶端:
// 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();
與 WebSocket/SSE 的取捨
- HTTP/2 長連接仍遵循請求 - 響應語義,客戶端先發 HEADERS 後才能收到 DATA;WebSocket 連接一旦升級後就不再有請求概念。
- 對純瀏覽器推送場景,若已升級 HTTP/2,可用長響應或 SSE 代替額外的 WS 鏈路,減少心跳管理;但 HTTP/2 仍不支持客戶端主動上行而不發請求。
- 若需要雙向通信或跨 TCP 針對房間廣播,WebSocket 更直接;若是單向通知且基礎設施已支持 HTTP/2,則利用其多路複用 / 壓縮可以讓接口更加高效。
nodejs 的事件模型
事件循環概念
- Node 架構:單線程 JS 執行線程 + 背景中的 libuv 線程池。所有 I/O 任務在內核或線程池中完成後,通過事件循環(Event Loop)調度回調進入 JS 主線程。
- 事件循環階段(每一輪 tick 都按順序執行):
- timers:到期的
setTimeout/setInterval; - pending callbacks:部分系統級回調,如 TCP errors;
- idle/prepare:內部使用;
- poll:獲取新的 I/O 事件,並執行相關回調。若爲空會視情況進入下一階段或阻塞等待;
- check:專門執行
setImmediate; - close callbacks:例如
socket.on('close', ...)。 - 微任務:
process.nextTick在每個階段結束都會優先執行,Promise microtask 緊隨其後;因此合理使用nextTick/queueMicrotask可以調整回調執行順序。
與瀏覽器事件循環的差異
- 階段模型不同:瀏覽器遵循 HTML 標準的 Task/Microtask 隊列(macro task 如
setTimeout、DOM 事件、fetch回調等;micro task 如 Promise、MutationObserver),每次執行完一個宏任務就清空所有微任務。Node 由 libuv 驅動,擁有 timers/poll/check 等 6 個階段,每個階段結束後才輪詢 microtask,並在process.nextTick專隊列後才執行 Promise 微任務。 - API 差異:瀏覽器沒有
setImmediate、process.nextTick,而是以MessageChannel、requestAnimationFrame、postMessage等手段來影響調度;Node 的setImmediate綁定 check 階段,nextTick用於打斷當前階段。相同的setTimeout(fn, 0)在瀏覽器最早也會在 4ms(>=5 次嵌套)後執行,而 Node 在 timers 階段只要達到 0ms 就會調度。 - 渲染與 UI 約束:瀏覽器事件循環與渲染管線耦合——
requestAnimationFrame、layout/paint 會在宏任務之間執行,若 JS 長時間佔用主線程會阻塞頁面渲染。Node 不存在渲染階段,CPU 密集邏輯只會阻塞 I/O 回調進入主線程。 - 多 event loop:瀏覽器中每個 window/frame/worker 擁有獨立事件循環,彼此通過 postMessage 交互;Node 只有一個主事件循環,若需要並行可藉助
worker_threads/cluster來創建額外線程 / 進程。 - 微任務優先級:瀏覽器 microtask(Promise 等)在宏任務完成後統一執行;Node 則是在
process.nextTick→ Promise microtask → 下一階段的順序下執行,因此過度使用nextTick可能讓 I/O 飢餓,而瀏覽器則主要擔心 microtask 長時間未清空導致渲染延遲。
事件循環示例
// 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');
可能的輸出(取決於 libuv poll 階段狀態):
sync
nextTick // 微任務最先執行
timeout / immediate(順序非固定)I/O callback
immediate in I/O // I/O 回調階段後立即執行 check 階段
timeout in I/O
通過這個腳本可以觀察 timers/poll/check 在不同上下文下的調度次序。
EventEmitter 與自定義事件
Node 內部大量 API 基於 EventEmitter(http.Server、net.Socket 等)。我們也可以通過繼承或直接實例化來發送 / 監聽自定義事件。
// custom-event.js
const EventEmitter = require('events');
class TaskBus extends EventEmitter {addTask(task) {
// 模擬異步執行完成後觸發自定義事件
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'});
要點:
on/addListener:添加長期監聽;once:一次性監聽並在觸發後自動移除。emit(event, ...args):同步觸發監聽器,按註冊順序執行。若希望異步觸發可在 emit 前後使用setImmediate.removeListener/off與removeAllListeners用於釋放資源,避免泄漏。
事件驅動模式有利於解耦生產者 / 消費者:I/O 完成、業務狀態變化等都封裝爲事件。結合事件循環的調度,Node 既能保持單線程的編程模型,又能充分利用異步 I/O 的高併發能力。
node 底層異步 IO 與線程池
libuv 模型
- Node 的異步 IO 能力來自 C 層的 libuv:它向操作系統請求異步文件 /TCP/DNS 等操作,並通過事件循環在完成後觸發 JS 回調。
- 對於 真正異步的內核調用(多數 socket、epoll/kqueue 支持的文件描述符),libuv 只需註冊事件並在可讀 / 可寫時向上通知,不佔用線程池。
- 對於無法提供非阻塞接口的操作(如部分文件系統調用、DNS 解析、壓縮 / 加密等 CPU 密集任務),libuv 會將任務派發到一個固定大小的線程池中執行,完成後再把結果投遞迴事件循環。
線程池細節
- 默認大小爲 4,可通過環境變量
UV_THREADPOOL_SIZE(最多 128)調整,例如UV_THREADPOOL_SIZE=8 node server.js。 - 使用線程池的 Node API 包括:
fs.readFile/writeFile、crypto.pbkdf2、zlib、dns.lookup(默認)、fs.stat等。 - 線程池中的任務一旦被佔滿,後續同類操作會排隊等待;因此高併發文件 IO 場景建議將同步讀寫改爲流式
fs.createReadStream/createWriteStream,或適當增大池規模。
示例:對比 IO 與 CPU 密集任務
// 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);
});
}
運行後你會發現 pbkdf2 的回調最多同時處理 4 個任務,因爲線程池默認僅提供 4 條工作線程;而 fs.readFile 也會佔用同一個池。若對 CPU 密集任務並行度有更高需求,可以在啓動時調大 UV_THREADPOOL_SIZE,但要注意機器核數和上下文切換成本。
與事件循環的協作
- 線程池執行完任務後,會將完成事件放入
pending callbacks階段隊列;事件循環會在下一輪處理它們,從而調用 JS 回調。 - 若 JS 層長時間處理 CPU 邏輯,事件循環無法盡快回到 poll 階段,線程池排隊的結果也無法被消費,這就是單線程阻塞問題。解決方案包括:拆分任務、使用
worker_threads、或遷移到專用進程。
實踐建議
- 避免在請求線程中直接執行
crypto.pbkdf2、zlib.gzip等重 CPU 操作,可放到worker_threads或外部服務。 - 大量文件操作時使用流式 API,或調節
UV_THREADPOOL_SIZE並結合批量 / 限流控制。 - 對網絡 IO,儘量使用真正異步的 socket/HTTP 客戶端,藉助 libuv epoll/kqueue 能力可以在單線程下同時管理數萬連接。
總體來說,Node 的“異步 IO + 線程池”模式將 CPU 與 IO 工作拆分:IO 事件由內核通知、CPU 耗時由線程池承擔,JS 主線程只負責驅動狀態機和業務邏輯,從而達到高併發、高吞吐的效果。
Node 連接 MySQL / MongoDB / Redis
MySQL:事務型業務
適用於電商訂單、庫存等強一致場景。推薦 mysql2,支持 Promise 與連接池。
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();
}
}
實戰提示:
- 所有參數使用
?佔位 + 綁定數組,防 SQL 注入; - 把長連接集中託管在
pool中,設置waitForConnections避免連接數耗盡; - 對報表、只讀接口可配置從庫連接,實現主寫從讀;
- 結合
pool.on('connection', conn => conn.query('SET SESSION sql_mode=...'))做統一設置。
ORM 示例:Sequelize
在中大型項目中可使用 ORM 來抽象模型、關係與遷移。
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}); // 生產環境建議用 migration
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 要點:
- 支持模型鉤子、驗證、作用域、樂觀鎖 (
version) 等高級特性; - 生產環境使用
sequelize-cli維護 migration,避免sync({alter: true})在多節點環境下帶來不可控變更; - 當 SQL 需要優化時可使用
sequelize.query()下發原生語句,同時保留模型定義。
ORM 示例:Prisma
Prisma 使用 Schema 文件定義模型,並生成類型安全的客戶端,適合 TypeScript 項目。
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())
}
執行 npx prisma migrate dev --name init 生成表,然後:
// 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 會根據 Schema 自動生成類型定義,支持中間件(如審計日誌)、連接池複用、query event hook 等,能在保持 SQL 控制的同時保證開發效率。
MongoDB:文檔型場景
內容管理、社交 Feed、日誌或靈活 Schema 使用 MongoDB 更方便。可用官方驅動或 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();}
要點:
- 使用
maxPoolSize限制連接數量,搭配連接監控; - 查詢鏈路最後調用
lean(),減少 Mongoose 的 getter/setter 開銷; - 對高頻字段建立複合索引(如 tag + createdAt),避免全表掃描;
- 可以利用副本集多節點讀 (
readPreference=secondaryPreferred) 用於報表或實時推薦。
Redis:緩存 + 消息
Redis 擅長緩存、會話、計數器、Pub/Sub。ioredis 支持 Cluster/Sentinel、自動重連。
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;
}
// 分佈式鎖:防止重複下單
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:訂單狀態通知
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'}));
實踐建議:
- 緩存必須設置 TTL,配合隨機過期(
EX + Math.random())降低雪崩; - 需要持久化的數據配合 AOF/RDB 機制,或使用 Redis Cluster 提高可用性;
- Pub/Sub 只保證盡力而爲,若需可靠消費可用 Redis Streams (
XADD/XREADGROUP); - 全局自增 ID 可用
INCR,排行榜使用ZADD/ZREVRANGE。
通過合理地將 MySQL 的強事務、MongoDB 的靈活文檔與 Redis 的高速緩存結合,Node 應用可以 3 層分工:寫入鏈路首先落盤 MySQL/MongoDB,讀取優先命中 Redis,錯失再回源;實時事件通過 Redis Pub/Sub 或 Stream 推送到 WebSocket/SSE,與上文實時通信部分協同,構建端到端的高併發服務。
Node 主進程、子進程與 Cluster
child_process 基礎
Node 默認單線程執行 JS,如需利用多核或調用系統命令,可使用 child_process 模塊創建子進程:
spawn(command, args, options):最常用,返回流式stdout/stderr,適合長期任務;exec(command, options, callback):將輸出緩存在內存,適合短命令;fork(modulePath, args, options):專門用來運行 Node 子腳本,自動建立主子進程 IPC 通道。
// 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));
fork 創建的管道是基於 IPC 的,process.send 和 child.on('message') 傳遞的對象會自動序列化。在 spawn/exec 場景也可以通過 child.stdin.write、child.stdout.on('data') 來實現流式通信。
共享端口與 Cluster
單個 Node 進程無法使用多核。cluster 模塊封裝了 child_process.fork,允許多個 Worker 共享同一個服務器端口;Master 負責監聽端口並將連接分發給 Worker(在 Linux 上通過 SO_REUSEPORT 或內部 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(); // 自動重啓});
} else {
http
.createServer((req, res) => {res.end(`Handled by worker ${process.pid}\n`);
})
.listen(3000);
}
主進程中可以監聽 cluster.on('online')、cluster.on('message') 等事件,Worker 也可通過 process.send 與主進程交互,例如上報負載、健康狀態。
Cluster 與子進程通信
- Master -> Worker:
worker.send(payload); - Worker -> Master:
process.send(payload);Master 監聽cluster.on('message', (worker, msg) => {}); - Worker 間通信需要由 Master 做中轉,或使用 Redis/Message Queue;
- 對於 CPU 密集型任務,
worker_threads是更輕量的選擇,且共享內存;Cluster 側重端口共享 + 進程隔離。
實踐建議
- 子進程默認與主進程同生命週期,確保監聽
exit/error以便重啓或降級; - IPC 消息會序列化,避免傳輸巨大對象,可使用共享內存文件或 Socket;
- Cluster 模式下的 Worker 是獨立進程,狀態(緩存、連接)不共享,需在外部存儲會話(如 Redis);
- 在容器環境中可考慮使用 PM2、forever 等進程管理工具封裝 cluster,附帶日誌、重啓策略和健康檢查。
通過 child_process + cluster,Node 可以在保持單線程模型的前提下複用多核、隔離崩潰,並與系統命令或其他 Node 子腳本協作,構建更可靠的服務。
Express 與 Koa 項目搭建
Express:經典 MVC 框架
Express 是 Node 上使用最廣泛的 Web 框架之一,提供路由、中間件機制,可以方便地組織 MVC/REST 項目。
初始化步驟
mkdir express-demo && cd express-demo
npm init -y
npm install express dotenv morgan
目錄結構示例:
express-demo/
├─ app.js # 創建 express 實例
├─ routes/
│ ├─ index.js
│ └─ users.js
├─ controllers/
│ └─ user.controller.js
├─ models/
│ └─ user.model.js # 可使用 Sequelize/Prisma 等 ORM
└─ 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);
}
};
Express 模型層通常由 ORM/ODM 提供(Sequelize/Prisma/Mongoose 等)。Express 引入中間件棧,因此可以輕鬆插入鑑權、日誌、限流等邏輯,適合 REST API 和 SSR 應用。
Koa:更輕量的中間件組合
Koa 由 Express 團隊打造,使用 async/await + 洋蔥模型中間件,核心更輕,適合按需組合。
初始化步驟
mkdir koa-demo && cd koa-demo
npm init -y
npm install koa koa-router koa-body koa-logger dotenv
目錄結構示例:
koa-demo/
├─ app.js
├─ routes/
│ └─ user.route.js
├─ controllers/
│ └─ user.controller.js
├─ services/
│ └─ user.service.js
└─ models/ # 可放 ORM 定義
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 示例:
const userService = require('../services/user.service');
exports.list = async (ctx, next) => {const users = await userService.findAll();
ctx.body = users;
};
Koa 推薦的“模型”組織方式通常是 Service/Repository 層來封裝業務邏輯,結合 async/await 洋蔥模型,可以對請求前後做流式處理(如響應壓縮、錯誤捕獲)。配合 koa-compose、koa-jwt 等中間件,可快速搭建 GraphQL、REST、或 BFF 服務。
洋蔥皮模型原理
Koa 的中間件本質是一個 async 函數組成的數組(middleware stack),利用 await next() 將執行權傳給下箇中間件,再在 next 返回後繼續向外執行,形成“先遞進、後回溯”的洋蔥模型。koa-compose 的核心實現如下(簡化版):
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)));
}
};
}
示例:
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';
});
輸出順序:A before -> B before -> handler -> B after -> A after。因此,想要在請求完成後做收尾工作(日誌、事務、響應包裝),只需把邏輯寫在 await next() 之後即可;想在進入下層之前預處理(鑑權、解析)則寫在 await next() 之前。該模型通過 Promise/async 遞歸即可實現,無需複雜的狀態機。
Express vs Koa
- 中間件機制:Express 基於 callback 棧;Koa 基於 async/await 洋蔥模型,中間件可以
await next()後在返回時再處理。 - 生態:Express 中間件生態龐大;Koa 核心輕量,但需要手動選擇中間件搭建功能。
- 默認功能:Express 自帶大量便捷方法(
res.json、express.static);Koa 只提供核心,需要額外引入包。 - 錯誤處理:Koa 的 try/catch 搭配
app.on('error')能捕獲 async 中的異常;Express 通過next(err)傳遞到錯誤處理中間件。
無論是 Express 還是 Koa,都可以將前述數據庫層(MySQL/MongoDB/Redis)和實時能力(WebSocket/SSE)整合進來,按照路由 -> 控制器 -> 服務 -> 模型的分層設計,構建高可維護的 Node Web 項目。