TypeScript 网络编程从零到一:net 模块全解析
很多刚接触网络编程的同学都会有一个困惑:B 站、论坛上的教程全是 C++/Java,语法晦涩、环境折腾,还没写通一个 TCP 连接就先被劝退了。
但其实,TypeScript + Node.js 是新手入门网络编程的最优选择 :语法友好、跨平台、不用折腾复杂的编译环境,Node.js 内置的 net 模块封装了完整的 TCP 网络通信能力,API 设计简洁易懂,写通一个客户端 - 服务端通信只需要十几行代码。
这篇博客会从零开始,带你吃透 net 模块的所有核心能力,从基础概念到完整实战,再到新手必踩的坑,全程只讲 TypeScript,无额外技术栈干扰,看完就能独立写出完整的 TCP 网络程序。
一、前置环境准备(一步到位,零踩坑)
1. 必须安装的环境
-
Node.js:LTS 长期支持版(官网直接下载,下一步安装即可,自带 npm)
-
TypeScript
:全局安装,终端执行:
bashnpm install -g typescript @types/node -
编辑器:VSCode(代码提示、终端集成,对 TS 新手最友好)
2. 验证环境是否正常
终端执行以下 3 条命令,能正常输出版本号即环境正常:
bash
node -v
npm -v
tsc -v
3. 项目初始化 & TS 配置
-
新建一个空文件夹,用 VSCode 打开
-
终端执行
tsc --init,自动生成tsconfig.json配置文件 -
把
tsconfig.json里的内容替换为以下极简可用配置(新手零报错):
json{ "compilerOptions": { "target": "ES6", "module": "CommonJS", "outDir": "./dist", "rootDir": "./src", "esModuleInterop": true, "strict": true, "skipLibCheck": true }, "include": ["src/**/*"] } -
新建
src文件夹,我们所有的代码都放在这里面。
4. 最稳的 TS 代码运行方式(新手必看)
很多新手第一步就栽在运行命令上,这里给你 2 种零报错、兼容所有系统的运行方式,优先用第一种。
方式 1:先编译,后运行(最稳,无任何依赖)
TS 代码不能直接被 Node.js 执行,必须先编译成 JS,再运行,分 2 步执行,避开所有语法坑:
- 编译 TS 代码:
tsc src/你的文件名.ts - 运行编译后的 JS 文件:
node dist/你的文件名.js
方式 2:ts-node 直接运行(省事,开发首选)
全局安装 ts-node 后,可跳过手动编译,直接运行 TS 代码:
- 全局安装:
npm install -g ts-node - 直接运行:
ts-node src/你的文件名.ts
二、核心概念大白话扫盲(先懂逻辑,再写代码)
网络编程的本质,就是两个运行在不同设备(或同一设备)上的程序,通过网络互相收发数据。我们用「打电话」的类比,一次性搞懂所有核心概念,再也不会被术语绕晕。
| 术语 | 大白话解释 | 打电话类比 |
|---|---|---|
| TCP | 传输控制协议,一种可靠、面向连接的网络通信标准,保证数据不丢失、不重复、按顺序到达 | 正规的有线电话,必须对方接通了才能说话,不会漏话、不会串线 |
| 服务端(Server) | 被动等待连接的程序,一直监听一个固定的端口,等待客户端连接 | 守在固定电话旁的人,等着别人打电话进来 |
| 客户端(Client) | 主动发起连接的程序,知道服务端的地址和端口,主动发起通信 | 主动拨打电话的人 |
| Socket | 网络通信的「通道」,服务端和客户端建立连接后,会生成一个专属的 socket,所有数据收发都通过这个 socket 完成 | 电话线,连接两端的通话通道 |
| 端口(Port) | 设备上的「门牌号」,范围 0-65535,一个端口只能被一个程序占用,客户端通过端口找到对应的服务端 | 固定电话的号码,拨对号码才能找到对应的人 |
| IP 地址 | 设备在网络中的「身份证号」,局域网 / 公网中唯一标识一台设备 | 你家的地址,快递(数据)能精准送到 |
我们接下来所有的代码,都是围绕「服务端监听端口 → 客户端主动连接 → 两端通过 socket 收发数据」这个核心逻辑展开的。
三、net 模块核心 API 全解析(逐行讲透,细节拉满)
net 是 Node.js 内置的 TCP 网络编程模块,无需额外安装,直接导入即可使用。注意:TS 中必须用 import \* as net from 'net' 导入,不能用 import net from 'net',否则会报「no default export」错误,这是新手第一大坑。
我们把 API 分为「服务端 API」和「客户端 API」两部分,逐个拆解,每个 API 都讲清作用、参数、示例和触发时机。
1. 服务端核心 API
1.1 net.createServer([options], connectionListener)
作用:创建一个 TCP 服务端实例,是所有服务端代码的入口。
- 参数 1
options(可选):服务端配置,新手入门几乎不用,默认即可 - 参数 2
connectionListener(必填) :回调函数,每当有新的客户端连接成功时,就会执行一次 - 回调函数的参数
socket:核心中的核心,代表「和当前客户端的专属连接通道」,所有和这个客户端的数据收发、断开操作,都通过这个 socket 完成 - 返回值 :
net.Server实例,也就是我们创建的服务端
基础示例:
typescript
import * as net from 'net';
// 创建TCP服务端
const server = net.createServer((socket: net.Socket) => {
// 有新客户端连接时,这里就会执行
console.log(`新客户端连接成功!客户端地址:${socket.remoteAddress}:${socket.remotePort}`);
});
1.2 server.listen(port, [host], [backlog], [callback])
作用 :让服务端开始监听指定的端口,等待客户端连接。服务端必须调用 listen 后,才能接收客户端连接。
- 参数 1
port(必填):要监听的端口号,范围 1024-65535(1024 以下是系统保留端口) - 参数 2
host(可选) :要监听的 IP 地址,默认0.0.0.0(监听本机所有网卡地址,局域网内其他设备也能连接) - 参数 3
backlog(可选):最大等待连接数,新手不用管,默认即可 - 参数 4
callback(可选) :回调函数,监听成功后,仅执行一次,用来提示服务端启动完成
新手必懂的误区纠正:
listen 不是「每隔几秒刷新一次」,而是让服务端进入「持续阻塞监听」状态,只要服务端不关闭,就会一直守在这个端口,等待客户端连接,不会自动停止。
基础示例:
typescript
// 监听3000端口
server.listen(3000, '0.0.0.0', () => {
console.log('✅ TCP服务端启动成功,正在监听3000端口...');
});
1.3 server.close([callback])
作用:关闭服务端,停止接收新的客户端连接,已经建立的连接不会被强制断开。
- 参数
callback(可选):服务端完全关闭后执行的回调函数
示例:
typescript
// 10秒后关闭服务端
setTimeout(() => {
server.close(() => {
console.log('❌ 服务端已关闭,不再接收新连接');
});
}, 10000);
1.4 服务端常用事件监听 server.on('事件名', 回调)
和 DOM 的事件监听逻辑一致,当服务端发生对应事件时,会自动执行回调函数。
| 事件名 | 触发时机 | 常用场景 |
|---|---|---|
listening |
服务端成功调用 listen,开始监听端口时 | 打印服务端启动成功日志 |
connection |
有新的客户端连接成功时 | 和 createServer 的回调函数作用一致,管理新连接 |
close |
服务端完全关闭时 | 打印关闭日志,清理资源 |
error |
服务端发生错误时(最常见:端口被占用) | 捕获错误,提示用户更换端口 |
完整的服务端事件示例:
typescript
// 监听启动成功
server.on('listening', () => {
console.log('服务端开始监听端口');
});
// 监听新连接
server.on('connection', (socket) => {
console.log('有新客户端接入');
});
// 监听关闭
server.on('close', () => {
console.log('服务端已关闭');
});
// 监听错误(必加!否则端口被占用会直接崩溃)
server.on('error', (err: Error) => {
console.error('服务端出错:', err.message);
// 端口被占用时的提示
if (err.message.includes('EADDRINUSE')) {
console.error('端口3000已被占用,请更换端口或关闭占用程序');
}
});
2. Socket 核心 API(服务端 & 客户端通用)
socket 是网络通信的核心,无论是服务端接收到的连接,还是客户端创建的连接,都是 net.Socket 实例,所有数据收发、连接管理都通过它完成,API 完全通用。
2.1 数据收发 API
| API | 作用 | 参数说明 |
|---|---|---|
socket.write(data, [encoding], [callback]) |
向对方发送数据 | data:要发送的内容,支持字符串 / Buffer;encoding:编码,默认 utf8;callback:发送成功后的回调 |
socket.on('data', (data: Buffer) => {}) |
监听对方发来的数据 | 回调函数的参数 data 是 Buffer 类型,必须用 .toString() 转成字符串才能正常读取 |
socket.end([data], [encoding], [callback]) |
主动关闭当前连接 | 可选参数 data:关闭前先发送这段数据,再断开连接 |
核心示例:
typescript
// 监听对方发来的数据
socket.on('data', (data: Buffer) => {
// 把Buffer转成字符串
const message = data.toString();
console.log('收到对方消息:', message);
// 给对方回消息
socket.write(`我已收到你的消息:${message}`);
// 回复后,主动断开连接
socket.end();
});
2.2 连接状态 & 事件监听
socket 的所有状态变化,都通过事件监听来处理,这是 TCP 通信的核心逻辑。
| 事件名 | 触发时机 | 常用场景 |
|---|---|---|
connect |
连接成功建立时 | 客户端专用,连接成功后立刻给服务端发消息 |
data |
收到对方发来的数据时 | 核心,处理对方发送的内容 |
end |
对方主动关闭连接时 | 打印断开提示,清理资源 |
close |
连接完全关闭时 | 打印连接断开日志 |
error |
连接发生错误时 | 捕获错误,避免程序崩溃 |
timeout |
连接超时时 | 断开长时间无响应的连接,释放资源 |
2.3 常用属性
| 属性 | 含义 | 常用场景 |
|---|---|---|
socket.remoteAddress |
对方的 IP 地址 | 区分不同的客户端,打印日志 |
socket.remotePort |
对方的端口号 | 区分不同的客户端 |
socket.localAddress |
本地的 IP 地址 | 调试网络配置 |
socket.localPort |
本地的端口号 | 调试网络配置 |
socket.connected |
布尔值,当前连接是否处于活跃状态 | 发送数据前判断连接是否正常 |
3. 客户端核心 API
3.1 net.createConnection(options, [connectListener])
作用:创建一个 TCP 客户端实例,主动向服务端发起连接。
-
参数 1
options(必填):连接配置,核心参数如下
参数 含义 必填 默认值 port要连接的服务端端口号 是 无 host要连接的服务端 IP 地址 否 127.0.0.1(本机) -
参数 2
connectListener(可选) :回调函数,和服务端连接成功后,仅执行一次 -
返回值 :
net.Socket实例,也就是客户端的连接通道,后续所有数据收发都通过它完成
基础示例:
typescript
import * as net from 'net';
// 主动连接本机3000端口的服务端
const client = net.createConnection({ port: 3000, host: '127.0.0.1' }, () => {
// 连接成功后执行
console.log('✅ 成功连接到服务端!');
// 连接成功后,立刻给服务端发消息
client.write('你好服务端,我是客户端!');
});
3.2 客户端事件监听
客户端的事件监听和 socket 完全通用,核心常用事件如下:
typescript
// 监听服务端发来的消息
client.on('data', (data: Buffer) => {
console.log('📩 服务端回复:', data.toString());
// 收到回复后,1秒后主动断开连接
setTimeout(() => {
client.end();
}, 1000);
});
// 监听连接断开
client.on('close', () => {
console.log('👋 与服务端的连接已断开');
});
// 监听连接错误(必加!服务端没启动会触发)
client.on('error', (err: Error) => {
console.error('❌ 连接失败:', err.message);
if (err.message.includes('ECONNREFUSED')) {
console.error('请检查服务端是否已启动,端口号是否正确');
}
});
四、实战案例:从极简到进阶,写通每一行
案例 1:极简 TCP 回声服务(入门必写,10 行代码跑通)
回声服务的逻辑:服务端收到客户端的消息后,原封不动地回传给客户端,是网络编程最经典的入门案例。
1. 服务端代码 `src/echo-server.ts
typescript
import * as net from 'net';
// 创建服务端
const server = net.createServer((socket: net.Socket) => {
console.log(`客户端 ${socket.remoteAddress}:${socket.remotePort} 已连接`);
// 收到客户端消息,原封不动回传
socket.on('data', (data: Buffer) => {
const message = data.toString();
console.log(`收到消息:${message}`);
socket.write(`回声:${message}`);
});
// 客户端断开连接
socket.on('close', () => {
console.log(`客户端 ${socket.remoteAddress}:${socket.remotePort} 已断开`);
});
// 捕获连接错误
socket.on('error', (err) => {
console.error('客户端连接出错:', err.message);
});
});
// 监听3000端口
server.listen(3000, () => {
console.log('✅ 回声服务端已启动,监听3000端口');
});
// 捕获服务端错误
server.on('error', (err) => {
console.error('服务端出错:', err.message);
});
2. 客户端代码 `src/echo-client.ts
typescript
import * as net from 'net';
// 连接服务端
const client = net.createConnection({ port: 3000 }, () => {
console.log('✅ 已连接到回声服务端');
// 发送测试消息
client.write('Hello TypeScript 网络编程!');
});
// 接收服务端的回声
client.on('data', (data: Buffer) => {
console.log('📩 收到服务端回复:', data.toString());
// 收到回复后断开连接
client.end();
});
// 断开提示
client.on('close', () => {
console.log('👋 连接已断开');
});
// 错误处理
client.on('error', (err) => {
console.error('❌ 连接失败:', err.message);
});
3. 运行步骤(必看!)
-
先启动服务端
:新开一个终端,执行
bashtsc src/echo-server.ts && node dist/echo-server.js看到「服务端已启动」的提示,就说明服务端正常运行,
这个终端不要关!
-
再启动客户端
:再新开一个终端(必须新开,不能复用服务端的终端),执行
bashtsc src/echo-client.ts && node dist/echo-client.js -
观察两个终端的日志,就能看到完整的通信过程了。
案例 2:多人 TCP 聊天室(进阶实战,巩固核心能力)
学会了基础的一对一通信,我们来写一个支持多人同时在线的聊天室,核心逻辑是:服务端管理所有接入的客户端连接,收到某个客户端的消息后,广播给所有其他在线的客户端。
服务端代码 `src/chat-server.ts
typescript
import * as net from 'net';
// 核心:存储所有在线的客户端连接
const clientMap = new Map<string, net.Socket>();
// 创建服务端
const server = net.createServer((socket: net.Socket) => {
// 生成客户端唯一标识
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
console.log(`新用户加入聊天室:${clientId}`);
// 把新客户端加入map
clientMap.set(clientId, socket);
// 给所有在线用户广播新用户加入的消息
broadcast(`【系统消息】用户 ${clientId} 加入了聊天室`, clientId);
// 监听用户发来的消息
socket.on('data', (data: Buffer) => {
const message = data.toString().trim();
console.log(`${clientId} 说:${message}`);
// 把消息广播给所有其他用户
broadcast(`【${clientId}】:${message}`, clientId);
});
// 用户断开连接
socket.on('close', () => {
console.log(`用户 ${clientId} 离开了聊天室`);
// 从map中移除
clientMap.delete(clientId);
// 广播用户离开的消息
broadcast(`【系统消息】用户 ${clientId} 离开了聊天室`, clientId);
});
// 错误处理
socket.on('error', (err) => {
console.error(`用户 ${clientId} 连接出错:`, err.message);
clientMap.delete(clientId);
});
});
// 广播消息的工具函数
function broadcast(message: string, excludeClientId: string) {
// 遍历所有在线客户端,发送消息
for (const [clientId, socket] of clientMap) {
// 不给发送者自己发消息
if (clientId !== excludeClientId && socket.connected) {
socket.write(message);
}
}
}
// 启动服务端
server.listen(3000, () => {
console.log('✅ 多人聊天室服务端已启动,监听3000端口');
});
// 服务端错误处理
server.on('error', (err) => {
console.error('服务端出错:', err.message);
});
客户端代码 `src/chat-client.ts
typescript
import * as net from 'net';
import * as readline from 'readline';
// 创建终端输入接口,让用户能在终端输入消息
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// 连接聊天室服务端
const client = net.createConnection({ port: 3000 }, () => {
console.log('✅ 成功加入多人聊天室!');
console.log('请输入消息,按回车发送:');
// 监听用户在终端的输入
rl.on('line', (input) => {
// 把用户输入的内容发送给服务端
client.write(input.trim());
});
});
// 监听服务端广播的消息
client.on('data', (data: Buffer) => {
// 换行打印消息,避免和输入框重叠
console.log(`\n${data.toString()}`);
console.log('请输入消息,按回车发送:');
});
// 连接断开
client.on('close', () => {
console.log('\n👋 已退出聊天室');
rl.close();
process.exit(0);
});
// 错误处理
client.on('error', (err) => {
console.error('❌ 聊天室连接失败:', err.message);
rl.close();
process.exit(1);
});
运行效果
- 启动服务端
- 新开多个终端,分别启动多个客户端
- 在任意一个客户端输入消息,其他所有客户端都能收到,完美实现多人聊天室。
五、新手必踩的 10 个坑 & 避坑指南
我把 TS 网络编程入门阶段 99% 的新手会踩的坑都整理在这里,提前避坑,少走弯路。
1. 导入错误:Module "net" has no default export
- 原因 :用了
import net from 'net'的错误写法 - 解决 :必须用
import * as net from 'net'导入
2. 运行命令报错:标记"&&"不是此版本中的有效语句分隔符
- 原因 :Windows 旧版 PowerShell(5.x)不支持
&&语法 - 解决 :分两行执行命令,先
tsc 文件名.ts,再node 文件名.js;或者升级到 PowerShell 7+
3. 端口占用报错:EADDRINUSE :::3000
- 原因:3000 端口已经被其他程序占用,或者上一次的服务端没正常关闭
- 解决:
- 终端按
Ctrl+C关闭正在运行的服务端 - 更换代码里的端口号(比如 3001)
- Windows 执行
netstat -ano | findstr "3000"找到占用进程,杀掉即可
- 终端按
4. 客户端连接失败:ECONNREFUSED 127.0.0.1:3000
- 原因:服务端没启动,或者端口号、IP 地址填错了
- 解决:先确认服务端已经正常启动,端口号和客户端填写的完全一致,IP 地址正确
5. 终端复用导致客户端无法运行
- 原因:服务端启动后占住了终端,再在同一个终端运行客户端,自然无法执行
- 解决 :服务端和客户端必须在两个完全独立的终端里运行,不能复用同一个终端
6. 收到的消息是乱码,或者是 <Buffer 68 65 6c 6c 6f>
- 原因 :收到的
data是 Buffer 类型,没有转成字符串 - 解决 :用
data.toString()把 Buffer 转成 UTF8 字符串
7. 服务端重启后,客户端连不上
-
原因:TCP 连接有「TIME_WAIT」状态,服务端关闭后,端口会被临时占用几十秒
-
解决
:等待几十秒再重启,或者给服务端加上
reuseAddress配置:
typescriptconst server = net.createServer({ allowHalfOpen: false, pauseOnConnect: false }); server.on('error', (err) => { if (err.message.includes('EADDRINUSE')) { server.close(); server.listen(3000); } });
8. 多次发送消息,服务端一次收到了多条
- 原因:TCP 的「粘包问题」,TCP 是面向流的协议,会把多次小的数据包合并成一个大的发送
- 入门级解决 :给每条消息加结束符(比如换行符
\n),接收端按结束符拆分消息
9. 客户端断开后,服务端还在给它发消息,导致崩溃
- 原因:没有判断连接是否还处于活跃状态,给已经断开的 socket 执行 write 操作
- 解决 :发送消息前,先判断
socket.connected是否为 true,只有活跃的连接才能发消息
10. Code Runner 点运行,总是跑错文件
-
原因:Code Runner 默认复用同一个终端,且没有正确识别当前打开的文件
-
解决:
- 打开 VSCode 设置,勾选
Code Runner: Run In New Terminal(每次运行新开终端) - 勾选
Code Runner: Respect File Dir(识别当前打开的文件) - 把 Code Runner 的 TS 执行命令改成:
tsc $fileName && node $fileNameWithoutExt.js
- 打开 VSCode 设置,勾选
六、进阶学习路线
学完这篇博客的内容,你已经掌握了 TS 网络编程的核心能力,能独立写出完整的 TCP 通信程序。接下来可以按这个路线继续深入:
- UDP 网络编程 :用 Node.js 内置的
dgram模块,学习无连接的 UDP 通信,适合游戏、直播等实时场景 - HTTP/HTTPS 服务开发 :用 Node.js 内置的
http/https模块,手写 HTTP 接口服务,理解 Web 请求的本质 - WebSocket 通信:学习双向实时通信,适合 IM 聊天、实时看板等场景
- 计算机网络基础:深入学习 TCP/IP 协议、三次握手 / 四次挥手、OSI 七层模型,理解底层原理
- 高并发网络模型:学习 Node.js 的事件循环、异步 IO、Reactor 模型,理解高性能网络服务的底层逻辑
很多人觉得网络编程门槛很高,必须学 C++/Java 才能入门,但其实 TypeScript + Node.js 给我们提供了一个极低门槛的入门方式,不用折腾复杂的环境,不用被晦涩的语法劝退,先写通核心逻辑,再逐步深入底层原理。
网络编程的核心从来不是语言,而是「通信逻辑」和「网络协议」,你用 TS 掌握了这些核心能力,未来无论切换到什么语言,都能快速上手。
希望这篇博客能帮你敲开网络编程的大门,少走弯路,真正享受编程的乐趣。