跳至主要內容
Skip to content

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` 模組,再學習框架。這樣遇到框架問題時,你知道底層發生了什麼。


進階挑戰

  1. 僅使用原生 http 模組,實作一個支援 GET/POST 的簡易 REST API。
  2. 實作一個簡易的靜態檔案伺服器,能正確處理 MIME 類型。
  3. 使用 Cluster 模式,比較單核心和多核心的效能差異。

延伸閱讀與資源