# 网络服务 http req
30 秒速记
req是http.IncomingMessage:服务端代表收到的请求,客户端代表收到的响应- 它是可读流:请求头在对象上直接读,请求体要按流读(
data/end或for await) URL、请求头、请求体全来自外部,都不可信,要校验和限长- 请求头的键全是小写;重复头有的丢弃、有的合并,
Set-Cookie是数组 - 判断客户端提前断开:
res.on('close')时看res.writableFinished
req 就是一个 http.IncomingMessage 对象,你可以把它理解成「收到的那封信」:信封上的东西(方法、地址、请求头)直接能读,信里的内容(请求体)要当流慢慢读。 服务端回调里的 req 是客户端发来的请求,http.get 回调里的 res 是服务端返回的响应,两个是同一个类,只是方向不同。用的时候我最在意两件事:一是所有字段都来自外部,不能直接信;二是请求体必须读完或者明确丢弃,不然连接会一直挂着。
理解「网络服务 http req」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
http 模块里请求和响应各有一个核心对象:req 是 http.IncomingMessage,res 是 http.ServerResponse。IncomingMessage 同时承担两个角色:在服务端它是客户端发来的请求,在客户端(http.get / http.request)它是服务端返回的响应。
const http = require('http');
http.createServer(async (req, res) => {
// 信封上的信息:直接读
console.log(req.method, req.url, req.httpVersion); // GET /hello?a=1 1.1
console.log(req.headers['user-agent']); // 键全是小写
// 信里的内容:按流读,限制大小
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > 1e6) { res.writeHead(413).end(); return req.destroy(); }
chunks.push(chunk);
}
const body = Buffer.concat(chunks).toString('utf8');
res.end(`got ${body.length} chars`);
}).listen(3000);
记住三件事:所有字段都是外部输入,要校验;请求体要读完或者明确丢弃(req.resume()),否则 keep-alive 连接没法复用;客户端提前断开时,要把取消传给下游任务,别让服务端继续做无用功。
面试官追问
追问 1服务端回调里的 req 和 http.get 回调里的 res 是同一个类吗?
是,都是 http.IncomingMessage,都是可读流。区别在方向:前者有 url、method,后者有 statusCode、statusMessage。
追问 2为什么请求体不能像 req.headers 一样直接取?
请求头在请求开头就到齐了,请求体可能很大、分多次到达,Node.js 不会帮你全读进内存。要自己按流读,for await (const chunk of req) 是最简洁的写法。
追问 3客户端请求到一半关了页面,服务端怎么知道?
新版本 Node.js 推荐 res.on('close', () => { if (!res.writableFinished) abort() })。req 上的 aborted 事件在 Node.js 17 左右已经废弃,新代码别再依赖它。
追问 4req.headers 里两个同名头会怎样?
看字段:Content-Type、Host 这类只保留第一个;Set-Cookie 是数组;其他大多用 , 拼成一个字符串。要看原始样子用 req.rawHeaders,Node.js 18.3 起还有 req.headersDistinct 全部给数组。
# 概览
30 秒速记
- 服务端
createServer((req, res) => {})里的req和客户端http.get(url, res => {})里的res都是IncomingMessage - 服务端那个 = 收到的请求 → 读
method、url、headers、请求体 - 客户端那个 = 收到的响应 → 读
statusCode、headers、响应体 - 变量名不重要,看它是谁「收到」的消息
IncomingMessage 可以理解成「进来的消息」:服务端收到请求、客户端收到响应,拿到的都是它。 所以服务端回调里叫 req,客户端 http.get 回调里叫 res,类型其实一样。区别是消息方向:请求里有 url 和 method,响应里有 statusCode。我见过有人在服务端读 req.statusCode 拿到 undefined 还以为是框架的问题,其实是读错了方向。
本文的重点会放在req这个对象上。前面已经提到,它其实是http.IncomingMessage实例,在服务端、客户端作用略微有差异
- 服务端处:获取请求方的相关信息,如request header等。
- 客户端处:获取响应方返回的相关信息,如statusCode等。
服务端例子:
// 下面的 req
var http = require('http');
var server = http.createServer(function(req, res){
console.log(req.headers);
res.end('ok');
});
server.listen(3000);
客户端例子
// 下面的res
var http = require('http');
http.get('http://127.0.0.1:3000', function(res){
console.log(res.statusCode);
});
面试官追问
追问 1同事说服务端的 req 和 http.get 回调里的 res 类型不一样,对吗?
不对,两个都是 http.IncomingMessage。只是一个是收到的请求、一个是收到的响应,能用的字段不同。
追问 2订单服务要记录调用方的 User-Agent,探活脚本要拿返回状态,分别从哪取?
订单服务读 req.headers['user-agent'],探活脚本在 http.get 回调里读 res.statusCode。
追问 3在服务端读 req.statusCode 会得到什么?
undefined,statusCode 只有响应方向的 IncomingMessage 才有值。服务端要设状态码是往 res(ServerResponse)上设。
追问 4TypeScript 里这两个对象怎么标类型?
都标 http.IncomingMessage。业务层更好的做法是在入口把它转成自己的 { method, path, query } 结构,下游就不用关心原始对象了。
# 属性/方法/事件 分类
30 秒速记
- 两端共有:
headers、rawHeaders、httpVersion、socket、destroy()、setTimeout()、close事件 - 服务端特有:
url、method;客户端特有:statusCode、statusMessage headers键名小写、重复头已处理;rawHeaders是[名, 值, 名, 值]扁平数组,保留原始大小写和顺序aborted事件新版本已废弃,断开检测改用close配合状态判断req.socket.remoteAddress是直连方地址,挂了代理就是代理的IP
IncomingMessage 的成员分两类:两端都有的通用部分,和只在某一端有意义的部分。 通用的有 headers、rawHeaders、httpVersion、socket,以及 destroy()、setTimeout();服务端特有 url 和 method,客户端特有 statusCode 和 statusMessage。实际写代码最常用的是 headers 和 rawHeaders 的区别:前者键名统一小写、方便取值,后者保留原貌、适合审计和排查。
http.IncomingMessage的属性/方法/事件 不是特别多,按照是否客户端/服务端 特有的,下面进行简单归类。可以看到
- 服务端处特有:url
- 客户端处特有:statusCode、statusMessage
| 类型 | 名称 | 服务端 | 客户端 |
|---|---|---|---|
| 事件 | aborted | ✓ | ✓ |
| 事件 | close | ✓ | ✓ |
| 属性 | headers | ✓ | ✓ |
| 属性 |
