# 网络服务 http
30 秒速记
http是流式模型:req是可读流、res是可写流,数据边到边处理- 每个请求都要有结局:请求体要读完或丢掉,响应必须
res.end(),否则连接一直挂着 - 给慢请求设上限:
server.headersTimeout、server.requestTimeout(Node 18起默认300s)、请求体大小自己限 keep-alive复用连接,server.keepAliveTimeout(默认5s)要比前面负载均衡的空闲超时长,否则偶发502- 客户端中途断开要监听
close,把下游请求、数据库查询一起取消
Node 的 http 模块是流式的:请求进来是一个可读流,响应出去是一个可写流,每个请求都必须读完、写完、结束。 这一点决定了很多坑的形态:忘了 res.end(),客户端就一直转圈;不限制请求体大小就往内存里拼,一个大包能把进程撑爆;keep-alive 的超时和前面 Nginx、负载均衡对不上,就会出现偶发 502。所以写原生 http 服务,我关心的是每条分支最后有没有收尾,以及超时和大小上限有没有设。
理解「网络服务 http」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1接口偶发一直转圈,进程和端口都正常,第一反应查什么?
查有没有分支漏了 res.end(),尤其是 catch 里和提前 return 的地方。Node 不会替你结束响应,漏一个分支,那个请求就挂到超时为止。
追问 2Node 服务前面挂了负载均衡,偶尔报 502,后端日志里根本没这个请求,可能是什么?
大概率是 keep-alive 超时没对齐。Node 默认 keepAliveTimeout 是 5s,负载均衡空闲超时一般是 60s,Node 先关了连接,负载均衡还往这条旧连接上发请求就会 502。把 server.keepAliveTimeout 调到比负载均衡长,比如 65000,headersTimeout 再大一点。
追问 3用户上传到一半关掉页面,服务端要做什么?
监听 req.on('close') 或 res.on('close'),判断 res.writableFinished 为 false 就说明是中途断开,这时把还在跑的下游请求用 AbortController 取消掉,临时文件删掉。
追问 4为什么说不读请求体会出问题?
客户端那边:http.get 拿到响应却不消费,socket 一直被占着回不到连接池,并发一高就卡死,不需要就调 res.resume() 丢掉。服务端没读完请求体就响应,连接可能没法复用。
追问 5线上排查原生 http 问题时,为什么建议先缩成一个最小服务复现?
框架的中间件、默认超时会叠在一起干扰判断。用十几行原生 http.createServer 复现,如果问题还在,就是协议或网络层的事;不在,再一层层把中间件加回去。
# http模块概览
30 秒速记
http是Node写Web服务最核心的模块,几行就能起一个服务器- 四个核心实例:
http.Server、http.ClientRequest、http.ServerResponse、http.IncomingMessage - 理顺角色:服务端「收请求
IncomingMessage+ 回响应ServerResponse」,客户端「发请求ClientRequest+ 收响应IncomingMessage」 - 底下全是流:
IncomingMessage可读、ServerResponse/ClientRequest可写 Express、Koa都是在这套对象外面包了一层,排障时还得回到这四个对象
http 模块上手简单、深入头疼,原因是它同时管服务端和客户端,四个核心对象来回出现。 把它们按「谁发、谁收」排好就清楚了:服务端是 http.Server,每来一个请求给你一对 req(IncomingMessage)和 res(ServerResponse);客户端发请求用 ClientRequest,收到的响应同样是一个 IncomingMessage。Express、Koa 的 req、res 也是这两个对象扩展出来的,所以学透这四个,看框架源码也不吃力。
大多数nodejs开发者都是冲着开发web server的目的选择了nodejs。正如官网所展示的,借助http模块,可以几行代码就搞定一个超迷你的web server。
在nodejs中,http可以说是最核心的模块,同时也是比较复杂的一个模块。上手很简单,但一旦深入学习,不少初学者就会觉得头疼,不知从何入手。
本文先从一个简单的例子出发,引出http模块最核心的四个实例。看完本文,应该就能够对http模块有个整体的认识。
面试官追问
追问 1会写 http.createServer 算不算掌握了 http 模块?
只会了一半。Node 里调别的服务也用 http.request,那边的 ClientRequest 和收到的 IncomingMessage 才是做网关、做代理时天天打交道的东西。
追问 2Express 的 req 和原生 req 是什么关系?
Express 的 req 就是原生 IncomingMessage,只是把原型链接到了自己的扩展对象上,加了 req.query、req.params 这些。所以原生的 req.on('data')、req.headers 在 Express 里照样能用。
追问 3健康检查接口要不要引入框架?
一般不用。if (req.url === '/health') return res.end('ok') 一行就搞定,依赖越少越不容易挂。路由和中间件多起来了再上框架。
追问 4为什么说 http 的对象都是流?
因为 HTTP 报文可能很大或者持续很久,Node 不会等全部收齐再给你。req 可以 pipe 到文件,文件也可以 pipe 到 res,内存占用跟文件大小无关。
# 一个简单的例子
30 秒速记
- 一个脚本里同时起服务端和客户端:服务端回显访问路径,客户端把响应打印出来
- 服务端:
http.createServer((req, res) => res.end(...))+server.listen(3000) - 客户端:
http.get(url, cb),回调里的clientRes是可读流,pipe(process.stdout)直接输出 - 没写路径时
req.url是'/',不是空字符串 http.get会自动调用req.end();http.request要自己调
这个例子用一个脚本把一次完整的 HTTP 往返跑了一遍:服务端收到请求、回写访问路径,客户端发请求、把响应打到控制台。 运行后控制台会打印 您访问的地址是:/,因为 http.get('http://127.0.0.1:3000') 没写路径时,请求行里的路径就是 /。客户端回调拿到的 clientRes 是可读流,所以能直接 pipe 到 process.stdout。真要写进项目,还得补错误处理,示例里一个 error 监听都没有,端口占用会直接让进程崩掉。
在下面的例子中,我们创建了1个web服务器、1个http客户端
- 服务器server:接收来自客户端的请求,并将客户端请求的地址返回给客户端。
- 客户端client:向服务器发起请求,并将服务器返回的内容打印到控制台。
代码如下所示,只有几行,但包含了不少信息量。下一小节会进行简单介绍。
var http = require('http');
// http server 例子
var server = http.createServer(function(serverReq, serverRes){
var url = serverReq.url;
serverRes.end( '您访问的地址是:' + url );
});
server.listen(3000);
// http client 例子
var client = http.get('http://127.0.0.1:3000', function(clientRes){
clientRes.pipe(process.stdout);
});
面试官追问
追问 1http.get('http://127.0.0.1:3000') 没写路径,服务端 req.url 拿到什么?
拿到 '/'。HTTP 请求行里路径不能为空,Node 会补上根路径。带查询串的话 req.url 是 '/a?x=1',路径和参数要自己用 new URL(req.url, 'http://x') 拆。
追问 2客户端报 ECONNREFUSED,可服务端明明写了 listen(3000),可能是什么?
listen 是异步的,而且失败不会抛到同步代码里。给 server.on('error') 打个日志,多半是端口被占了;要严格保证顺序,就把 http.get 放进 server.listen(3000, () => {...}) 的回调里。
