← DNS 模块 crypto 模块 →

Node.js Net模块教程:TCP服务端与客户端编程全解

著
原创 2026-10-12 Node.js 已有人查阅

TCP编程在Node.js中的位置

HTTP协议构建在TCP之上,WebSocket构建在TCP之上,Redis客户端、MySQL连接池、消息队列的底层通信同样构建在TCP之上。当应用需要自定义协议、需要长连接、需要比HTTP更低的传输开销时,直接操作TCP是绕不开的一步。

Node.js的net模块把这层能力封装成了事件驱动的API。它不关心你传输的是JSON、二进制帧还是纯文本,它只负责建立连接、搬运字节、在连接状态变化时通知你。这种低层抽象意味着更大的自由度,也意味着更多需要自己处理的细节——粘包、心跳、超时、错误恢复,这些在HTTP框架中由库代劳的事情,在net模块中需要显式设计。

代码号学习编程的过程中,net模块常常被跳过,因为日常业务用HTTP就够了。但理解net模块的工作方式,对排查HTTP连接问题、理解Node.js的流与事件模型都有直接帮助。下面从模块导入开始,逐步展开。

导入net模块

net是Node.js核心模块,无需安装。

// 代码号学习编程:导入 Net 模块
const net = require('node:net');

require('net')和require('node:net')功能等价,node:前缀是官方推荐的写法,明确标识内置模块,避免与node_modules中的同名包混淆。新项目建议统一使用该前缀。

导入后即可访问createServer、createConnection、isIP、isIPv4、isIPv6等方法,以及Server、Socket两个核心类。Server代表监听端,Socket代表一条具体的连接。理解这两者的分工,是掌握net模块的关键。

创建TCP服务端:net.createServer()

net.createServer()返回一个Server实例。传入的回调函数会在每次客户端连接建立时执行,参数是一个Socket对象,代表这条具体的连接。

// 代码号学习编程:创建 TCP 服务端
const net = require('node:net');

const server = net.createServer((socket) => {
    console.log('Client connected');
    socket.write('Welcome to the TCP Server!\n');
    socket.on('end', () => {
        console.log('Client disconnected');
    });
});

server.listen(3000, () => {
    console.log('Server running on port 3000');
});

服务端控制台先输出Serverrunningonport3000,客户端接入后输出Clientconnected,客户端断开后输出Clientdisconnected。

需要留意的是,socket.write()发送的WelcometotheTCPServer!是写给客户端的,服务端控制台不会显示这条内容。这是初学者容易困惑的一点:write是往对端发送数据,console.log才是往本地终端输出。

server.listen()的参数可以是端口号、端口加主机名、或者一个包含port、host、backlog等字段的对象。backlog指定等待队列的长度,当并发连接数超过服务端处理能力时,多余连接会进入这个队列。在高并发场景下适当调大backlog有助于减少连接被拒绝的情况。

创建TCP客户端:net.createConnection()

net.createConnection()创建一条到服务端的连接。连接建立后触发回调,之后可以收发数据。

// 代码号学习编程:创建 TCP 客户端
const net = require('node:net');

const client = net.createConnection(
    { port: 3000 },
    () => {
        console.log('Connected to server');
    }
);

client.on('data', (data) => {
    console.log(data.toString());
    client.end();
});

输出:

Connected to server
Welcome to the TCP Server!

data事件在服务端发来数据时触发。data参数是Buffer对象,调用toString()转为字符串。client.end()发送FIN包,优雅关闭连接。

这里有一个实际项目中的注意点:运行客户端之前,服务端必须已经在监听。如果服务端未启动,客户端会触发error事件,错误码为ECONNREFUSED。如果不监听error事件,Node.js会抛出未捕获异常导致进程退出。这一点在编写客户端代码时必须处理。

双向通信:发送与接收数据

TCP是全双工协议,客户端和服务端可以同时向对方发送数据。下面的例子演示一次完整的请求-响应交互。

服务端

// 代码号学习编程:服务端接收并响应
const net = require('node:net');

const server = net.createServer((socket) => {
    socket.on('data', (data) => {
        console.log('Client:', data.toString());
        socket.write('Message received');
    });
});

server.listen(3000);

客户端

// 代码号学习编程:客户端发送并接收响应
const net = require('node:net');

const client = net.createConnection({ port: 3000 }, () => {
    client.write('Hello Server');
});

client.on('data', (data) => {
    console.log('Server:', data.toString());
    client.end();
});

服务端控制台输出Client:HelloServer,客户端控制台输出Server:Messagereceived。

客户端通过client.write()发送数据,服务端在data事件中收到并打印,随后用socket.write()回传响应。客户端收到响应后关闭连接。

个人经验:在项目中,很少直接传输裸字符串。通常会在数据前后加长度前缀或分隔符来标识消息边界,否则会遇到粘包问题。TCP是字节流协议,不保证发送方的一次write对应接收方的一次data事件。发送方连续写入三条消息,接收方可能一次data事件收到全部三条,也可能分三次收到。这是设计自定义协议时必须优先考虑的问题。

核心方法与事件速查

常用方法

方法 作用
net.createServer() 创建TCP服务端
net.createConnection() 创建TCP客户端连接
server.listen() 启动监听,等待连接
server.close() 停止接受新连接,已有连接继续
socket.write() 向对端发送数据
socket.end() 优雅关闭连接,发送FIN
socket.destroy() 立即销毁连接,不等待缓冲数据发送
server.getConnections() 获取当前活跃连接数
net.isIP() 判断字符串是否为IP地址
net.isIPv4() 判断是否为IPv4地址
net.isIPv6() 判断是否为IPv6地址

常用事件

事件 触发时机
connection 客户端连接到服务端
data 收到对端数据
end 对端发送FIN,连接进入半关闭状态
close 连接关闭
error 发生错误
timeout 连接空闲超时
drain 写缓冲区排空,可以继续写入
listening 服务端开始监听

end和close的区别值得注意:end表示对端不再发送数据,但本端仍可写入;close表示连接彻底关闭,无法再收发数据。在处理半关闭场景时,这两个事件的顺序和时机直接影响逻辑正确性。

HTTP模块与Net模块的差异

对比维度 HTTP模块 Net模块
协议层 应用层HTTP协议 传输层TCP协议
抽象层级 较高,封装请求响应 较低,直接操作字节流
数据格式 解析HTTP头与体 原始Buffer
连接管理 自动处理keep-alive 需手动管理
典型场景 Web应用与API 自定义协议、长连接、实时系统

HTTP模块内部也是基于net模块实现的。当你调用http.createServer()时,底层仍然是一个TCP服务端在监听端口,HTTP模块负责解析收到的字节流,将其转换为req和res对象。理解这一层关系,有助于在HTTP出现异常时向下排查到TCP层面。

个人建议:除非确实需要自定义协议或极致性能,否则优先使用HTTP模块或更上层的框架。net模块带来的灵活度伴随着大量需要自行处理的边界情况,包括但不限于粘包、心跳保活、连接池管理、背压控制。这些在HTTP生态中已有成熟方案。

项目实例:Echo服务器

Echo服务器是最能体现net模块工作方式的例子:收到什么就回传什么。

// 代码号学习编程:Echo 服务器
const net = require('node:net');

const server = net.createServer((socket) => {
    socket.on('data', (data) => {
        console.log(data.toString());
        socket.write(`Echo: ${data.toString()}`);
    });
});

server.listen(3000, () => {
    console.log('Echo server running on port 3000');
});

服务端启动后,客户端发送Hello,服务端控制台输出Hello,客户端收到Echo:Hello。

这个例子的价值在于演示了data事件的触发和write的回传。在此基础上可以扩展的方向包括:给每条消息加长度前缀解决粘包、增加心跳机制检测死连接、设置socket.setTimeout()处理空闲连接、用socket.pipe()将数据流转发到其他流。

错误处理与常见

TCP编程中的错误大多来自连接状态变化,而非语法问题。以下是几个实际项目中高频出现的场景。

ECONNREFUSED:客户端连接时服务端未监听。必须在客户端监听error事件,否则进程退出。

ECONNRESET:对端强制关闭连接,本端仍在读写。常见于对端进程崩溃或网络中断。

EPIPE:向已关闭的连接写入数据。需要在write前检查socket.writable,或监听error事件统一处理。

粘包:TCP是字节流,不保留消息边界。解决办法有两种:定长消息、或长度前缀加消息体。长度前缀更通用,实现方式是先读取固定字节数的长度字段,再按该长度读取消息体。

背压:当写入速度超过对端读取速度时,socket.write()返回false,表示缓冲区已满。此时应暂停写入,等待drain事件后再继续。忽略背压会导致内存持续增长。

// 代码号学习编程:处理背压
function sendData(socket, data) {
    const canContinue = socket.write(data);
    if (!canContinue) {
        socket.once('drain', () => {
            // 缓冲区已排空,可以继续写入
        });
    }
}

在代码号学习编程的实践中,背压是最容易被忽略的问题。小数据量测试时不会暴露,一旦生产环境流量上来,内存占用会快速攀升。

现在框架中的惯用写法

在Express、Koa这类HTTP框架中,net模块通常不会直接出现,但底层的连接管理仍然依赖它。当需要实现WebSocket服务时,ws库内部使用net或http的upgrade机制建立连接。在实现自定义RPC协议时,常见做法是在net之上封装一层编解码器,负责消息的序列化、分帧和重组。

Node.js的stream模块与net模块配合紧密。socket本身就是一个双工流,可以调用pipe()将数据导向其他流,也可以被其他流pipe进来。在实现在理或隧道时,clientSocket.pipe(serverSocket)和反向pipe是常见写法。

本节课程知识要点

  1. net是Node.js内置模块,无需安装,推荐使用node:net前缀导入。

  2. net.createServer()创建TCP服务端,回调参数是代表单条连接的Socket。

  3. net.createConnection()创建TCP客户端,连接建立后触发回调。

  4. socket.write()发送数据,socket.end()优雅关闭,socket.destroy()立即销毁。

  5. 核心事件:connection、data、end、close、error、timeout、drain。

  6. end是半关闭,close是关闭,两者时机不同。

  7. TCP是字节流协议,不保留消息边界,需要自行处理粘包。

  8. 客户端必须监听error事件,否则ECONNREFUSED等错误会导致进程退出。

  9. socket.write()返回false表示缓冲区已满,需等待drain事件后再写入。

  10. HTTP模块构建在net模块之上,net提供更底层的控制能力。

练习与思考

  1. 编写一个TCP服务端,统计当前活跃连接数,并在每次连接建立和断开时打印。使用server.getConnections()验证统计结果。

  2. 实现一个简单的长度前缀协议:发送方先写入4字节的消息长度,再写入消息体;接收方先读4字节确定长度,再读取对应字节数的消息体。思考如何处理长度字段跨多个data事件到达的情况。

  3. 编写一个TCP客户端,连接失败时输出明确的错误信息,连接成功后每5秒发送一次心跳,服务端超过15秒未收到心跳则关闭连接。

  4. 思考:为什么socket.write()返回false时不能继续无限制写入?如果忽略背压,内存增长发生在哪个环节?

  5. 使用socket.pipe()实现一个简单的TCP代理,将3000端口的流量转发到4000端口。

← Node.js DNS模块实战教程:域名解析、记录查询与Promise API全解 Node.js crypto模块教程:哈希、HMAC、AES加解密与密钥派生指南 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号