在 Node.js 中不仅要能接收 HTTP 请求,很多场景下服务本身也需要扮演客户端的角色:调用第三方 API、请求下游微服务、抓取网页内容等。http 和 https 模块提供了最原生的客户端请求能力。理解这套 API 的原理,能帮助你在不引入第三方库的情况下完成基本的 HTTP 调用,也能更好地理解诸如 axios、got 等流行库的内部实现。
原生 http.get 快速发起 GET 请求
对于不需要自定义请求头、不需要发送 Body 的简单 GET 场景,http.get 是最快捷的方法。它自动将请求方法设为 GET,并调用 req.end() 结束请求。
const https = require('https');
https.get('https://api.github.com/users/github', (res) => {
let data = '';
// 响应数据可能分块到达,需要拼接
res.on('data', (chunk) => {
data += chunk;
});
// 数据接收完毕
res.on('end', () => {
try {
const parsed = JSON.parse(data);
console.log(`用户名称: ${parsed.name}`);
console.log(`粉丝数: ${parsed.followers}`);
} catch (e) {
console.error('JSON 解析失败:', e.message);
}
});
}).on('error', (err) => {
console.error('请求失败:', err.message);
});
输出会是类似这样的结果:
用户名称: GitHub
粉丝数: 12345
http.get 实际上返回一个 http.ClientRequest 对象,可以监听 error 事件。在 Node.js 中,如果没有为这个返回对象添加错误处理器,请求过程中发生的错误会直接抛出并可能导致进程崩溃。因此 始终为请求对象添加 error 监听器 是一个铁律。
通用请求方法:http.request
http.request 比 get 更灵活,可以指定任意 HTTP 方法(POST / PUT / DELETE 等)、自定义请求头以及发送 Body 数据。它返回同样的 ClientRequest 对象,但 需要手动调用 req.end() 来发送请求。
发送 POST 请求(带 JSON Body)
const https = require('https');
// 待发送数据
const postData = JSON.stringify({
title: 'foo',
body: 'bar',
userId: 1,
});
const options = {
hostname: 'jsonplaceholder.typicode.com',
path: '/posts',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
},
};
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
console.log('状态码:', res.statusCode);
console.log('响应:', JSON.parse(responseData));
});
});
req.on('error', (err) => {
console.error('请求出错:', err.message);
});
// 写入请求体并终止请求
req.write(postData);
req.end();
要点说明:
Content-Type和Content-Length是服务端正确解析 JSON 的必要头信息,尤其是Content-Length,部分严格的服务端会根据它来判断请求体是否接收完整。req.write()可多次调用,用于发送大文件或分块数据;对于一次性的数据,也可以直接将数据写在req.end(postData)里。- 请求对象有
error事件,响应对象也可能有error事件(例如响应流意外中断)。健壮的代码应当同时监听两者。
设置超时与终止请求
原生 API 并不自动提供超时机制,长时间未响应的请求会导致程序挂起。可以通过 req.setTimeout() 或 req.on('timeout', ...) 来实现超时控制:
const req = https.request(options, (res) => { /* ... */ });
req.setTimeout(5000, () => {
// 超时后销毁请求,触发 error 事件
req.destroy(new Error('请求超时'));
});
req.on('error', (err) => {
console.error('请求失败:', err.message);
});
req.end();
当请求在 5 秒内未完成时,req.destroy() 会主动断开底层 Socket,并触发 error 事件。注意要在销毁后清理可能的资源泄漏。
对于 http.get,同样可以链式调用 setTimeout:
https.get('https://api.example.com', { timeout: 5000 }, (res) => { /* ... */ })
.on('error', console.error);
HTTPS 的特有配置:忽略证书验证(仅开发调试)
在对接自签名证书或测试环境时,HTTPS 模块默认会验证服务器证书的有效性,导致 UNABLE_TO_VERIFY_LEAF_SIGNATURE 之类的错误。可以在开发阶段 临时 设置环境变量:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
但 绝对禁止在生产环境关闭证书验证,这样做会使连接容易遭受中间人攻击。更安全的做法是为特定请求传入自定义的 https.Agent:
const agent = new https.Agent({
rejectUnauthorized: false, // 仅用于内部测试
});
https.get('https://self-signed.internal', { agent }, (res) => { /* ... */ });
处理重定向
http/https 模块 不会自动跟随重定向。当收到 301/302 状态码时,需要手动提取 Location 头并重新发起请求。实际开发中这个逻辑往往被封装为函数,或者直接使用具备自动重定向功能的第三方库(如 axios 的 maxRedirects)。
一个简单的手动重定向实现思路:
function requestWithRedirect(url, callback, maxRedirects = 5) {
const lib = url.startsWith('https') ? https : http;
lib.get(url, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (maxRedirects <= 0) {
callback(new Error('重定向次数过多'));
return;
}
// 递归跟随重定向
requestWithRedirect(res.headers.location, callback, maxRedirects - 1);
} else {
callback(null, res);
}
}).on('error', callback);
}
注意需要处理相对路径与绝对路径的重定向地址,并使用 url.resolve 或 new URL 来拼接完整 URL。
连接复用:使用 Agent 管理连接池
http/https 默认会为每个请求创建新的 TCP 连接,并在响应完成后保持连接一段时间(keep-alive)。通过设置 http.Agent 可以更精细地控制连接池的行为:
const keepAliveAgent = new https.Agent({
keepAlive: true, // 启用连接复用
maxSockets: 10, // 同一主机最大连接数
maxFreeSockets: 5, // 空闲连接池上限
timeout: 60000, // 空闲连接回收时间
});
const options = {
hostname: 'api.example.com',
agent: keepAliveAgent,
};
https.get(options, (res) => { /* ... */ });
对于需要短时间内向同一服务发起大量请求的场景,连接复用能显著提升性能并降低端口耗尽风险。
与第三方库的对比选择
原生 http/https 模块足够底层、零依赖,适合一些极简场景或编写底层工具。但在生产项目中,开发者通常会选择更高层次的 HTTP 客户端库:
- axios:支持 Promise、自动 JSON 解析、请求/响应拦截器、取消请求、自动重定向。
- node-fetch:将浏览器 Fetch API 引入 Node.js,符合 Web 标准,适合前后端代码统一。
- got:专为 Node.js 设计,支持超时、重试、进度事件、HTTP/2。
- superagent:链式 API,支持浏览器和 Node.js。
这些库普遍提供了:
- 更简洁的 API
- 内置超时与重试机制
- 自动解析 JSON
- 请求/响应拦截与转换
- 完善的错误处理
例如用 axios 完成上述 POST 请求只需几行:
const axios = require('axios');
axios.post('https://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: 'bar',
userId: 1,
})
.then(response => console.log(response.data))
.catch(error => console.error(error.message));
尽管封装层次不同,理解原生模块的工作原理仍然有价值:当库无法满足特殊需求(比如自定义协议、底层流控、资源精细管理),或者需要避免依赖膨胀时,你可以直接使用 http/https 写出高度可控的客户端代码。
实际开发中的注意点
- 始终处理
error事件:请求对象、响应流都应监听错误,避免未捕获异常导致进程退出。 - 限制请求体大小:服务端返回的响应可能会非常大,应当在
res.on('data')中累积数据前做好长度检查,防止内存溢出。 - 设置合理的超时:网络环境不可靠,永远不要假设请求一定会完成。
- 关闭不必要的连接:如果不再需要某个 Agent 或请求,应及时清理,避免占用文件描述符。
- 区分 HTTP 和 HTTPS 模块:两者 API 几乎相同,但
http.request无法请求 HTTPS URL(会报错),反之亦然。可以使用url.startsWith('https')动态选择模块。
通过 http/https 原生的客户端请求能力,你可以构建轻量级的代理、爬虫、接口测试工具,也为深入理解 Node.js 的网络通信机制打下了基础。在后续的章节中,我们将进一步讨论文件上传、流式处理以及安全相关的编程实践。