人人都会AI编程

原生搭建 HTTP 服务器

更新时间:2026-07-11

在 Node.js 的生态中,Express、Koa、Fastify 等框架提供了便捷的路由、中间件机制,但它们的底层依然使用的是 Node.js 内置的 http 模块。直接使用 http 模块搭建服务器,不仅能帮助我们理解框架的本质,在编写轻量级服务、代理、或自定义协议时也经常用到。本节就围绕 http 模块,从零开始构建一个功能完备的 HTTP 服务器。

1. 最简服务器:从三行代码开始

http.createServer 接收一个回调函数,每当有 HTTP 请求到达时,Node.js 就会调用该回调,并传入两个对象:

  • reqhttp.IncomingMessage):代表客户端请求,包含方法、URL、请求头、请求体等。
  • reshttp.ServerResponse):代表服务端响应,可以设置状态码、响应头、写入响应体、结束响应。
const http = require('http');

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, World\n');
});

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

访问 http://localhost:3000 就能看到返回的 Hello, World。这里有几个细节:

  • res.end 不仅发送响应体,还会通知服务器本次响应已完成。如果遗漏了 end,连接会一直挂起直到超时。
  • listen 会启动服务器并监听指定端口,第二个参数是启动成功的回调。除了端口,还可以监听 Unix Socket。

2. 解析请求:方法与路由

在实际应用中,服务器需要根据请求方法和路径分发逻辑。req.methodreq.url 是原生提供的属性。

const http = require('http');

const server = http.createServer((req, res) => {
  const { method, url } = req;
  
  // 简单路由分发
  if (method === 'GET' && url === '/api/users') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ users: ['Alice', 'Bob'] }));
  } else if (method === 'POST' && url === '/api/users') {
    // 处理 POST 创建用户(后面会展开说明如何解析请求体)
    res.writeHead(201, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'User created' }));
  } else {
    res.statusCode = 404;
    res.end('Not Found');
  }
});

req.url 包含完整路径和查询字符串(例如 /api/users?page=2),如需分离参数,可以结合 url 模块解析:

const { URL } = require('url');

// 在回调内
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
console.log(parsedUrl.pathname);  // '/api/users'
console.log(parsedUrl.searchParams.get('page'));  // '2'

这种基于 if-else 的路由方式会随着业务膨胀变得混乱,因此才有了 Express、Koa 等框架的诞生。但掌握原生路由仍然是理解中间件和路由实现原理的基础。

3. 处理请求体:数据流读取

Node.js 的请求对象 req 是一个可读流。当客户端通过 POST 或 PUT 发送 JSON 或表单数据时,我们需要从流中逐步拼接数据,再在 end 事件中解析。

if (method === 'POST' && url === '/api/users') {
  let body = '';
  req.on('data', chunk => {
    body += chunk.toString(); // 注意:大体积数据可能需要限制大小
  });
  
  req.on('end', () => {
    try {
      const parsed = JSON.parse(body);
      // 处理 parsed 数据...
      res.writeHead(201, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ message: 'User created', data: parsed }));
    } catch (e) {
      res.statusCode = 400;
      res.end('Invalid JSON');
    }
  });
}

需要注意:

  • 默认情况下,req 的数据流是不限制大小的,恶意客户端可能发送海量数据导致内存溢出。在生产环境应结合 req.headers['content-length'] 设定上限,或使用流式处理。
  • 对于 multipart/form-data 文件上传,手动拼接字节并解析边界极其繁琐;此时更推荐使用 busboy 或框架内置的中间件。

4. 响应控制:状态码、响应头、流式响应

通过 res.writeHead(statusCode, headers) 可以一次性设置状态码和多个响应头。也可以用 res.statusCoderes.setHeader 逐个设置,二者等价。

res.writeHead(200, {
  'Content-Type': 'text/html; charset=utf-8',
  'X-Powered-By': 'Node.js'
});

响应体同样可以通过多次 res.write 流式发送,最后用 res.end 结束:

res.write('<html><body><h1>Hello</h1>');
res.write('<p>This is a streaming response</p>');
res.end('</body></html>');

这种方式适合动态生成大页面,避免了一次性构建完整字符串的内存压力。

5. 错误处理与优雅关闭

服务器运行过程中可能因未捕获异常而崩溃,因此需要处理两类错误:

  • 服务器级别错误:例如端口被占用(EADDRINUSE),可以通过 server.on('error', callback) 捕获。
  • 请求处理中的同步错误:可以通过 try/catch 封装回调逻辑,阻止进程退出。更好的方式是将错误转化为对应的 HTTP 错误响应。
const server = http.createServer((req, res) => {
  try {
    // 业务逻辑...
  } catch (err) {
    res.writeHead(500, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: err.message }));
  }
});

server.on('error', (err) => {
  console.error('Server Error:', err);
});

// 捕获未处理的 Promise 拒绝,避免进程静默死亡
process.on('unhandledRejection', (reason) => {
  console.error('Unhandled Rejection:', reason);
});

要实现服务平滑关闭(例如 Docker 容器重启时),可以监听 SIGTERM 信号并停止接收新请求,等待已有请求处理完毕后再退出:

process.on('SIGTERM', () => {
  console.log('Received SIGTERM, closing server...');
  server.close(() => {
    console.log('Server closed');
    process.exit(0);
  });
});

server.close 会停止接收新的连接,但会等待现有请求完成。如果存在长连接(WebSocket、Keep-Alive),可能需要主动销毁连接。

6. 性能与注意事项

原生 HTTP 服务器虽然轻量,但在高并发下仍需注意:

  • Keep-Alive 默认行为:HTTP/1.1 默认使用持久连接,res.end 后连接不会立即关闭,而是复用直到超时。这减少了 TCP 握手的开销,但大量空闲持久连接会占用文件描述符。可以通过 server.keepAliveTimeout 调整超时时间(默认5秒)。
  • 监听连接事件:可以通过 server.on('connection', callback) 跟踪连接数,并在过高时拒绝新连接,实现简单的过载保护。
  • 安全性:原生服务器不会自动处理 CORS、XSS 防护、请求解析攻击等。至少要添加 Content-Type 检查、设置 X-Content-Type-Options: nosniff 等安全头,或直接使用 helmet 中间件。

7. 完整示例:一个带超时保护的原生服务器

下面整合了以上知识点,展示一个相对健壮的原生 HTTP 服务器,包含简单路由、JSON 解析、请求大小限制、超时处理和全局错误拦截。

const http = require('http');
const { URL } = require('url');

const MAX_BODY_SIZE = 1024 * 10; // 10KB
const server = http.createServer((req, res) => {
  // 设置统一的响应头
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('X-Content-Type-Options', 'nosniff');

  const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
  const pathname = parsedUrl.pathname;

  // 根路径返回简单信息
  if (req.method === 'GET' && pathname === '/') {
    return res.end(JSON.stringify({ status: 'ok' }));
  }

  // 接收 JSON 数据的 POST 路由
  if (req.method === 'POST' && pathname === '/data') {
    const contentLength = parseInt(req.headers['content-length'], 10);
    if (isNaN(contentLength) || contentLength > MAX_BODY_SIZE) {
      res.statusCode = 413;
      return res.end(JSON.stringify({ error: 'Payload too large' }));
    }

    let body = '';
    req.on('data', chunk => {
      body += chunk.toString();
      if (body.length > MAX_BODY_SIZE) {
        req.destroy(); // 数据过大,主动断开连接
        res.statusCode = 413;
        return res.end(JSON.stringify({ error: 'Payload too large' }));
      }
    });

    req.on('end', () => {
      try {
        const data = JSON.parse(body);
        res.statusCode = 200;
        res.end(JSON.stringify({ received: data }));
      } catch (e) {
        res.statusCode = 400;
        res.end(JSON.stringify({ error: 'Invalid JSON' }));
      }
    });

    return;
  }

  // 404
  res.statusCode = 404;
  res.end(JSON.stringify({ error: 'Not found' }));
});

// 超时处理:超过 5 秒未完成响应则断开
server.on('request', (req) => {
  req.setTimeout(5000, () => {
    res.statusCode = 408;
    res.end(JSON.stringify({ error: 'Request timeout' }));
  });
});

server.on('error', (err) => {
  console.error('Server error:', err);
});

server.listen(3000);

小结

原生 HTTP 模块是 Node.js 网络编程的根基。理解 http.createServerreqres 的流式特性、简单路由分发和错误处置,是掌握更高层框架原理的前提。尽管生产环境中很少直接用 http 模块编写大型服务,但在构建代理、轻量工具或学习框架源码时,这些知识是不可或缺的。后续章节我们将看到 Koa 和 Express 如何在这一基础之上抽象出中间件模型,使开发效率产生质的飞跃。