协议
协议就是数据封装格式+传输 , 数据的发送是一直封装数据,数据接受方一直都是解封
OSI 七层
- 应用层,包括以下3个,
- 应用层 网络服务和客户的接口,如浏览器
- 表示层 转换(html,json)和表示(http, sftp)数据,如压缩
- 会话层 建立,管理,终止会话
- 传输层 网络层数据容易丢,在网络层上建立的 TCP和UDP协议
- 网络层 进行逻辑地址寻址 IP ARP 等 传输的数据叫数据包
- 网络接口层,底层发送数据的,包括以下2个,
- 数据链路层,就是拉货的,进行硬件地址寻址,帧是传输单位,大的数据拆分为小数据(帧),流量控制
- 理 物理层,相当于路 ,关心用什么表示0和1
TCP/IP
是很多协议的集合
- IP负责网络主机寻址和数据路由包
- ARP协议,获得同一物理网路中的mac地址
- RARP协议,就是反ARP协议, 通过mac地址找IP地址
- ICMP 数据通信,用来发送数据
IP
- 每个全球IP地址都可以拓展到250多个局域网地址
- 上网的时候,服务器拿到的客户端的IP是路由器的公网IP,而私有IP是拿不到的
- 要实现局域网内部的主机与外网之间的通信,要做地址转换的工作
- 主机发送的数据包的目的IP地址就是该服务器的IP,源IP地址是主机的局域网IP地址(如192,168.1.2),数据包经过路由器,路由器将数据包的源IP地址改为路由器的全球IP地址(200.24.5.8),再将数据包发送给服务器
- 主机发送给服务器时,与路由器建立一个端口地址,路由器会将端口的地址发给服务器
- 网关就是一个网络连接另一个网络的“关口”, 网关IP是终端访问外网的“第一关”
作者:Gen_
链接:https://www.jianshu.com/p/26e0bbb64b4e
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
通信方式
- 单工 单向工作
- 双工 双向工作
- 半双工 同一时间只能一个方向
选址
交换机是靠mac地址工作的
mac地址
mac地址在内嵌在网卡,全球唯一,6个字节,前3个表示厂商 ,交换机是找IP地址
路由
路由就是在线路中选择一条最短的 。路由器是转发IP地址的,交换机属于数据链路层
IP地址
- 127 开头的代表本机
- 255.255.255.255 代表向所有网络主机广播
- A 类(0 + 7位为表示网络)、B类( 10 + 14位表示网络)、C(110 + 21位)类地址
- TTL 最大跳步数,超过了几个路由器,数据就无效了
子网掩码
- 就是划分小网络用的
- 不可能每个用户都有一个独立的IP地址,子网掩码必须和IP一起用,分为主机部分和网络部分
- 由32个二进制构成
- 网络位用1,主机位用0
- IP和子网掩码进行 按位与 运算,得到的就是网络位 ,网络位相同就在一个局域网
- 192.168.0.9/24 24是子网掩码
TCP传输层
- 发送数据之前,要先连接 ,3次握手,保证双方发送接受都是正常的
- chrome每个tab页面都是一个进程,每个进程监听不同的端口号
- 传输层的数据叫数据段 segment
- 数据分段打包传输,控制顺序,流量控制
- TCP 源端口 到 目标端口号,端口号最大值65535
- 32位序列号(随机产生),tcp用序列号对数据包进行标记,
- 一方收到一方的数据,会回应一个ack 表示数据收到 ,如果没回ack,就会重新发
- 通信一方能接受数据多少,由自己的窗口大小觉得决定
- 通信一方会校验,数据和校验和(加密串)一样的话,会发回收到确认。否则不发确认
3次握手 4次挥手
- 3次握手过程
- 主机A发送 syn请求连接,并告诉主机B传输的起始序列号 seq = x
- 主机B用一个确认 ack= x + 1 , 并通知主机A发送的起始序列号 seq = y
- 主机A 收到了确认数据ack = y + 1
- 4次挥手, 如果服务端没有数据返回了,2,3可以合并为一次,把FIN置位1,
- 客户端发FIN为1,表示要断开
- 服务端把ack剩下数据发出去
- 服务端发一个FIN是1
- 客户端断开了
const net = require("net");
// 创建基于流的 TCP 可以进程间通信的服务器和客户端
const server = net.createServer(function (socket) {
// 最大连接2个客户端
// server.maxConnections = 2;
server.getConnections((err, count) => {
console.log("hello", count)
})
socket.on("data", function (data) {
console.log(data.toString())
});
socket.on("error", function (data) {
console.log('error')
});
// 客户端开始关闭
socket.on("end", function (data) {
console.log("client end")
// 当所有的客户端断开连接后,服务器停止服务
server.unref();
});
// 客户端关闭
socket.on("close", function (data) {
console.log("client close")
})
// server close 方法 不在接受新的连接 旧的连接可以继续服务
setTimeout(() => {
server.close();
})
});
server.listen(8989, function () {
console.log("server started")
})
server.on("close", function () {
console.log("server close")
})
server.on("error", function () {
console.log("server error")
})
UDP
传输效率高,速度快,不可靠的,无连接的协议
编码
// 一个 byte 是 由 8个bit组成, 可表示256种状态
let a = 0b10100;//二进制
let b = 0o24;//八进制
let c = 20;//十进制
let d = 0x14;//十六进制
console.log(a == b);
console.log(b == c);
console.log(c == d);
// 转8进制
console.log(c.toString(8))
console.log(c.toString(2))
// 把输入按照进制转换成10进制的数字
console.log(parseInt('10100', 2));
// ASCII 码 第一位是0 ,其余7位表示不同的字符
// gb2312 如果小于127还是原来的,大于127 ,两个字节表示一个汉字
// Unicode (16进制) 保持其原编码不变,只是将其长度由原来的 8 位扩展为16 位
// UTF-8 (2进制) 就是在互联网上使用最广的一种 Unicode 的实现方式 utf8变长,英文一个字节,汉字3个
// 汉字3个字节 ['1110xxxx', '10xxxxxx', '10xxxxxx']
nodejs特点
- nodejs主线程是单线程,但是libuv库是多线程
- nodejs 是事件驱动,每个操作都被当做事件处理
- Node.js 并不适合 CPU 密集型任务
- node没有锁的问题
浏览器模型
- 用户界面,包括地址栏、前进/后退按钮、书签菜单等
- 浏览器引擎,在用户界面和呈现引擎之间传送指令
- 浏览器内核(渲染引擎),在线程方面又称为UI线程
浏览器内核
- GUI 渲染线程 负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程,和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行
- JS 引擎线程:单线程工作,负责解析运行 JavaScript 脚本。和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
- 事件触发线程:当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理
- 定时器触发线程:浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理
- http 请求线程:http 请求的时候会开启一条请求线程。请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
Js引擎是单线程
- Js引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。
- HTML5的Web-Worker API,主要是为了解决页面阻塞问题,但没改变 JavaScript 是单线程的本质
- Web-Worker的特点是1. 完全受主线程控制 2. 不可以操作dom
浏览器js事件循环
- js有主线程和 call-stack调用栈
- call-stack 后进先出
- 同步任务会在调用栈中按照顺序排队等待主线程执行,
- 异步任务在异步有了结果后将注册的回调函数添加到任务队列中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构
- event loop 调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。
nodejs 四层
- 应用层: 即 JavaScript 交互层,常见的就是 Node.js 的模块,比如 http,fs
- V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互
- NodeAPI层: 为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互 。
- LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的核心
- Node.js 内部都是通过 线程池 来完成异步 I/O 操作的,而 LIBUV 针对不同平台的差异性实现了统一调用。因此,Node.js 的单线程仅仅是指 JavaScript 运行在单线程中,而并非 Node.js 是单线程
nodejs工作过程
libuv内部是多线程的线程池和同步IO模拟一个异步的
- V8引擎解析JavaScript脚本。
- 解析后的代码,调用Node API。
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
- V8引擎再将结果返回给用户
nodejs事件循环(基于libuv 实现 )
- Node.js 在主线程里维护一个事件队列,当接到请求,就将请求作为事件放入队列,然后继续接收其他请求。
- 当主线程空闲(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,要分两种情况:- -
- 如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;
- 如果是 I/O 任务(宏任务),就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。
- 当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop)
- process.nextTick()方法将 callback 添加到 next tick 队列(微任务队列), 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。
- setImmediate(建议使用,不建议使用nextTick) 放在事件队列的尾部
- setImmediate 预定立即执行的 callback,它是在 I/O 事件的回调之后被触发
- process.nextTick 方法优先级比Promise高
eventLoop:function(){
// 如果队列不为空,就继续循环
while(this.globalEventQueue.length > 0){
// 从队列的头部拿出一个事件
var event = this.globalEventQueue.shift();
// 如果是耗时任务
if(isIOTask(event)){
// 从线程池里拿出一个线程
var thread = getThreadFromThreadPool();
// 交给线程处理
thread.handleIOTask(event)
}else {
// 非耗时任务处理后,直接返回结果
var result = handleEvent(event);
// 最终通过回调函数返回给V8,再由V8返回给应用程序
event.callback.call(null,result);
}
}
}
同步与异步 (取决于被调用者)
- 同步和异步关注的是消息通知机制
- 同步就是发出调用后,没有得到结果之前,该调用不返回,一旦调用返回,就得到返回值了。 简而言之就是调用者主动等待这个调用的结果
- 而异步则相反,调用者在发出调用后这个调用就直接返回了,所以没有返回结果。换句话说当一个异步过程调用发出后,调用者不会立刻得到结果,而是调用发出后,被调用者通过状态、通知或回调函数处理这个调用。
阻塞与非阻塞 (取决于调用者)
- 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
node加载模块顺序
-> 否 -> 查找文件模块 -> 缓存文件模块 -> 返回exports
require -> 是否在模块缓存中 -> 否 -> 是否原生模块 -> 是 -> 是否在原生模块缓存区 -> 是 -> 返回exports
-> 否 -> 加载原生模块 -> 缓存原生模块 -> 返回exports
-> 是 -> 返回exports
查找文件模块
- 是否是相对路径
- 不是相对路径
- 查找module.paths 和 global module
- 是相对路径
- 得到查找路径
- 相对路径 ./a 找文件 ./a -> ./a.js -> ./a.json -> a.node -> ./a 目录 -> 从包中找 package.json 描述文件 -> index.js -> index.node
- 不是相对也不是绝对 比如 const demo = require(“demo”) module.paths 和 global 路径,没找到就报错了
require加载文件模块的顺序
- 找到文件
- 读取文件内容
- 封装在一个函数里执行,有5个参数
- 把module.exports 给引入对象
// 效果是一样的
this.name = 'haha'
module.name = 'haha'
// 文件内的module对象
module.id // 主文件id是. , 其余为模块的文件名
module.path // 模块的查找路径
// require
// node加载文件是同步的,因为会缓存模块的export对象,所以一个模块加载多次是没用的
console.log(require.cache)
// resolve 方法解析一个模块的绝对路径,但是又不加载执行它
console.log(require.resolve("./module.js"))
// 主模块
console.log(require.main)
require实现方法
// require方法实现
const fs = require("fs");
const path = require("path");
let math = get("./math.js");
function get(url) {
const content = fs.readFileSync(path.join(__dirname, url));
const fn = new Function('exports', 'require', 'module', '__filename', '__dirname', content + '\n return module.exports;')
let module = {
exports: {}
}
return fn(module.exports, get, module, __filename, __dirname)
}
console.log(math)
js加载3中模块的方法不一样
- js 模块,如上
- json模块,读取文件内容,JSON.parse转成对象
- node模块是二进制模块,不需要读,直接用
process
// 不加参数默认参默认就是 [node地址,文件地址]
process.argv.forEach(function(item){
console.log(item);
});
process.on('exit',function(){
console.log('clear');
});
process.on('uncaughtException',function(err){
console.log(err);
})
// change工作目录
process.chdir("../../")
// 返回用户环境的变量
console.log(process.env)
js可执行脚本
新建一个文件 hello, 无后缀
#!/usr/bin/env node
console.log('hello');
console.log('hello ',process.argv);
// 给可执行权限
chmod 755 hello
// 在package.json 里声明
{
"bin":{
"hello":"hello"
}
}
npm link
- 可以直接在该目录下
hello
命令 - 指令linux命令
let {exec} = require('child_process');
let child = exec('echo hello 参数是'+name,(err,stdout,stderr)=>{
if(err) throw err;
console.info(stdout);
});
curl
curl是一种命令行工具,作用是发出网络请求,然后得到和提取数据
curl www.sina.com
// 保存网页
curl -o index.html www.sina.com
process.argv
- 第一个元素是 process.execPath。
- 第二个元素是当前执行的 JavaScript 文件的路径。
- 剩余的元素都是额外的命令行参数
Buffer (buffer 就是个装数据的容器 )
- 初始化buffer 基本操作
// 分配6个字节的buffer
const buf1 = Buffer.alloc(6)
// 分配一块没有初始化的内存
let buf2 = Buffer.allocUnsafe(6)
// console.log( Buffer.from("花1"))
// <Buffer e8 8a b1>
buf1.fill( 'a',1, 3)
// console.log(buf1)
// 内容 开始index 长度
buf1.write("张玉华", 0 , 6, 'utf-8')
//console.log(buf1.toString())
const buff = Buffer.alloc(6);
// 在其中一位 填充一个8位的数字
buff.writeInt8(127, 0)
buff.writeInt8(-127, 1)
//console.log(buff)
const buff1 = Buffer.alloc(16);
// BE是大头在前,高位在前 LE是小头在前
buff1.writeInt16BE(256, 0)
console.log(buff1)
console.log(buff1.toString())
// read 也要 按照大头小头相应读
// slice 是浅拷贝
let buf4 = Buffer.alloc(6, 1);
let buf41 = buf4.slice(2, 5)
- string_decoder 对文字异常的处理
let {StringDecoder}= require("string_decoder");
let buf = Buffer.from("张玉华")
let buf1 = buf.slice(0, 5)
let sd = new StringDecoder()
console.log(buf1.toString()) // 张* 乱码
// write 读取内容,返回一个字符串
// write 判断buffer是不是一个字符,不是的话,缓存,放到下个字符串
console.log(sd.write(buf1)) // 张
- 合并buffer
let bf1 = Buffer.from("上海");
let bf2 = Buffer.from("徐家汇");
console.log(bf1)
console.log(bf1.length)
bf1.forEach(item => {
// log 输出都是10进制
console.log(item)
})
let bf = Buffer.concat([bf1, bf2])
console.log(bf.toString())
linux 权限位
有三个执行者,所有者,所属组,其他用户,如 777 就是所有权限
读 | 写 | 执行 |
---|---|---|
r | w | x |
4 | 2 | 1 |
fs 模块
readFile会一次性读入到内存中
// 从当前路径解析出一个绝对路径
console.log(path.resolve("huahua"))
const fs = require("fs")
// fs 不加第二个参数。默认读取文件是buffer
let conf = {
encoding: "utf8",
flag: 'r'
}
fs.readFile("./code.js", conf, (err, data) => {
if (err) {
console.log(err)
} else {
console.log(data)
}
})
let conf1 = {
encoding: "utf8",
flag: 'a', // a表示追加编码
mode: '644'
}
fs.writeFile("hua.txt", 'hello', conf1, (err, data) => {
})
// 文件内容追加 用 fs.appendFile 是一样的
// 上述都是把文件作为一个整体操作,如果文件size大于内存,就无法执行
// fd 文件描述符
// 0 标准输入 1 标准输出 2 错误输出
fs.open("./code.js", "r", 0o666, function(err, fd){
console.log(fd)
})
// 监控文件变化
fs.watchFile("./demo.js", function(next, prev){
if(Date.parse(prev.ctime) == 0){
console.log("新增文件")
}else if(Date.parse(prev.ctime) !== Date.parse(next.ctime)){
console.log("修改文件了")
}else if(Date.parse(next.ctime) == 0){
console.log("删除文件了")
}
})
fs 拷贝文件
function copy(src, dist){
const SIZE = 3;
// 以只读的方式打开文本文件,文件必须存在
fs.open(src, "r", 0o666, function(err, readFD){
// w 以只写的方式打开文本文件,文件若存在则清空文件内容从文件头部开始写,若不存在则根据文件名创建新文件并只写打开
fs.open(dist, 'w', 0o666, function(err, writeFD){
let buff = Buffer.alloc(SIZE);
function next(err){
if(err){
console.log(err)
return;
}
fs.read(readFD, buff, 0, SIZE, null, function(err, bytesRead, buff){
if(bytesRead > 0){
fs.write(writeFD, buff, 0, bytesRead, null, next)
// 强行把缓存区写入文件,并且关闭
fs.fsync(fd, function(err){
fs.close(() => {console.log('close')})
})
}
})
}
next();
})
})
}
function myWrite(){
fs.open("./hua.txt", 'w', 0o666, function(err, fd){
let _bf = Buffer.from("玉华");
// write 写文件时,会先写入缓存区,再写入物理文件
fs.write(fd, _bf , 0 , 3, null, (err, byteWrite, buff) => {
fs.write(fd, _bf , 3 , 3, null, (err, byteWrite, buff) => {
fs.fsync(fd, () => {
fs.close(fd, () => {
console.log("关闭文件")
})
})
});
});
})
}
// 截断文件,保留一部分
function myDemo1(){
fs.open("aaa.hh", "r", 0o666, function(err, readFD){
if(err){
console.log(err)
}
})
}
mkdir
function makeDir(path){
path = path.split("/")
function next(index){
console.log(index)
if(index > path.length){
return;
}
let cur = path.slice(0, index).join("/")
fs.access(cur,function(err){
if(err){
fs.mkdir(cur, 0o666, () => next(++index))
}else {
next(++index)
}
})
};
next(1)
}
makeDir("a/b/c")
同步删除目录
function rmDirS(dir) {
const fileList = fs.readdirSync(dir);
fileList.forEach(item => {
const _path = dir + '/' + item;
if (data = fs.statSync(_path).isFile()) {
fs.unlinkSync(_path)
} else {
rmDirS(_path)
}
})
fs.rmdirSync(dir)
}
异步删除目录
// 遍历一个文件夹,通常是深度优先 + 前序遍历
function removeFolder(dir) {
return new Promise(function (resolve, reject) {
fs.stat(dir, (err, data) => {
if(err){
reject(err)
}
if(!data){
reject()
}
if (data.isFile()) {
fs.unlink(dir, resolve)
} else {
fs.readdir(dir, (err, files) => {
if (err) {
reject(err)
}
Promise.all(files.map(file =>removeFolder(dir + '/' + file))).then(res => {
fs.rmdir(dir, resolve)
})
})
}
});
})
}
iconv
默认node不支持uft8 ,iconv把gbk转成utf8
const fs = require("fs")
const iconv = require('iconv-lite');
fs.readFile("./gbk.txt", function(err,data){
let str = iconv.decode(data, 'gbk')
console.log(str)
})
目录的遍历
一般使用深度优先前序遍历
stream 流
- 流,文件流,http的req和res,zlib压缩流
- 管道在Linux中,流在Shell中被实现为可以通过 |(管道符) 进行桥接的数据,一个进程的输出(stdout)可被直接作为下一个进程的输入(stdin)
- 可读流有两种模式,flowing mode(一直流动,流动模式不缓存数据,直接发射)和paused mode (边读取,边使用, 使用缓存区(缓冲区可理解无限大)放数据,如每次读取highwatermark个字节,缓冲区里的不够highwatermark个字节,然后会再读highwatermark个字节 )
流的类型
- Readable - 可读的流 (例如 fs.createReadStream()).
- Writable - 可写的流 (例如 fs.createWriteStream()).
- Duplex - 可读写的流 (例如 net.Socket).
- Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate()).
可读流
const fs = require("fs");
const option = {
highWaterMark: 3,
flags: 'r',
start: 2,
end: 9,
mode: 0o666
};
// 默认是返回 buffer
let rs = fs.createReadStream("./hua.txt", option);
// 文件流额外两个事件 open close
rs.on("open", () => {
console.log("open")
})
// 监听data事件,按照缓冲区大小,来读取数据
// 这个事件是一直进行的
rs.on("data", function(data){
console.log(data.toString())
// 暂停读取和发射data事件
rs.pause()
setTimeout(() => {
rs.resume()
}, 1500)
})
rs.on("end", () => {
console.log("end")
})
rs.on("error", () => {
console.error("error")
})
可写流
let fs = require('fs');
const option = {
highWaterMark: 3,
flags: 'w',
mode: 0o666
};
var fs=require("fs");
var rs = fs.createReadStream("./Koala.jpg");//默认64KB
var ws = fs.createWriteStream("./Copy.jpg");//默认16KB,写入速度小于读取速度
rs.on("data",function(data){
var flag = ws.write(data);
if (!flag){ //缓冲区已满
rs.pause();//停止触发data事件
}
});
ws.on("drain",function(){
rs.resume();//如果当前的缓冲区写入完毕,就重新触发data事件
});
rs.on("end",function(){
ws.end();//将剩下的数据全部写入,并且关闭写入的文件
})
按行读数据
const fs = require("fs");
const ev = require("events");
const util = require("util");
const NEW_LINE = 0x0A;
const RETURN = 0x0D;
// 3个操作系统的换行不一样,windows是回车+换行 \r\n 就是 0d0a mac 是 \r linux 是 \n
// fs.readFile("./2.txt", function (err, data) {
// console.log(data)
// })
function LineReader(path) {
ev.call(this)
this._reader = fs.createReadStream(path);
this.on("newListener", (type, listener) => {
if (type === 'newLine') {
let buffers = []
this._reader.on("readable", () => {
let ch;
while ( null !== (ch = this._reader.read(1)) ) {
if (ch[0] === NEW_LINE) {
this.emit("newLine", Buffer.from(buffers));
} else if (ch[0] === RETURN) {
this.emit("newLine", Buffer.from(buffers));
buffers = [];
let nextCh = this._reader.read(1);
if (nextCh[0] !== NEW_LINE) {
buffers.push(nextCh[0])
}
} else {
buffers.push(ch[0])
}
}
})
this._reader.on("end", () => {
this.emit("newLine", Buffer.from(buffers));
this.emit("end")
})
}
});
}
util.inherits(LineReader, ev)
let reader = new LineReader("./2.txt");
reader.on("newLine", function (data) {
console.log(data.toString())
})
实现可写流
const fs = require("fs");
const Event = require("events");
class WriteStream extends Event {
constructor(path, opts) {
super(path, opts)
this.path = path;
this.flag = opts.flag || 'w'
this.mode = opts.mode || 0o666
this.start = opts.start || 0
// 写入索引
this.pos = this.start;
this.encoding = opts.encoding || "utf8"
this.autoClose = opts.autoClose;
this.highWaterMark = opts.highWaterMark || 16 * 1024;
this.writing = false;
// 缓存区字节大小
this.length = 0;
// 缓冲区 源码是链表
this.buffers = []
this.open()
}
open() {
fs.open(this.path, this.flag, this.mode, (err, fd) => {
if (err) {
if (this.autoClose) {
this.destry()
}
this.emit("error")
}
this.fd = fd;
this.emit("open")
})
}
write(chunk, encoding = 'utf8', cb) {
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)
// 缓存区长度增加
let len = chunk.length;
this.length += len;
if (this.writing) {
this.buffers.push({ chunk, encoding, cb })
console.log(this.buffers)
} else {
this.writing = true;
this._write(chunk, encoding, () => this.clearBuffer())
}
return this.length < this.highWaterMark;
}
_write(chunk, encoding, cb) {
if (typeof this.fd !== 'number') {
return this.once("open", function () {
this._write(chunk, encoding, cb);
})
}
fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, bytesWritten) => {
if (err) {
if (this.autoClose) {
this.emit("error");
this.destry();
}
}
this.pos += bytesWritten;
// 写入多少字节,缓存区要减少多少字节
this.length -= bytesWritten
cb && cb()
})
}
destry() {
fs.close(this.fd, () => {
this.emit("close")
})
}
clearBuffer() {
let data = this.buffers.shift()
if (data) {
this._write(data.chunk, data.encoding, () => this.clearBuffer())
} else {
this.writing = false;
// 缓存区清空了
this.emit("drain")
}
}
}
let ws = new WriteStream("./21.txt", {
highWaterMark: 3,
autoClose: true
})
let n = 9;
ws.on("error", () => {
console.log("error")
})
function write() {
let flag = true;
while (flag && n > 0) {
flag = ws.write(n + '')
n--;
}
ws.once("drain", () => {
console.log("drain")
write()
})
}
write()
实现可读流带pipe
const fs = require("fs");
const Event = require("events");
class ReadStream extends Event {
constructor(path, opts) {
super(path, opts)
this.path = path;
this.highWaterMark = opts.highWaterMark || 64 * 1024;
this.flag = opts.flag || 'r';
this.mode = opts.mode || 0o666;
this.start = opts.start || 0;
this.end = opts.end;
this.autoClose = opts.autoClose || false;
this.pos = this.start;
this.encoding = opts.encoding;
this.flowing = null;
this.buffer = Buffer.alloc(10);
this.open();
this.on("newListener", (type, listener) => {
// 监听了data事件就切换到流动模式
if (type === 'data') {
this.flowing = true;
this.read();
}
})
}
read() {
if (typeof this.fd !== 'number') {
return this.once("open", this.read)
}
let readNum = this.end ? Math.min(this.highWaterMark, this.end - this.pos + 1) : this.highWaterMark;
// this.buffer 不是缓冲区
fs.read(this.fd, this.buffer, 0, readNum, this.pos, (err, bytesRead, buffer) => {
if (err) {
if (this.autoClose) {
this.destroy()
}
return this.emit("error")
}
if (bytesRead) {
this.pos += bytesRead;
let data = this.buffer.slice(0, bytesRead);
data = this.encoding ? data.toString(this.encoding) : data;
this.emit("data", data);
if (this.end && this.pos > this.end) {
return this.endFn()
} else {
if(this.flowing){
this.read()
}
}
}
});
}
endFn() {
this.emit("end");
this.destroy();
}
open() {
fs.open(this.path, this.flag, this.mode, (err, fd) => {
if (err) {
if (this.autoClose) {
this.destroy();
this.emit("error", err);
}
}
this.fd = fd;
this.emit("open");
})
}
pipe(ws) {
this.on("data", data => {
let flag = ws.write(data);
if (!flag) {
this.pause();
}
});
ws.on("drain", this.resume)
}
pause() {
this.flowing = false;
}
resume = () => {
this.flowing = true;
this.read();
}
destroy() {
fs.close(this.fd, () => {
this.emit("close")
})
}
}
可读流暂停模式
// 在可读流创建会,自动进入暂停模式,并填充缓存区
class ReadStream extends Event {
constructor(path, opts) {
super(path, opts)
this.path = path;
this.highWaterMark = opts.highWaterMark || 64 * 1024;
this.flag = opts.flag || 'r';
this.mode = opts.mode || 0o666;
this.start = opts.start || 0;
this.end = opts.end;
this.autoClose = opts.autoClose || false;
this.pos = this.start;
this.encoding = opts.encoding;
this.flowing = null;
// 这只是一个临时容器
this.buffer = Buffer.alloc(this.highWaterMark);
// 缓存区
this.cache = [];
// 缓存区的数据长度
this.len = 0;
this.open();
this.on("newListener", (type, listener) => {
// 监听了data事件就切换到流动模式
if (type === 'data') {
this.flowing = true;
this.read();
}
})
}
open() {
fs.open(this.path, this.flag, this.mode, (err, fd) => {
if (err) {
if (this.autoClose) {
this.destroy();
this.emit("error", err);
}
}
this.fd = fd;
this.emit("open");
this.read()
})
}
read(n) {
let ret;
if (n > 0 && n < this.len) {
ret = Buffer.alloc(n);
let index = 0;
let bf;
while (null != (bf = this.cache.shift())) {
for (let i = 0; i < bf.length; i++) {
ret[index++] = bf[i]
if (index === n) {
bf = bf.slice(i)
this.len -= n;
this.cache.unshift(bf)
break;
}
}
}
}
// 数据为空,或者数据小于最高水位线,就要重新读取
if (this.len == 0 || this.len < this.highWaterMark) {
fs.read(this.fd, this.buffer, 0, this.highWaterMark, null, (err, bytesRead) => {
if (bytesRead) {
let data = this.buffer.slice(0, bytesRead)
this.cache.push(data);
this.len += data.length;
this.emit("readable")
} else {
this.emit("end")
}
})
}
return ret;
}
}