Node.js HTTP 练习答案(三):34~42
34. 观察 keep-alive 复用
import * as http from "node:http";
const socketIds = new WeakMap<object, number>();
let nextSocketId = 1;
const server = http.createServer((req, res) => {
let id = socketIds.get(req.socket);
if (id === undefined) {
id = nextSocketId++;
socketIds.set(req.socket, id);
}
console.log({ url: req.url, socketId: id, remotePort: req.socket.remotePort });
res.end("socket=" + id);
});
server.listen(8080);
使用同一个 curl 进程请求多个 URL,例如 curl -v http://localhost:8080/a http://localhost:8080/b,通常可看到 Re-using existing connection。分别启动两个 curl 进程不保证复用。
35. keepAliveTimeout
server.keepAliveTimeout = 5000;
server.headersTimeout = 6000;
keepAliveTimeout 是服务器发送完一个响应后,等待同一连接下一次请求的空闲期限。WebSocket 升级后不再是普通 HTTP keep-alive;TCP keepalive 是内核探测失效对端的机制,也不是 HTTP 连接复用。headersTimeout 应合理大于 keepAliveTimeout,具体还要与 Node.js 版本默认策略和代理配置协调。
36. 路由业务总期限
class DeadlineError extends Error {}
async function withDeadline<T>(task: Promise<T>, milliseconds: number): Promise<T> {
let timer: NodeJS.Timeout | undefined;
const timeout = new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => reject(new DeadlineError("deadline exceeded")), milliseconds);
});
try { return await Promise.race([task, timeout]); }
finally { if (timer !== undefined) clearTimeout(timer); }
}
const limit = pathname === "/fast" ? 2000 : pathname === "/report" ? 30000 : undefined;
if (limit !== undefined) {
try { res.end(await withDeadline(runBusinessTask(), limit)); }
catch (error: unknown) {
if (error instanceof DeadlineError && !res.headersSent) res.writeHead(504).end("Gateway Timeout");
else throw error;
}
}
Promise.race 只停止等待,不会取消 runBusinessTask。应向数据库、fetch 或子进程传递 AbortSignal,写操作还要用事务、幂等键和明确的超时后状态。
37. 上传使用更长 socket timeout
if (req.method === "POST" && pathname === "/upload") {
req.setTimeout(60_000, () => {
req.destroy(new Error("upload idle timeout"));
});
} else {
req.setTimeout(10_000, () => {
req.destroy(new Error("request idle timeout"));
});
}
req.setTimeout 关注 socket 无活动时间;只要不断收到字节就可能一直不超时。它不是上传总耗时、Body 大小或业务处理 deadline。
38. Connection close
res.setHeader("Connection", "close");
res.end("this connection will close");
curl -v 会看到 Header 和连接关闭。它降低连接复用效率,通常只用于明确的协议/错误处理需求,不应作为默认优化。
39. 最大 5 MB 请求
const maxUploadBytes = 5 * 1024 * 1024;
async function readLimited(
req: http.IncomingMessage,
maximum: number,
): Promise<Buffer> {
const chunks: Buffer[] = [];
let total = 0;
for await (const chunk of req) {
const buffer = Buffer.from(chunk);
total += buffer.length;
if (total > maximum) throw Object.assign(new Error("too large"), { code: "BODY_TOO_LARGE" });
chunks.push(buffer);
}
return Buffer.concat(chunks, total);
}
const declared = req.headers["content-length"];
if (declared !== undefined && Number(declared) > maxUploadBytes) {
res.writeHead(413, { Connection: "close" }).end("Payload Too Large");
req.destroy(); return;
}
try {
await readLimited(req, maxUploadBytes);
res.end("uploaded");
} catch (error: unknown) {
if (error instanceof Error && "code" in error && error.code === "BODY_TOO_LARGE") {
if (!res.headersSent) res.writeHead(413, { Connection: "close" }).end();
req.destroy(); return;
}
throw error;
}
示例聚合 Buffer 只是练习;真正文件上传应把解析后的文件流写入受控临时文件,并继续限制数量、单文件、字段和总大小。
40. 多文件策略设计
Content-Length 只能表示整个 multipart 消息,而且可能缺失或虚假;它不能区分各文件、字段和边界开销。Multer/Busboy 在解析到每个 Part Header 后就能决定是否接受类型,并在文件数据 Chunk 到达时累计该文件及请求总字节数,不需要等整文件落盘。
推荐“任一失败则整批失败”作为普通表单的默认事务语义:
- 所有文件先写入每请求唯一临时目录。
- 任一文件超限,停止该文件并中止/排空请求。
- 删除本请求已经写入的所有临时文件。
- 全部验证成功后再原子发布并写数据库。
部分成功适合明确的批处理 API,但响应必须逐文件返回 success/error,成功文件不能被清理,失败文件和部分文件必须清理,还要支持幂等重试。无论哪种策略,客户端 mimetype 只能早期筛选,真实内容仍需后续检测。
41. Expect: 100-continue
const server = http.createServer(normalHandler);
server.on("checkContinue", (req, res) => {
const maximum = 5 * 1024 * 1024;
const value = req.headers["content-length"];
const length = value === undefined ? undefined : Number(value);
if (
length !== undefined
&& (!Number.isSafeInteger(length) || length < 0 || length > maximum)
) {
res.writeHead(length !== undefined && length > maximum ? 413 : 400, {
Connection: "close",
}).end();
return;
}
res.writeContinue();
void handleUpload(req, res, maximum).catch((error: unknown) => {
if (!res.headersSent) res.writeHead(500).end();
else res.destroy(error instanceof Error ? error : undefined);
});
});
客户端先发 Header 并等待 100;服务器认可后调用 writeContinue,客户端才发送 Body。没有 Content-Length 时仍必须流式计数。测试可使用 curl -v -H “Expect: 100-continue” --data-binary @large.bin URL。
42. chunked 请求体
Node 客户端不设置 Content-Length 并分次 write 时,通常使用 chunked:
const request = http.request({
hostname: "127.0.0.1",
port: 8080,
path: "/upload",
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
});
request.write(Buffer.alloc(1024));
request.write(Buffer.alloc(2048));
request.end();
服务端仍使用第 39 题 readLimited 读取解码后的 Body Chunk 并累计实际字节。Transfer-Encoding: chunked 下没有 Content-Length;Chunk 的协议边界也不是应用消息或文件边界。
async function handleUpload(
req: http.IncomingMessage,
res: http.ServerResponse,
maximum: number,
): Promise<void> {
const body = await readLimited(req, maximum);
res.end("bytes=" + body.length);
}
function normalHandler(
_req: http.IncomingMessage,
res: http.ServerResponse,
): void {
res.end("ok");
}