网络通信基本原理

网络通信方式

  • 交换机
  • 路由器

网络层次模型

OSI 七层模型

  • 应用层: 用户与网络的接口
  • 表示层: 数据加密,转换,压缩
  • 会话层: 控制网络连接建立与中止
  • 传输层: 控制数据传输可靠性
  • 网络层: 确定目标网络
  • 数据链路层: 确定目标主机
  • 物理层: 各种网络屋里设备

TCP 四层模型

数据封装与解封装

TCP 三次握手与四次挥手

TCP 协议

  • TCP 属于传输层协议
  • TCP 是面向连接的协议
  • TCP 用于处理实时通信

本质上请求连接或请求断开都是四次连接,但是请求断开中服务端请求不能合并成一条.

常见控制字段

  • SYN=1: 请求建立连接
  • FIN=1: 请求断开连接
  • ACK=1: 数据信息的确认

本来应该是两次一来一回,共四次,但是服务端这里可以将请求和确认合并发送,就是三次握手连接.
image.png
断开连接:
变成四次是因为确保服务端已经将数据全部传输完毕,所以不能进行合并.

创建 TCP 通信

通信过程

  • 创建服务端: 接收和诙谐客户端数据
  • 创建客户端: 发送接收服务端数据
  • 数据传输: 内置服务事件和方法读写数据

通信事件

  • listening 事件: 调用 server.listen 方法后触发
  • connection 事件: 新的连接建立时触发
  • close 事件: 当 server 关闭时触发
  • error 事件: 当错误出现时触发

通信事件&方法

  • data 事件: 当接收到数据时触发
  • write 方法: 在 socket 上发送数据,默认 UTF-8
  • end 操作: 当 socket 的一端发送 FIN 包时触发,结束可读端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// server.js
const net = require("net");

// 创建服务端实例
const server = net.createServer();
const PORT = 1234;
const HOST = "localhost";

server.listen(PORT, HOST);

//调用listen后触发listening事件
server.on("listening", () => {
console.log(`服务端已开启在 ${HOST}: ${PORT}`);
});

// 接收消息 回写消息
server.on("connection", (socket) => {
socket.on("data", (chunk) => {
const msg = chunk.toString();
console.log(msg);

//回数据
socket.write(Buffer.from("您好" + msg));
});
});

server.on("close", () => {
console.log("服务端关闭");
});
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
console.log("地址占用");
} else {
console.log(err);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// client.js
const net = require("net");

const client = net.createConnection({
port: 1234,
host: "127.0.0.1",
});

client.on("connect", () => {
client.write("edu");
});

client.on("data", (chunk) => {
console.log(chunk.toString());
});

client.on("close", () => {
console.log("客户端断开连接");
});
client.on("error", (err) => {
console.log(err);
});

TCP 数据粘包

发送端和接收端都是等待缓冲数据之后再消费.
出现问题的原因: 发送间隔太短导致数据堆积.

数据的封包和拆包

将消息头拆分为序列号和消息长度

数据传输过程

  • 进行数据编码,获取二进制数据包
  • 按规则拆解数据,获取指定长度数据

Buffer 数据读写

  • writeInt16BE: 将 value 从指定位置写入
  • readInt16BE: 从指定位置开始读取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 编码解码
class myTransform {
constructor() {
// 固定header总长度4字节
this.packageHeaderLen = 4;
// 当前包的编号
this.serialNum = 0;
this.serialLen = 2;
}

// 编码
encode(data, serialNum) {
//把传入数据变为二进制
const body = Buffer.from(data);
// 01 先按照指定长度申请内存空间作为header使用
const headerBuf = Buffer.alloc(this.packageHeaderLen);
// 02 写操作
headerBuf.writeInt16BE(serialNum || this.serialNum);
// 数据的长度作为消息体的总长度写入另一空间
headerBuf.writeInt16BE(body.length, this.serialLen); // 跳过前面占用的位置
// 设置编号
if (serialNum === undefined) {
this.serialNum++;
}
return Buffer.concat(headerBuf, body);
}

// 解码
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;
} else {
return this.packageHeaderLen + buffer.readInt16BE(this.serialLen);
}
}
}

module.exports = myTransform;

数据粘包解决

利用上面的编解码工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// server.js
const myTransform = require("./myTransform.js");

let overageBuffer = null;
let ts = new myTransform();
// ...
// 接收消息 回写消息
server.on("connection", (socket) => {
socket.on("data", (chunk) => {
// 如果有未使用完的buffer
if (overageBuffer) {
//将数据拼接
chunk = Buffer.concat([overageBuffer, chunk]);
}

let packageLen = 0;
// 如果数据为0,则说明不足以读取,否则就是可读取
while ((packageLen = ts.getPackageLen(chunk))) {
// 把包的内容截取出来
const packageCon = chunk.slice(0, packageLen);
// 更新chunk,将已读取的截掉
chunk = chunk.slice(packageLen);

const ret = ts.decode(packageCon);
//回数据
socket.write(ret.body, ret.serialNum);
}
// 将剩余的chunk交给overageBuffer下一次处理
overageBuffer = chunk;
});
});

Http 协议

获取 http 请求信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const http = require("http");
const url = require("url");

const server = http.createServer((req, res) => {
const { pathname, query } = url.parse(req.url, true);
// 请求方式
console.log(req.method);
// http版本号
console.log(req.httpVersion);
// 请求头
console.log(req.headers);
// 请求体数据获取
let arr = [];
// req是个可读流
req.on("data", (data) => {
arr.push(data);
});

req.on("end", () => {
console.log(Buffer.concat(arr).toString());
});
});

server.listen(1234, () => {
console.log("server start ...");
});

设置 http 响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const http = require("http");
const url = require("url");

const server = http.createServer((req, res) => {
// res就是可写流
// res.write('ok')
// 使用end结束写入操作,或者直接在里面写入
// res.end()

// 设置响应头
res.statusCode = 200;
res.setHeader("Content-type", "text/html;charset=utf-8");
res.end("你好");
});

server.listen(1234, () => {
console.log("server start ...");
});

客户端代理

相当于服务端,客户端与不同端口域名协议的服务端通信有跨域,那就采用客户端代理,它与服务端就不存在跨域.