Node.js HTTP 练习答案(三):34~42

导航:题目 · 1~18 · 19~33 · 43~50

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 到达时累计该文件及请求总字节数,不需要等整文件落盘。

推荐“任一失败则整批失败”作为普通表单的默认事务语义:

  1. 所有文件先写入每请求唯一临时目录。
  2. 任一文件超限,停止该文件并中止/排空请求。
  3. 删除本请求已经写入的所有临时文件。
  4. 全部验证成功后再原子发布并写数据库。

部分成功适合明确的批处理 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");
}