在 Node.js 的生态中,Express、Koa、Fastify 等框架提供了便捷的路由、中间件机制,但它们的底层依然使用的是 Node.js 内置的 http 模块。直接使用 http 模块搭建服务器,不仅能帮助我们理解框架的本质,在编写轻量级服务、代理、或自定义协议时也经常用到。本节就围绕 http 模块,从零开始构建一个功能完备的 HTTP 服务器。
1. 最简服务器:从三行代码开始
http.createServer 接收一个回调函数,每当有 HTTP 请求到达时,Node.js 就会调用该回调,并传入两个对象:
req(http.IncomingMessage):代表客户端请求,包含方法、URL、请求头、请求体等。res(http.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.method 和 req.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.statusCode 和 res.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.createServer、req 与 res 的流式特性、简单路由分发和错误处置,是掌握更高层框架原理的前提。尽管生产环境中很少直接用 http 模块编写大型服务,但在构建代理、轻量工具或学习框架源码时,这些知识是不可或缺的。后续章节我们将看到 Koa 和 Express 如何在这一基础之上抽象出中间件模型,使开发效率产生质的飞跃。