我們有了以下知識
終於可以進到 Node.js http 模組了!
本文測試、引用的 Node.js 原始碼為 v24.x
http.Agent ?如果沒有 http.Agent 的話
import http from "http";
// server
const server = http.createServer((req, res) => {
console.log("req.headers", req.headers);
res.end();
});
server.listen(5000);
// client
const noop = () => {};
const request = http.request({
host: "localhost",
port: 5000,
path: "/",
agent: false, // ✅ 不使用 `http.Agent`
});
request.end();
request.on("response", (res) => {
console.log("res.headers", res.headers);
res.resume();
res.on("end", noop);
});
從 log 的 Connection: close 可以得出結論:每次 HTTP round trip 結束,都會關閉 TCP 連線
req.headers { host: 'localhost:5000', connection: 'close' }
res.headers {
date: 'Fri, 06 Feb 2026 01:33:20 GMT',
connection: 'close',
'content-length': '0'
}
從 TCP 的視角來看,每次都需要
"三次交握開啟連線" + "四次交握關閉連線",效能上會比較差

http.Agent 為此而生,它幫使用者管理
new http.Agent(options)https://nodejs.org/docs/latest-v24.x/api/http.html#new-agentoptions
| option | description |
|---|---|
| keepAlive | Keep sockets around even when there are no outstanding requests, so they can be used for future requests without having to reestablish a TCP connection. |
| keepAliveMsecs | 同 net.createServer 的 keepAliveInitialDelay |
| agentKeepAliveTimeoutBuffer | 假設 server 設定 keep-alive: timeout=3,http.Agent 設定 agentKeepAliveTimeoutBuffer = 1000,那 http.Agent 會在 3000 - 1000 = 2 秒後,將這個連線視為過期,為了避免 client 還想傳送資料,但 server 已經要關閉這條連線 |
| maxSockets | 每個 origin 最多可以有幾個 concurrent TCP socket,參考 options.maxSockets 圖解,(origin 是 agent.getName([options]) 的回傳值) |
| maxTotalSockets | 最多可以有幾個 concurrent TCP socket |
| maxFreeSockets | Only works when keepAlive = true |
| scheduling | 要如何從 freeSockets 陣列中選擇 - fifo (First In First Out) - lifo (Last In First Out) |
| timeout | 同 socket.timeout |
| proxyEnv | v24.5.0 加入的,目前還在 Stability: 1.1 - Active development,細節在未來(9/1 的文章)介紹 |
| defaultPort | Default port to use when the port is not specified in requests |
| protocol | The protocol to use for the agent |
| method | description |
|---|---|
| createConnection(options[, callback]) | 同 net.createConnection(),❌ 正常不會碰到,有客製化行為才需要 override |
| keepSocketAlive(socket) | ❌ 正常不會碰到,有客製化行為才需要 override |
| reuseSocket(socket, request) | ❌ 正常不會碰到,有客製化行為才需要 override |
| destroy() | 銷毀整個 http.Agent |
| getName([options]) | ❌ 正常不會碰到,用來當作連線池的 group key,詳細請參考 Read-Only properties |
這三個是在 new http.Agent(options) 設定的,故不多贅述
這三個則是由 http.Agent 控制的
{
'example.com:80:': [Socket, Socket],
'www.google.com:80:': [Socket, Socket]
}
{
'example.com:80:': [ClientRequest, ClientRequest],
'www.google.com:80:': [ClientRequest, ClientRequest]
}
http.Agent 使用中的 sockets{
'example.com:80:': [Socket, Socket],
'www.google.com:80:': [Socket, Socket]
}
這邊的 example.com:80: 跟 www.google.com:80: 就是 agent.getName([options]) 回傳的 group key
options.maxSockets 圖解ClientRequest 跟 net.Socket 連結的橋樑當你用 http.request 發起請求時,背後會優先從 http.Agent 的連線池(freeSockets)挑選一個已連線的 net.Socket 關聯到這個 ClientRequest。若 freeSockets 為空,就會建立一個新的 TCP 連線
詳細的實作可以看 lib/_http_agent.js 的 Agent.prototype.addRequest
我們寫個 PoC 來測試
import http from "http";
import assert from "assert";
import { nextTick } from "process";
// HTTP server
const httpServer = http.createServer((req, res) => res.end());
httpServer.listen(5000);
// HTTP client
const agent = new http.Agent({ keepAlive: true });
// ✅ 剛開始沒有建立任何 TCP 連線
assert(Object.keys(agent.freeSockets).length === 0);
const clientRequest1 = http.request({ host: "localhost", port: 5000, agent });
clientRequest1.on("socket", () => console.log(clientRequest1.reusedSocket)); // ❌ false
clientRequest1.end();
clientRequest1.on("close", () =>
nextTick(() => {
// ✅ 使用 nextTick,確保 localhost:5000 的 TCP socket 已經回收到 freeSockets
assert(Object.keys(agent.freeSockets).length === 1);
const clientRequest2 = http.request({
host: "localhost",
port: 5000,
agent,
});
clientRequest2.on("socket", () => console.log(clientRequest2.reusedSocket)); // ✅ true
clientRequest2.end();
}),
);
ClientRequest 跟 net.Socket 關聯的瞬間觸發net.Socket 是否關聯過其他 ClientRequest
Without http.Agent (agent: false) |
With http.Agent (keepAlive: true) |
|
|---|---|---|
| Connection | New TCP connection per request | Reuse socket from freeSockets |
| Header | Connection: close |
Connection: keep-alive |
| Handshake | 3-way + 4-way handshake x N | 3-way handshake x 1, then reuse |
| Property | State | Description |
|---|---|---|
sockets |
In use | Socket currently handling a request |
freeSockets |
Idle | Kept alive for reuse (keepAlive: true) |
requests |
Queued | Waiting for a free socket (over maxSockets / maxTotalSockets) |