Node.js http 模組原理:從零理解伺服器
在使用 Express、Koa 之前,讓我們先回到最基礎:Node.js 原生的 http 模組。理解它,才能真正掌握後端框架的運作原理。
一、 最簡單的 HTTP 伺服器
1.1 Hello World
javascript
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World");
});
server.listen(3000, () => {
console.log("Server running at http://localhost:3000/");
});這短短幾行程式碼做了什麼?
1.2 拆解 createServer
javascript
// createServer 返回一個 Server 實例
const server = http.createServer();
// request 事件:每當收到請求時觸發
server.on("request", (req, res) => {
// req = IncomingMessage 物件(請求)
// res = ServerResponse 物件(回應)
res.end("Hello");
});
server.listen(3000);二、 請求物件(IncomingMessage)
2.1 常用屬性
javascript
server.on("request", (req, res) => {
console.log(req.method); // 'GET', 'POST', etc.
console.log(req.url); // '/path?query=value'
console.log(req.headers); // { host: 'localhost', ... }
console.log(req.httpVersion); // '1.1'
});2.2 解析 URL
javascript
const url = require("url");
server.on("request", (req, res) => {
// 舊方法
const parsed = url.parse(req.url, true);
console.log(parsed.pathname); // '/path'
console.log(parsed.query); // { query: 'value' }
// 新方法 (Node.js 10+)
const myUrl = new URL(req.url, `http://${req.headers.host}`);
console.log(myUrl.pathname); // '/path'
console.log(myUrl.searchParams.get("query")); // 'value'
});2.3 讀取 Body
req 是一個 可讀流(Readable Stream):
javascript
server.on("request", (req, res) => {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
console.log("Body:", body);
res.end("Received");
});
});使用 Promise 封裝:
javascript
function getBody(req) {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => resolve(body));
req.on("error", reject);
});
}
// 使用
const body = await getBody(req);
const data = JSON.parse(body);三、 回應物件(ServerResponse)
3.1 設定狀態碼和標頭
javascript
// 方法一:writeHead(一次設定所有)
res.writeHead(200, {
"Content-Type": "application/json",
"X-Custom-Header": "value",
});
// 方法二:分開設定
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.setHeader("X-Custom-Header", "value");3.2 發送回應
javascript
// 直接結束
res.end("Hello World");
// 分段寫入
res.write("Part 1");
res.write("Part 2");
res.end("Final part");
// JSON 回應
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true, data: [] }));3.3 串流回應
javascript
const fs = require("fs");
server.on("request", (req, res) => {
if (req.url === "/video") {
res.writeHead(200, { "Content-Type": "video/mp4" });
const stream = fs.createReadStream("./video.mp4");
stream.pipe(res);
}
});四、 實作簡易路由
4.1 基本路由
javascript
const server = http.createServer((req, res) => {
const { method, url } = req;
if (method === "GET" && url === "/") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("<h1>Home</h1>");
} else if (method === "GET" && url === "/api/users") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify([{ id: 1, name: "John" }]));
} else if (method === "POST" && url === "/api/users") {
getBody(req).then((body) => {
const user = JSON.parse(body);
res.writeHead(201, { "Content-Type": "application/json" });
res.end(JSON.stringify({ id: 2, ...user }));
});
} else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
}
});4.2 路由表模式
javascript
const routes = {
"GET /": (req, res) => {
res.end("Home");
},
"GET /api/users": (req, res) => {
res.end(JSON.stringify([]));
},
"POST /api/users": async (req, res) => {
const body = await getBody(req);
res.end(`Created: ${body}`);
},
};
const server = http.createServer((req, res) => {
const key = `${req.method} ${req.url.split("?")[0]}`;
const handler = routes[key];
if (handler) {
handler(req, res);
} else {
res.writeHead(404);
res.end("Not Found");
}
});五、 錯誤處理
5.1 請求錯誤
javascript
server.on("request", (req, res) => {
req.on("error", (err) => {
console.error("Request error:", err);
res.writeHead(400);
res.end("Bad Request");
});
res.on("error", (err) => {
console.error("Response error:", err);
});
});5.2 伺服器錯誤
javascript
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
console.error("Port already in use");
} else {
console.error("Server error:", err);
}
});5.3 未捕獲的例外
javascript
const server = http.createServer(async (req, res) => {
try {
// 處理請求
const result = await handleRequest(req);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result));
} catch (error) {
console.error(error);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Internal Server Error" }));
}
});六、 Keep-Alive 與連線管理
6.1 預設行為
Node.js 的 http 模組預設啟用 Keep-Alive:
javascript
// 設定超時
server.keepAliveTimeout = 5000; // 5 秒
server.headersTimeout = 60000; // 60 秒
server.timeout = 120000; // 120 秒6.2 監控連線
javascript
server.on("connection", (socket) => {
console.log("新連線建立");
socket.on("close", () => {
console.log("連線關閉");
});
});七、 HTTPS 伺服器
7.1 建立 HTTPS 伺服器
javascript
const https = require("https");
const fs = require("fs");
const options = {
key: fs.readFileSync("private-key.pem"),
cert: fs.readFileSync("certificate.pem"),
};
const server = https.createServer(options, (req, res) => {
res.end("Secure!");
});
server.listen(443);7.2 自簽憑證(開發用)
bash
# 生成自簽憑證
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes八、 效能優化
8.1 Cluster 模式
利用多核心 CPU:
javascript
const cluster = require("cluster");
const http = require("http");
const os = require("os");
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on("exit", (worker) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // 自動重啟
});
} else {
http
.createServer((req, res) => {
res.end(`Worker ${process.pid}`);
})
.listen(3000);
}8.2 壓縮回應
javascript
const zlib = require("zlib");
server.on("request", (req, res) => {
const acceptEncoding = req.headers["accept-encoding"] || "";
const data = JSON.stringify({ large: "data".repeat(1000) });
if (acceptEncoding.includes("gzip")) {
res.writeHead(200, {
"Content-Type": "application/json",
"Content-Encoding": "gzip",
});
zlib.gzip(data, (err, compressed) => {
res.end(compressed);
});
} else {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(data);
}
});九、 為什麼需要框架?
原生 http 模組的問題:
| 問題 | 說明 |
|---|---|
| 路由繁瑣 | 需要手動解析 URL 和方法 |
| Body 解析 | 需要手動處理流 |
| 中介軟體 | 沒有統一的處理機制 |
| 錯誤處理 | 需要到處 try-catch |
| 功能缺失 | 沒有模板、靜態檔案服務等 |
這就是 Express、Koa、Fastify 存在的原因!
總結
| 概念 | 說明 |
|---|---|
http.createServer() | 建立伺服器 |
req (IncomingMessage) | 請求物件,可讀流 |
res (ServerResponse) | 回應物件,可寫流 |
server.listen() | 開始監聽埠號 |
| Keep-Alive | 預設啟用連線復用 |
> **學習建議**:先理解原生 `http` 模組,再學習框架。這樣遇到框架問題時,你知道底層發生了什麼。
進階挑戰
- 僅使用原生
http模組,實作一個支援 GET/POST 的簡易 REST API。 - 實作一個簡易的靜態檔案伺服器,能正確處理 MIME 類型。
- 使用 Cluster 模式,比較單核心和多核心的效能差異。