TCP 通信
创建 TCP 通信
net 模块实现了底层通信接口
通信过程
- 创建服务端: 接收和回写客户端数据
- 创建客户端: 发送和接收服务端数据
- 数据传输: 内置服务事件和方法读写数据
通信事件 & 方法
- listening 事件:调用 server.listen 方法之后触发
- connection 事件:新的连接建立时触发
- close 事件:当 server 关闭时触发
- error 事件:当错误出现时触发
- data 事件:当接受到数据的时候触发
- write 方法:在 socket 上发送数据,默认是 UTF8 编码
- end 操作:当 socket 的一段发送 FIN 包时触发,结束可读端
// server.js
const net = require("net");
// 创建服务端实例
const server = net.createServer();
const PORT = 3306,
HOST = "localhost";
// 开启服务
server.listen(PORT, HOST);
// 订阅事件
server.on("listening", () => {
console.log(`服务端已经开启在 ${HOST}:${PORT}`);
});
// 接收消息 回写消息
server.on("connection", (socket) => {
// socket 就是 netSocket 的实例
socket.on("data", (chunk) => {
const msg = chunk.toString();
console.log(msg);
// 回数据
socket.write(Buffer.from(`hello ${msg}`));
});
});
server.on("close", () => {
console.log("服务端关闭了");
});
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
console.log("地址正在被使用");
} else {
console.log(err);
}
});
// client.js
const net = require("net");
// 创建客户端实例
const client = net.createConnection({ port: 3306, host: "localhost" });
const dataArr = ["已连接1", "已连接2", "已连接3", "已连接4"];
client.on("connect", () => {
client.write("已连接0");
});
client.on("data", (chunk) => {
console.log(chunk.toString());
});
client.on("error", (err) => {
console.log(err);
});
client.on("close", () => {
console.log("客户端断开连接");
});
☞ 完整代码案例
TCP 粘包及解决
TCP 数据粘包
通信包含数据发送端和接收端,发送端在工作的时候会累积数据进行统一发送,接收端也会缓存数据之后再消费。
这样的处理好处是可以减少 IO 操作带来的性能消耗,缺点是对于数据的使用会出现粘包的问题。
在上述的过程中还存在一个问题:数据是存在缓存中的,TCP 拥塞机制决定了数据的发送时机。
如上图所示,为数据粘包的体现,我们这里期望的结果是得到四次回复而不是一次。
解决方案
- 拉长发送时间间隔【不建议】
- 通过封包拆包处理(核心思想:按照约定好的自定义规则将数据打包,在使用数据的时候按照规则进行拆包) 不会影响数据发送的效率【建议使用】
数据的封包与拆包
使用长度编码的方式约定通信双方的数据传输方式。
将被传输的消息分为定长的消息头(header) 和不定长的消息体(body),同时将头部分为序列号和消息长度两个部分。
为了区分不同的消息包,在消息头中拆出”序列号“来存放编号,为了确定每次取多少长度的内容,拆出来定义每条数据长度的”消息长度“。
数据传输过程
- 进行数据编码,将数据转换为二进制(在转换的过程中按照上述的规则将数据进行封装)
- 数据接收方获取到数据后,按规则拆解数据,获取指定长度的数据(将二进制解码为字符串)
Buffer 数据读写
- writeInt16BE: 将 value 从指定位置写入(往内存中写入数据)
- readInt16BE: 从指定位置开始读取数据(从内存中读取数据)
备注
-
为什么要将数据进行打包?
因为这样就可以避免客户端连续多次发送数据时产生粘包现象。
-
自定义包的约束规则?
在 body 的前面拼上 header,在 header 里面去定义下当前包的编号以及当前想要读取包的内容长度,最后就可以自定义规则来实现数据的编码和解码
class TransformCode {
constructor() {
this.packageHeaderLen = 4; // header 总长度 4个字节
this.serialNum = 0; // 序列号
this.serialLen = 2; // 消息长度
}
/**
* 编码
* @param {*} data 要编码的数据
* @param {*} serialNum 数据包的编号
*/
encode(data, serialNum) {
// 将数据变为二进制
const body = Buffer.from(data);
/* 封装header */
// 1 按照指定的长度申请一片内存空间
const headerBuf = Buffer.alloc(this.packageHeaderLen);
// 2 将序列号写入
headerBuf.writeInt16BE(serialNum || this.serialNum);
// 将数据总长度写入 需要跳过序列号
headerBuf.writeInt16BE(body.length, this.serialLen);
if (serialNum === undefined) {
this.serialNum++;
}
return Buffer.concat([headerBuf, body]);
}
/**
* 解码
* @param {*} buffer 要解码的数据
*/
decode(buffer) {
const headerBuf = buffer.slice(0, this.packageHeaderLen);
const bodyBuf = buffer.slice(this.packageHeaderLen);
return {
serialNum: headerBuf.readInt16BE(),
bodyLength: headerBuf.readInt16BE(this.serialLen),
body: bodyBuf.toString(),
};
}
// 获取包长度
getPackageLen(buffer) {
if (buffer.length < this.packageHeaderLen) return 0;
return this.packageHeaderLen + buffer.readInt16BE(this.serialLen);
}
}
module.exports = TransformCode;