跳到主要内容

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 拥塞机制决定了数据的发送时机。

TCP粘包的体现

如上图所示,为数据粘包的体现,我们这里期望的结果是得到四次回复而不是一次。

解决方案

  1. 拉长发送时间间隔【不建议】
  2. 通过封包拆包处理(核心思想:按照约定好的自定义规则将数据打包,在使用数据的时候按照规则进行拆包) 不会影响数据发送的效率【建议使用】

数据的封包与拆包

使用长度编码的方式约定通信双方的数据传输方式。

将被传输的消息分为定长的消息头(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;

完整 TCP 粘包数据封包拆包解决方案实现