为什么Node.js的net模块只能跑1.3GB/s?完整解析turbo-net:底层高性能TCP网络库
Node.js 里写 TCP 服务,net 模块是首选,但它在高吞吐场景下只能跑到 1.3GB/s。而 turbo-net 这个底层高性能 TCP 网络库,通过直接驱动 libuv、复用缓冲区、支持散列写入,把同样的 Echo 服务吞吐提升到 3.4GB/s,内存占用反而从约 100MB 降到约 35MB。本文用通俗的语言带你搞清楚:差距从哪来,以及 turbo-net 是如何做到的。
📊 先看实测:同样的 Echo 服务,性能差 2.6 倍
项目内置了两个可直接运行的对照示例:
| 实现 | 吞吐 | 内存占用 | 示例文件 |
|---|---|---|---|
Node.js 原生 net 模块 | 1.3GB/s | 约 100MB | examples/echo-classic.js |
| turbo-net | 3.4GB/s | 约 35MB | examples/echo-turbo.js |
💡 Echo 服务是网络库的"体检项目":数据原样读进来再写回去,没有任何业务逻辑,因此吞吐差异几乎全部来自网络层本身的开销。
想自己验证?克隆仓库即可运行:
git clone https://gitcode.com/gh_mirrors/tu/turbo-net
🔍 为什么 net 模块"只跑一半"?两个隐藏开销
net 模块好用,是因为它站在两层"通用设施"之上,而这两层在极致吞吐场景下都会产生成本:
1. 高频内存分配:每次读数据都"新买一块地皮"
使用 net 时,每当触发 data 事件,底层都会新分配一个 Buffer 把数据放进去。数据流越大、越快,内存分配器(V8 堆 + 系统 malloc)就越忙。对比 examples/echo-classic.js 可以看到,经典写法完全依赖框架自动分配的缓冲区——每秒几 GB 的数据流意味着海量的临时 Buffer 出生又死亡,垃圾回收压力巨大。这也是为什么经典示例要吃掉约 100MB 内存。
2. Stream 封装的间接层
net 的 Socket 是基于 Node.js 的 Stream 体系构建的:数据经过流的事件循环分发、背压控制、编码转换等通用机制。这些机制对"通用性"很友好,但对"极致速度"来说,都是额外的函数调用和数据搬运。
🚀 turbo-net 的三板斧:把中间层全部拿掉
turbo-net 的核心思想很简单:跳过 Stream 层,让 JS 代码直接与 libuv(Node.js 底层的 C 网络库)对话。
① 缓冲区由你自己提供、反复复用
turbo-net 的 read 接口要求你传入一个 Buffer,数据直接写进这块内存(见 src/turbo_net.c 中的 turbo_net_tcp_read)。读完后回调返回,你再决定用同一块 Buffer 继续读。
官方示例 examples/echo-turbo.js 就是这个套路:预分配几块 Buffer 在"读 → 写"之间循环使用,全程几乎零新分配——这就是内存只有 35MB 的原因。
② 请求队列复用,避免反复创建对象
每次读写请求在 C 层需要一个句柄(uv_write_t)。turbo-net 用了一个巧妙的环形请求队列 lib/queue.js:请求对象一次性预创建,用完即回收,通过位掩码(mask)实现 O(1) 的入队出队。热路径上不再有任何对象分配。
③ writev 散列写入:一次系统调用发多段数据
TCP 一次发送由多段 Buffer 组成时(比如"协议头 + 正文"),普通写法要么拼接成新 Buffer(一次拷贝),要么发多次系统调用。turbo-net 的 writev 接口(lib/connection.js)直接把多段 Buffer 交给 C 层做 scatter-gather write(见 src/turbo_net.c 的双 Buffer 快速路径 turbo_net_tcp_write_two),一次系统调用搞定,零拷贝。
架构一览
整个项目结构非常精简,一眼能看完整条链路:
| 模块 | 作用 |
|---|---|
| src/turbo_net.c | C 原生层,通过 N-API 直接调用 libuv 的 TCP 接口 |
| lib/binding.js | 加载预编译二进制(node-gyp-build) |
| lib/server.js | TCP 服务端封装(listen、connection 事件) |
| lib/connection.js | 连接封装(read / write / writev / end / close) |
| lib/queue.js | 读写请求的环形复用队列 |
| lib/lookup.js | 域名解析,纯 IP 直接跳过 DNS |
| examples/ | 3 个可直接运行的示例 |
| test/ | 服务端、连接、读写的完整测试用例 |
⚡ 五分钟上手:写出 3.4GB/s 的 Echo 服务
npm install turbo-net
它保留了与 net 相似的 API 风格,迁移成本很低(入口在 index.js):
const turbo = require('turbo-net')
const server = turbo.createServer(function (socket) {
socket.read(Buffer.alloc(32 * 1024), function onread (err, buf, read) {
if (err) throw err
socket.write(buf, read, function (err) {
if (err) throw err
socket.read(buf, onread) // 复用同一个 Buffer 继续读
})
})
})
server.listen(8080)
常用 API 与 net 的对照:
| turbo-net | 对应 net 用法 | 说明 |
|---|---|---|
turbo.createServer(fn) | net.createServer(fn) | 创建服务端,支持 allowHalfOpen 选项 |
server.listen(port, cb) | 同名 | 还支持 server.address() 查询实际端口 |
turbo.connect(port, host) | net.connect | 客户端连接,支持 DNS 域名 |
socket.read(buf, cb) | socket.on('data') | 关键差异:读入你提供的 Buffer |
socket.write(buf, len, cb) | socket.write | 可指定写入长度 |
socket.writev(buffers, cb) | 无 | 多段 Buffer 一次写入 |
socket.end() / close() | 同名 | 半关闭 / 全关闭 |
📌 注意:read 的回调参数是 (err, buffer, bytesRead),当 bytesRead 为 0 时表示对端已关闭可读方向。
🤔 turbo-net 适合谁?
适合:
- 文件同步 / P2P 传输 / 存储引擎等大流量、低消息频率场景
- 已经熟悉
net模块、想压榨最后一滴性能的开发者 - 用 C++ 扩展写底层、需要绕过 Stream 层的框架开发者(它同时支持 N-API 与 N-API 宏,跨 Node 版本稳定)
可能不需要:
- 普通 Web 后端、WebSocket 网关等以 HTTP 为主的业务——
http/net的性能早已够用 - 追求"完全零配置"的场景:缓冲区管理需要你显式编写(这也是它快、也更"底层"的原因)
✅ 总结
- Node.js
net模块的 1.3GB/s 上限,主要卡在高频内存分配与 Stream 通用层的间接开销; - turbo-net 通过自管缓冲区复用、直连 libuv、writev 散列写入三板斧,把 Echo 吞吐做到 3.4GB/s,内存还降低约 65%;
- API 与
net一脉相承,几行代码即可替换,是 Node.js 生态中少见的、面向极致吞吐的底层 TCP 网络库。
如果你的业务瓶颈在"数据搬得不够快",不妨跑一下 examples/echo-turbo.js 亲自感受一下这台"涡轮增压"。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



