强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 11 章 · HTTP 服务与客户端

第 11 章 · HTTP 服务与客户端

11.1 创建 HTTP 服务器

const http = require('http');

const server = http.createServer((req, res) => {
  console.log(`${req.method} ${req.url}`);
  
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Hello, World!\n');
});

server.listen(3000, () => {
  console.log('服务器运行在 http://localhost:3000/');
});

请求对象(req)

const server = http.createServer((req, res) => {
  // 基本信息
  console.log('方法:', req.method);          // GET, POST, PUT, DELETE
  console.log('URL:', req.url);              // /api/users?page=1
  console.log('HTTP 版本:', req.httpVersion); // 1.1
  console.log('头部:', req.headers);
  console.log('远程地址:', req.socket.remoteAddress);
  
  // 解析 URL
  const url = new URL(req.url, `http://${req.headers.host}`);
  console.log('路径:', url.pathname);        // /api/users
  console.log('查询参数:', Object.fromEntries(url.searchParams));
  // { page: '1' }

  res.end('OK');
});

响应对象(res)

const server = http.createServer((req, res) => {
  // 设置状态码和头部
  res.writeHead(200, {
    'Content-Type': 'application/json; charset=utf-8',
    'X-Custom-Header': 'my-value',
  });
  
  // 发送 JSON
  res.end(JSON.stringify({ message: '成功' }));
  
  // 或者分步写入
  // res.writeHead(200, { 'Content-Type': 'text/plain' });
  // res.write('第一部分');
  // res.write('第二部分');
  // res.end('最后一部分');
});

11.2 路由实现

const http = require('http');

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  const method = req.method;
  const path = url.pathname;

  // JSON 响应辅助函数
  function json(data, status = 200) {
    res.writeHead(status, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(data));
  }

  // 读取请求体
  async function readBody() {
    const chunks = [];
    for await (const chunk of req) {
      chunks.push(chunk);
    }
    return JSON.parse(Buffer.concat(chunks).toString());
  }

  // 路由匹配
  try {
    if (method === 'GET' && path === '/') {
      json({ message: '首页' });
    } else if (method === 'GET' && path === '/api/users') {
      json({ users: [{ id: 1, name: 'Alice' }] });
    } else if (method === 'POST' && path === '/api/users') {
      const body = await readBody();
      json({ created: body }, 201);
    } else if (method === 'GET' && path.match(/^\/api\/users\/(\d+)$/)) {
      const id = path.match(/^\/api\/users\/(\d+)$/)[1];
      json({ id, name: `User ${id}` });
    } else {
      json({ error: 'Not Found' }, 404);
    }
  } catch (err) {
    json({ error: err.message }, 500);
  }
});

11.3 中间件模式

const http = require('http');

// 中间件管理器
function createApp() {
  const middlewares = [];

  function use(fn) {
    middlewares.push(fn);
  }

  function handleRequest(req, res) {
    let index = 0;

    // JSON 响应辅助
    res.json = (data, status = 200) => {
      res.writeHead(status, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(data));
    };

    function next() {
      if (index < middlewares.length) {
        const middleware = middlewares[index++];
        middleware(req, res, next);
      }
    }

    next();
  }

  return { use, handleRequest };
}

// 使用中间件
const app = createApp();

// 日志中间件
app.use((req, res, next) => {
  const start = Date.now();
  console.log(`→ ${req.method} ${req.url}`);
  res.on('finish', () => {
    console.log(`← ${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`);
  });
  next();
});

// CORS 中间件
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  if (req.method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }
  next();
});

// 路由中间件
app.use((req, res, next) => {
  if (req.method === 'GET' && req.url === '/api/hello') {
    res.json({ message: 'Hello!' });
  } else {
    next();
  }
});

// 404 处理
app.use((req, res) => {
  res.json({ error: 'Not Found' }, 404);
});

const server = http.createServer(app.handleRequest);
server.listen(3000);

11.4 HTTP 客户端

内置 http/https

const http = require('http');
const https = require('https');

// GET 请求
function httpGet(url) {
  return new Promise((resolve, reject) => {
    const client = url.startsWith('https') ? https : http;
    client.get(url, (res) => {
      const chunks = [];
      res.on('data', (chunk) => chunks.push(chunk));
      res.on('end', () => {
        resolve({
          status: res.statusCode,
          headers: res.headers,
          body: Buffer.concat(chunks).toString(),
        });
      });
    }).on('error', reject);
  });
}

fetch API(Node.js 18+)

// fetch 是全局可用的
async function demo() {
  // GET 请求
  const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
  const data = await res.json();
  console.log(data);

  // POST 请求
  const createRes = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title: '标题', body: '内容', userId: 1 }),
  });
  const created = await createRes.json();
  console.log(created);

  // 超时控制
  const controller = new AbortController();
  setTimeout(() => controller.abort(), 5000);

  try {
    const res = await fetch('https://slow-api.com', {
      signal: controller.signal,
    });
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('请求超时');
    }
  }
}

11.5 静态文件服务器

const http = require('http');
const fs = require('fs');
const path = require('path');

const MIME_TYPES = {
  '.html': 'text/html',
  '.css': 'text/css',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
};

function createStaticServer(rootDir) {
  return http.createServer((req, res) => {
    let filePath = path.join(rootDir, req.url === '/' ? 'index.html' : req.url);
    filePath = path.normalize(filePath);

    // 防止目录遍历攻击
    if (!filePath.startsWith(rootDir)) {
      res.writeHead(403);
      res.end('Forbidden');
      return;
    }

    const ext = path.extname(filePath);
    const contentType = MIME_TYPES[ext] || 'application/octet-stream';

    fs.readFile(filePath, (err, data) => {
      if (err) {
        if (err.code === 'ENOENT') {
          res.writeHead(404, { 'Content-Type': 'text/plain' });
          res.end('Not Found');
        } else {
          res.writeHead(500);
          res.end('Internal Server Error');
        }
        return;
      }
      res.writeHead(200, { 'Content-Type': contentType });
      res.end(data);
    });
  });
}

createStaticServer('./public').listen(3000);

11.6 HTTP/2 支持

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem'),
});

server.on('stream', (stream, headers) => {
  const path = headers[':path'];
  const method = headers[':method'];

  stream.respond({ ':status': 200, 'content-type': 'application/json' });
  stream.end(JSON.stringify({ message: 'HTTP/2 响应', path }));
});

server.listen(8443);

注意事项

⚠️ 始终处理错误事件reqres 都需要监听 error 事件。

⚠️ 请求体读取:Node.js 不会自动解析请求体,需要手动读取流。

⚠️ 目录遍历防护:静态文件服务器必须验证路径,防止 ../../etc/passwd 类型的攻击。

⚠️ 连接超时:默认超时是 5 分钟(server.timeout),生产环境应适当调小。

业务场景

  1. API 网关:使用内置 http 模块构建轻量级 API 网关
  2. 文件服务器:构建内部静态资源服务器
  3. 代理服务器:转发请求到后端微服务
  4. 健康检查端点:为容器编排提供健康检查接口

扩展阅读


上一章第 10 章 · 文件系统 下一章第 12 章 · Express 框架 — 路由、中间件、模板引擎。