想作一个简单的 Web API,这个时候就须要搭建一个 Web 服务器,在 ASP.NET 中须要 IIS 来搭建服务器,PHP 中须要借助 Apache/Nginx 来实现,对于新手在还没开始以前看到这么多步骤,也许就要放弃了,可是在 Node.js 中开启一个 Web 服务器是 So Easy 的,咱们利用 Net、Dgram、HTTP、HTTPS 等模块经过几行简单的代码就可实现。html
大多数同窗对于 HTTP、HTTPS 会很熟悉,一般用于浏览器与服务端交互,或者服务端与服务端的交互,另外两个 Net 与 Dgram 也许会相对陌生,这两个是基于网络模型的传输层来实现的,分别对应于 TCP、UDP 协议,下面一图看明白 OSI 七层模型 与 TCP/IP 五层模型之间的关系,中间使用虚线标注了传输层,对于上层应用层(HTTP/HTTPS等)也都是基于这一层的 TCP 协议来实现的,因此想使用 Node.js 作服务端开发,Net 模块也是你必需要掌握的,这也是咱们本篇要讲解的重点。node
Interview1: 有些概念仍是要弄清楚的,什么是 TCP 协议?什么状况下又会选择 TCP 协议呢?git
TCP 是传输控制协议,大多数状况下咱们都会使用这个协议,由于它是一个更可靠的数据传输协议,具备以下三个特色:github
上面三个特色说到 TCP 是面向连接和可靠的,其一个显著特征是在传输以前会有一个 3 次握手,实现过程以下所示:面试
在一次 TCP 三次握手的过程当中,客户端与服务端会分别提供一个套接字来造成一个连接。以后客户端与服务端经过这个连接来互相发送数据。算法
以上了解了 TCP 的一些概念以后,咱们开始建立一个 TCP 服务端与客户端实例,这里咱们须要使用 Node.js 的 Net 模块,它提供了一些用于底层通讯的接口,该模块能够用于建立基于流的 TCP 或 IPC 的服务器(net.createServer())与客户端(net.createConnection())。api
可使用 new net.Server 建立一个 TCP 服务端连接,也能够经过工厂函数 net.createServer() 的方式,createServer() 的内部实现也是内部调用了 Server 构造函数来建立一个 TCP 对象,和 new net.Server 是同样的,代码以下所示:浏览器
function createServer(options, connectionListener) {
return new Server(options, connectionListener);
}
复制代码
function Server(options, connectionListener) {
if (!(this instanceof Server))
return new Server(options, connectionListener);
// Server 类内部仍是继承了 EventEmitter,这个不在本节范围
EventEmitter.call(this);
...
复制代码
在开始代码以前,先了解下其相关事件,参考官网 nodejs.cn/api/net.htm…,这里也不会把全部的都介绍,下面介绍一些经常使用的,而且经过代码示例,进行讲解,能够在这个基础之上在去参考官网,实践一些其它的事件或方法。
TCP 服务器事件
TCP 连接事件方法
const net = require('net');
const HOST = '127.0.0.1';
const PORT = 3000;
// 建立一个 TCP 服务实例
const server = net.createServer();
// 监听端口
server.listen(PORT, HOST);
server.on('listening', () => {
console.log(`服务已开启在 ${HOST}:${PORT}`);
});
server.on('connection', socket => {
// data 事件就是读取数据
socket.on('data', buffer => {
const msg = buffer.toString();
console.log(msg);
// write 方法写入数据,发回给客户端
socket.write(Buffer.from('你好 ' + msg));
});
})
server.on('close', () => {
console.log('Server Close!');
});
server.on('error', err => {
if (err.code === 'EADDRINUSE') {
console.log('地址正被使用,重试中...');
setTimeout(() => {
server.close();
server.listen(PORT, HOST);
}, 1000);
} else {
console.error('服务器异常:', err);
}
});
复制代码
const net = require('net');
const client = net.createConnection({
host: '127.0.0.1',
port: 3000
});
client.on('connect', () => {
// 向服务器发送数据
client.write('Nodejs 技术栈');
setTimeout(() => {
client.write('JavaScript ');
client.write('TypeScript ');
client.write('Python ');
client.write('Java ');
client.write('C ');
client.write('PHP ');
client.write('ASP.NET ');
}, 1000);
})
client.on('data', buffer => {
console.log(buffer.toString());
});
// 例如监听一个未开启的端口就会报 ECONNREFUSED 错误
client.on('error', err => {
console.error('服务器异常:', err);
});
client.on('close', err => {
console.log('客户端连接断开!', err);
});
复制代码
源码实现地址
https://github.com/Q-Angelo/project-training/tree/master/nodejs/net/chapter-1-client-server
复制代码
首先启动服务端,以后在启动客户端,客户端调用三次,打印结果以下所示:
服务端
$ node server.js
服务已开启在 127.0.0.1:3000
# 第一次
Nodejs 技术栈
JavaScript
TypeScript Python Java C PHP ASP.NET
# 第二次
Nodejs 技术栈
JavaScript TypeScript Python Java C PHP ASP.NET
复制代码
客户端
$ node client.js
# 第一次
你好 Nodejs 技术栈
你好 JavaScript
你好 TypeScript Python Java C PHP ASP.NET
# 第二次
你好 Nodejs 技术栈
你好 JavaScript TypeScript Python Java C PHP ASP.NET
复制代码
在客户端我使用 client.write() 发送了屡次数据,可是只有 setTimeout 以外的是正常的,setTimeout 里面连续发送的彷佛并非每一次一返回,而是会随机合并返回了,为何呢?且看下面 TCP 的粘包问题介绍。
Interview2: TCP 粘包是什么?该怎么解决?
上面的例子最后抛出了一个问题,为何客户端连续向服务端发送数据,会收到合并返回呢?这也是在 TCP 中常见的粘包问题,客户端(发送的一端)在发送以前会将短期有多个发送的数据块缓冲到一块儿(发送端缓冲区),造成一个大的数据块一并发送,一样接收端也有一个接收端缓冲区,收到的数据先存放接收端缓冲区,而后程序从这里读取部分数据进行消费,这样作也是为了减小 I/O 消耗达到性能优化。
问题思考:数据到达缓冲区什么时间开始发送?
这个取决于 TCP 拥塞控制,是任什么时候刻内肯定能被发送出去的字节数的控制因素之一,是阻止发送方至接收方之间的链路变得拥塞的手段,参考维基百科:zh.wikipedia.org/wiki/TCP拥塞控…
TCP 粘包解决方案?
一种最简单的方案是设置延迟发送,sleep 休眠一段时间的方式,可是这个方案虽然简单,同时缺点也显而易见,传输效率大大下降,对于交互频繁的场景显然是不适用的,第一次改造以下:
client.on('connect', () => {
client.setNoDelay(true);
// 向服务器发送数据
client.write('Nodejs 技术栈');
const arr = [
'JavaScript ',
'TypeScript ',
'Python ',
'Java ',
'C ',
'PHP ',
'ASP.NET '
]
for (let i=0; i<arr.length; i++) {
(function(val, k){
setTimeout(() => {
client.write(val);
}, 1000 * (k+1))
}(arr[i], i));
}
})
复制代码
控制台执行 node client.js 命令,彷佛一切 ok 了没有在出现粘包的状况,可是这种状况仅使用于交互频率很低的场景。
$ node client.js
你好 Nodejs 技术栈
你好 JavaScript
你好 TypeScript
你好 Python
你好 Java
你好 C
你好 PHP
你好 ASP.NET
复制代码
源码实现地址
https://github.com/Q-Angelo/project-training/tree/master/nodejs/net/chapter-2-delay
复制代码
Nagle 算法是一种改善网络传输效率的算法,避免网络中充斥着大量小的数据块,它所指望的是尽量发送大的数据块,所以在每次请求一个数据块给 TCP 发送时,TCP 并不会当即执行发送,而是等待一小段时间进行发送。
当网络中充斥着大量小数据块时,Nagle 算法能将小的数据块集合起来一块儿发送减小了网络拥堵,这个仍是颇有帮助的,但也并非全部场景都须要这样,例如,REPL 终端交互,当用户输入单个字符以获取响应,因此在 Node.js 中能够设置 socket.setNoDelay() 方法来关闭 Nagle 算法。
const server = net.createServer();
server.on('connection', socket => {
socket.setNoDelay(true);
})
复制代码
关闭 Nagle 算法并不老是有效的,由于其是在服务端完成合并,TCP 接收到数据会先存放于本身的缓冲区中,而后通知应用接收,应用层由于网络或其它的缘由若不能及时从 TCP 缓冲区中取出数据,也会形成 TCP 缓冲区中存放多段数据块,就又会造成粘包。
前面两种方案都不是特别理想的,这里介绍第三种封包/拆包,也是目前业界用的比较多的,这里使用长度编码的方式,通讯双方约定好格式,将消息分为定长的消息头(Header)和不定长的消息体(Body),在解析时读取消息头获取到内容占用的长度,以后读取到的消息体内容字节数等于字节头的字节数时,咱们认为它是一个完整的包。
消息头序号 (Header) | 消息体长度 (Header) | 消息体 (Body) |
---|---|---|
SerialNumber | bodyLength | body |
2(字节) | 2(字节) | N(字节) |
下面会经过编码实现,可是在开始以前但愿你能了解一下 Buffer,可参考我以前写的 Buffer 文章 Node.js 中的缓冲区(Buffer)到底是什么?,下面我列出本次须要用到的 Buffer 作下说明,对于不了解 Buffer 的同窗是有帮助的。
TCP 底层传输是基于二进制数据,可是咱们应用层一般是易于表达的字符串、数字等,这里第一步在编码的实现中,就须要先将咱们的数据经过 Buffer 转为二进制数据,取出的时候一样也须要解码操做,一切尽在代码里,实现以下:
// transcoder.js
class Transcoder {
constructor () {
this.packageHeaderLen = 4; // 包头长度
this.serialNumber = 0; // 定义包序号
this.packageSerialNumberLen = 2; // 包序列号所占用的字节
}
/** * 编码 * @param { Object } data Buffer 对象数据 * @param { Int } serialNumber 包序号,客户端编码时自动生成,服务端解码以后在编码时须要传入解码的包序列号 */
encode(data, serialNumber) {
const body = Buffer.from(data);
const header = Buffer.alloc(this.packageHeaderLen);
header.writeInt16BE(serialNumber || this.serialNumber);
header.writeInt16BE(body.length, this.packageSerialNumberLen); // 跳过包序列号的前两位
if (serialNumber === undefined) {
this.serialNumber++;
}
return Buffer.concat([header, body]);
}
/** * 解码 * @param { Object } buffer */
decode(buffer) {
const header = buffer.slice(0, this.packageHeaderLen); // 获取包头
const body = buffer.slice(this.packageHeaderLen); // 获取包尾部
return {
serialNumber: header.readInt16BE(),
bodyLength: header.readInt16BE(this.packageSerialNumberLen), // 由于编码阶段写入时跳过了前两位,解码一样也要跳过
body: body.toString(),
}
}
/** * 获取包长度两种状况: * 1. 若是当前 buffer 长度数据小于包头,确定不是一个完整的数据包,所以直接返回 0 不作处理(可能数据还未接收完等等) * 2. 不然返回这个完整的数据包长度 * @param {*} buffer */
getPackageLength(buffer) {
if (buffer.length < this.packageHeaderLen) {
return 0;
}
return this.packageHeaderLen + buffer.readInt16BE(this.packageSerialNumberLen);
}
}
module.exports = Transcoder;
复制代码
const net = require('net');
const Transcoder = require('./transcoder');
const transcoder = new Transcoder();
const client = net.createConnection({
host: '127.0.0.1',
port: 3000
});
let overageBuffer=null; // 上一次 Buffer 剩余数据
client.on('data', buffer => {
if (overageBuffer) {
buffer = Buffer.concat([overageBuffer, buffer]);
}
let packageLength = 0;
while (packageLength = transcoder.getPackageLength(buffer)) {
const package = buffer.slice(0, packageLength); // 取出整个数据包
buffer = buffer.slice(packageLength); // 删除已经取出的数据包,这里采用的方法是把缓冲区(buffer)已取出的包给截取掉
const result = transcoder.decode(package); // 解码
console.log(result);
}
overageBuffer=buffer; // 记录剩余不完整的包
}).on('error', err => { // 例如监听一个未开启的端口就会报 ECONNREFUSED 错误
console.error('服务器异常:', err);
}).on('close', err => {
console.log('客户端连接断开!', err);
});
client.write(transcoder.encode('0 Nodejs 技术栈'));
const arr = [
'1 JavaScript ',
'2 TypeScript ',
'3 Python ',
'4 Java ',
'5 C ',
'6 PHP ',
'7 ASP.NET '
]
setTimeout(function() {
for (let i=0; i<arr.length; i++) {
console.log(arr[i]);
client.write(transcoder.encode(arr[i]));
}
}, 1000);
复制代码
const net = require('net');
const Transcoder = require('./transcoder');
const transcoder = new Transcoder();
const HOST = '127.0.0.1';
const PORT = 3000;
let overageBuffer=null; // 上一次 Buffer 剩余数据
const server = net.createServer();
server.listen(PORT, HOST);
server.on('listening', () => {
console.log(`服务已开启在 ${HOST}:${PORT}`);
}).on('connection', socket => {
// data 事件就是读取数据
socket
.on('data', buffer => {
if (overageBuffer) {
buffer = Buffer.concat([overageBuffer, buffer]);
}
let packageLength = 0;
while (packageLength = transcoder.getPackageLength(buffer)) {
const package = buffer.slice(0, packageLength); // 取出整个数据包
buffer = buffer.slice(packageLength); // 删除已经取出的数据包,这里采用的方法是把缓冲区(buffer)已取出的包给截取掉
const result = transcoder.decode(package); // 解码
console.log(result);
socket.write(transcoder.encode(result.body, result.serialNumber));
}
overageBuffer=buffer; // 记录剩余不完整的包
})
.on('end', function(){
console.log('socket end')
})
.on('error',function(error){
console.log('socket error', error);
});
}).on('close', () => {
console.log('Server Close!');
}).on('error', err => {
if (err.code === 'EADDRINUSE') {
console.log('地址正被使用,重试中...');
setTimeout(() => {
server.close();
server.listen(PORT, HOST);
}, 1000);
} else {
console.error('服务器异常:', err);
}
});
复制代码
控制台执行 node server.js 开启服务端,以后执行 node client.js 开启客户端测试,输出结果以下所示:
$ node client.js
{ serialNumber: 0, bodyLength: 18, body: '0 Nodejs 技术栈' }
1 JavaScript
2 TypeScript
3 Python
4 Java
5 C
6 PHP
7 ASP.NET
{ serialNumber: 1, bodyLength: 13, body: '1 JavaScript ' }
{ serialNumber: 2, bodyLength: 13, body: '2 TypeScript ' }
{ serialNumber: 3, bodyLength: 9, body: '3 Python ' }
{ serialNumber: 4, bodyLength: 7, body: '4 Java ' }
{ serialNumber: 5, bodyLength: 4, body: '5 C ' }
{ serialNumber: 6, bodyLength: 6, body: '6 PHP ' }
{ serialNumber: 7, bodyLength: 10, body: '7 ASP.NET ' }
复制代码
以上结果中,setTimeout 函数里咱们同一时间先发送多条数据,以后一一返回,同时打印了包消息头定义的包序列号、消息体长度和包消息体,且是一一对应的,上面提的粘包问题也获得了解决。封包/拆包这块是有点复杂的,以上代码也已经尽量简单的介绍了实现思路,下面给出实现代码地址,能够作为参照本身也可使用不一样的方式去实现
https://github.com/Q-Angelo/project-training/tree/master/nodejs/net/chapter-3-package
复制代码
做者:五月君
连接:https://github.com/Q-Angelo/Nodejs-Roadmap
来源:Nodejs.js技术栈
复制代码