Node的学习记录

157 阅读46分钟

使用node来执行文件,他会把这个文件当成一个模块,并默认修改该文件(模块)的this

在前端中访问变量是通过window属性,但是在node后端中 想访问全局需要通过global字段

clearImmediate setImmediate
这两个全局方法是node 自己实现的 都是宏任务
Buffer node原生的二进制对象(用来操作文件)
(最早的时候浏览器不能直接读写文件,现在基于H5的开发也支持了)
node作为一种服务端语言,必然要解决文件的读写问题,因此出现了buffer

__dirname:当前执行文件所在的的目录

当前文件执行时的目录 是固定的的 (绝对路径)

console.log(__dirname);


// F:\Desktop\珠峰node架构\3.node-core

__filename:当前执行文件的绝对路径

console.log(__filename); // 文件自己的绝对路径
//F:\Desktop\珠峰node架构\3.node-core\1.js

process:进程。服务端代码都跑在进程中,进程里包含着线程

在 node 中 process对象是一个极为重要的对象,对象中包含了很多重要的信息 我们需要掌握几个重要的属性

1:platform:表示平台,我们可以根据不同的平台执行不同的命令 举例:

console.log(process.platform)//win32

2:cwd:表示当前工作目录(current working directory)

console.log(process.cwd()) //F:\Desktop\珠峰node架构\3.node-core

如果此时我们执行 cd .. 进入 /F:\Desktop\珠峰node架构目录 通过 node 3.node/1.js来执行的的话 就会打印

console.log(process.cwd()) //F:\Desktop\珠峰node架构

意思是在哪里运行命令,运行目录就指向那里

他的用处是:可以改变 webpack会自动查找运行webpack命令的目录下查找 webpack.config.js

3:env:执行代码时传入环境,根据不同的环境变量 执行不同的操作

if (process.env.NODE_ENV === 'development') {
    console.log('dev');
} else {
    console.log('prod');
}

4:argv:执行代码时传入的参数执行node所在的exe文件 [当前执行的文件,..其他参数]

console.log(process.argv); // 会根据用户传递的参数来解析 生成对应的功能

打印的结果:
[
  'F:\\NVM\\node\\node.exe',
  'F:\\Desktop\\珠峰node架构\\3.node-core\\1.js'
]


使用: 例如 node 1.js --port 3000 --info abs console.log(process.argv); 会打印出如下结果

[  'F:\\NVM\\node\\node.exe',  'F:\\Desktop\\珠峰node架构\\3.node-core\\1.js',  '--port',  '3000',  '--info',  'aaa']

这时就可以解析用户传入的参数来 生成不同的功能 例如 : 像下面进行解析

let argv = process.argv.slice(2).reduce((memo,current,index,arr)=>{
    if(current.startsWith('--')){
        memo[current.slice(2)] = arr[index+1]
    }
    return memo;
},{});
console.log(argv)

//打印:{ port: '3000', info: 'aaa' }

像一些脚手架工具 都会有交互式的操作,根据不同的传参来生成不同的功能就是基于上面的argv 例如 commanderargs包(webpack使用) 来帮我们解析用户的参数来做想做的事

5:nextTick:node中自己实现的异步api, 不属于node中的 EventLoop,优先级比promise更高 process.nextTick会在同步代码执行之后立即执行,不属于事件循环

node的事件循环(node的事件环是不包含微任务的)

image.png

这里的每一个阶段都对应一个事件队列,当eventLopp执行到某个阶段时会将当前阶段对应的队列依次执行。当该队列已用尽或者达到了回调限制,事件循环移动到下一阶段。

process.nextTick() 从技术上不属于事件循环。优先级高于微任务

timer 中存放 定时器的回调 (setTimeoutsetInterval

Pending Callback:待定回调():对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED。

poll:检索新的 I/O 事件;执行与 I/O 相关的回调;主要是放置一些 I/O 的回调。 如:(readFilewriteFile

check是专门放置setImmediate的回调的

close 是放置一些关闭回调的。如:socket.on('close', ...)

针对如下代码:

setTimeout(() => {
    console.log('timeout')
}, 0);
setImmediate(()=>{ // 异步方法
    console.log('setImmediate')
})

它的执行结果是不一定的 原因是:当前默认执行主栈代码,主栈执行完毕后要执行定时器,但是定时器可能没有到达时间(setTimeout的 0 并不是绝对的0,我记得最小是4毫秒,循环到timer 阶段时可能setTimeout的回调函数还没加入timer),就会进入事件循环的下一个阶段

image.png

但是,如果不是在主模块(主线程)中调用,结果就会是确定的 例如:

const fs = require('fs');
fs.readFile('./note.md', 'utf8', () => {
    setTimeout(() => {
        console.log('timeout')
    }, 0);
    setImmediate(()=>{ // 异步方法
        console.log('immediate')
    })
})

readFile 属于poll 阶段的 api, 在poll阶段:

image.png

也就是说上述代码会形成poll ===> check ===>timer ===>poll ====>timer ===> poll ===>timer ......的循环, 当事件循环执行到poll阶段时,会去检测poll的队列是否为空,如果不为空就执行队列中的任务,直到poll阶段超时或者poll队列执行完毕。当poll队列执行完之后会去检查setImmediate队列,该队列不为空时才会进入 check 阶段,否则等待poll的时间超时及,然后进入timer阶段,

在poll的等待时间中可能出现其他的callback(比如其他的poll事件加入了poll队列)

// 浏览器的特点是 先执行执行栈中代码,清空后会执行微任务 -> 取出一个宏任务来执行 不停的循环
// node先执行当前执行栈代码,执行完毕后 会进入到事件环中 拿出一个(宏任务)来执行,每执行完毕后 会清空微
// 任务 (nextTick promise.then) (早期有区别  11+); 
// node中的队列是多个 其他和浏览器一样
// 因为新版要和浏览器表现形式一致 所以这样设计的

所以,事件循环中每个阶段,在拿出一个任务执行之后都会清空微任务 。如果某个阶段的队列中任务过多,该阶段会超时,剩下的任务会进入 pending callback

完整循环是,主栈代码执行完,清空微任务(process.nextTick和promise.then),再进入事件环

Node的模块化

node中每个文件都是一个模块,模块化的实现借助的是函数。 node 相当于给每个文件的外部包裹了一个函数,函数中有参数,参数里面有五个属性 __dirname、 __filename、 require、 exports、 module

为什么要有模块化

最早是为了解决命名冲突问题(“单例模式” 不能完全解决这些问题)

用文件拆分的方式 配合 iife 自执行函数来解决(这种会有依赖的问题,前端浏览器不清楚哪个文件先请求过来,依赖的顺序无法解决)

umd 兼容 amd 和 cmd  + commonjs 但不支持es6模块
// commonjs规范  (基于文件读写的  如果依赖了某个文件我会进行文件读取) 动态的
// 一个文件就是一个模块
// 我想使用这个模块我就require
// 我想把模块给别人用 module.exports导出

你下载的模块的命令文件都会挂载到 .bin目录下,当你运行npm run xxx 时 默认在执行命令之前,会将环境变量.bin添加到全局path下,之后回去.bin目录下找相应的可执行文件

npm run xxx 会在 node_modules 里面找.bin目录下的可执行文件,使用对应的可执行文件进行执行

如果直接用 npm run script的方式 默认在执行命令之前,会将环境变量.bin添加到全局path下, 所以可以使用,但是命令执行完毕后会删掉 对应的 path
// npx 和 npm run 类似  ,但是 使用npx 如果模块不存在就会先安装再使用,使用后可以自动删除掉

package.json版本号的管理方式

// ^2.2.4 = 第一位只能是2 之后的更新的版本都能匹配到
// ~2.2.4 指定 MAJOR.MINOR 版本号下 不能超过 2 不能比2.2.4小
// <= >= 就不说了,太简单

因此,不同时期使用npm install 拉取依赖可能会拉取不同的版本的依赖(某个依赖有了更新的版本),所以需要package-lock.json 来锁定版本,确保拉取的版本一致

如果更改了 package.json 会同步给 -> package-lock.json文件,如果版本仍能兼容会采用lock的配置

一般来说,有package-lock.json存在的话都会采用package-lock来下载依赖

进制的概念

1:

最早前端(浏览器端)是无法直接读取文件、操作文件的(node是使用在服务端的), 但我们又需要对文件和前端传递的数据进行处理(所有文件内容都是以2进制来存储的, 数据都是以2进制形式来表现的)

例如:经典面试题

console.log((0.1+0.2)===0.3)//false
console.log(0.1+0.2) // 0.30000000000000000004

在服务端,我们需要一个东西可以来标识内存 。但是不能是字符串,因为字符串无法标识图片

node中用Buffer来标识内存的数据 他把内容转换成了16进制来显示 (16进制比较短)

Buffer代表的是内存,内存是一段“固定空间”, 产生的内存是固定大小,不能随意添加(放入超出大小的数据,如果需要,要去声明一个更大的空间(扩容的概念,需要动态创建一个新的内容,把内容迁移过去))

Buffer.alloc():

参数是新声明的buffer 所占的字节数量

一般使用alloc来声明一个buffer

let buffer1 = Buffer.alloc(5);
console.log(buffer1[0]);//0

像数组(但是和数组有区别),数组可以扩展,buffer不能扩展,可以用索引取值,取出来的内容是10进制

Buffer.from()

这种方法声明Buffer用的非常少

// 此方法用的非常少,我们不会直接填16进制
let buffer2 = Buffer.from([0x25, 0x26, 300]); // 超过255 会取余
console.log(buffer2[0]) //37
console.log(buffer2[2]) //44  // 超过255 会取余 除于256取余
console.log(buffer2) // <Buffer 25 26 2c> 16进制 300对256取余为44 44翻译为16进制就是2c

Buffer和字符串的转换 这种方法Buffer.from('珠峰')Buffer.alloc() 是常用的声明buffer的方法。

一般情况下,我们会alloc来声明一个buffer,或者把字符串转换成buffer使用

let buffer3 = Buffer.from('珠峰'); //字符串“珠峰” 采用utf8编码 默认转成6个字节
console.log(buffer3) // <Buffer e7 8f a0 e5 b3 b0>

后台获取的数据都是buffer,包括后面的文件操作获取的文件也都是buffer形式 我们上传的数据和文件本质上也是二进制数据,所以也是buffer

buffer的使用。 无论是2进制还是16进制他们表现的东西都是一样的,只不过形式不同

base64“编码”,在后期使用的过程中用的非常多 (base64 没有加密功能)所有人都知道这个规范

base64 可以字符串可以放到任何路径的链接里 (可以减少请求的发送)文件大小会变大(如果采用base64 他的缓存会依赖文件), base64转化完毕后会比之前的文件大1/3

为什么base64 会会比之前的文件大1/3呢 原来,base64 会把之前的每个字节(最大为0xff、小于255) 转为每个字节小于64的值

const r = Buffer.from('珠'); // 可以调用toString转化成指定的编码
console.log('r',r)//r <Buffer e7 8f a0>

// base64 的来源就是将每个字节多转化成 小于64的值
console.log('0xe7',0xe7.toString(2));//11100111
console.log('0x8f',0x8f.toString(2));// 10001111
console.log('0xa0',0xa0.toString(2));// 10100000

Base64 要求 ,每个字节前两位都是空的, 11100111、10001111、10100000 3*8 转为 111001 111000 111110 100000 的格式 在前两位加上 0 正好 4个字节

console.log(parseInt('111001', 2))
console.log(parseInt('111000', 2))
console.log(parseInt('111110', 2))
console.log(parseInt('100000', 2))

57
56
62
32

base64 可以字符串可以放到任何路径的链接里(可以减少请求的发送,url-loader 里就可以规定小于多少字节就转为base64直接写在文件里,就可以不用发请求了。但是,如果我们希望一个图片靠http的离线缓存时不要使用这种做法:一般是图片过大,值得发请求)

文件大小会变大(如果采用base64 他的缓存会依赖文件),

base64转化完毕后会比之前的文件大1/3

Buffer.toString()

const r = Buffer.from('珠').toString('base64');

console.log('r',r) // 54+g


console.log(parseInt('111001', 2))
console.log(parseInt('111000', 2))
console.log(parseInt('111110', 2))
console.log(parseInt('100000', 2))

57
56
62
32

let str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
str += str.toLocaleLowerCase();
str += '0123456789+/';
// 57 56  62 32
console.log(str[57] + str[56] + str[62] + str[32]); // 54+g 没有加密功能



可以看到,base64的编码 Buffer.from('珠').toString('base64')变成了 54+g

正是 把每个字节转成了64以下的 57 56 62 32 之后 按照 AB.....ab...01234567890+/ ,64位 ,再从中按照顺序拿出 str[57] + str[56] + str[62] + str[32] 正是54+g

Buffer.slice() :slice 返回的不是拷贝、而是原类数组的内存地址

let buffer4 = Buffer.from([1,2,3,4,5])


console.log(buffer4) // <Buffer 01 02 03 04 05>


let slicBuffer = buffer4.slice(0,1);
slicBuffer[0] = 100;
console.log(buffer4)// <Buffer 64 02 03 04 05>

由slicBuffer的表现我们可知,slice 返回的不是拷贝,而是原类数组的内存地址

Buffer.copy()

可以将buffer的数据拷贝到另一个buffer上 (一般用不到,concat是基于copy的)

let buf0 = Buffer.from('架构')
let buf1 = Buffer.from('珠');
let buf2 = Buffer.from('峰');

例如将上面三个buffer 转为一个

let buf0 = Buffer.from('架构')
let buf1 = Buffer.from('珠');
let buf2 = Buffer.from('峰');

Buffer.prototype.copy = function(targetBuffer, targetStart, sourceStart = 0, sourceEnd = this.length) {
    for (let i = sourceStart; i < sourceEnd; i++) {
        targetBuffer[targetStart++] = this[i];
    }
}
let bigBuffer = Buffer.alloc(12); // == new Buffer(12)
buf0.copy(bigBuffer, 6, 0, 6);
buf1.copy(bigBuffer, 0, 0, 3);
buf2.copy(bigBuffer, 3); // 默认后两个参数不用传递

console.log(bigBuffer.toString()) //珠峰架构

Buffer.concat()

concat是基于copy的

let buf0 = Buffer.from('架构')
let buf1 = Buffer.from('珠');
let buf2 = Buffer.from('峰');
Buffer.concat = function(bufferList, length = bufferList.reduce((a, b) => a + b.length, 0)) {
    let bigBuffer = Buffer.alloc(length);
    let offset = 0;
    bufferList.forEach(buf=>{
        buf.copy(bigBuffer,offset)
        offset += buf.length
    })
    return bigBuffer
}
// http 数据是分包传递的,把每段数据进行拼接
let bigBuf = Buffer.concat([buf1, buf2, buf0],100)


//  isBuffer
console.log(Buffer.isBuffer(bigBuf));

console.log(bigBuf.toString()); //珠峰架构

// buffer.length
console.log(bigBuf.byteLength,bigBuf.length,Buffer.from('珠峰').length);

用处: http 数据是分包传递的,把每段数据进行拼接

Buffer.isBuffer()

文件操作

fs模块中基本上 有两种api (同步、异步) 如果需要在后续代码运行前把文件操作做完,就采用同步。否则采用异步

运行之后一般采用异步,防止同步拿不到结果阻塞代码的运行

// 读取的时候默认不写编码是buffer类型,如果文件不存在则报错

// 写入的时候默认会将内容以utf8格式写入,如果文件不存在会创建

// 如果采用嵌套的写法,就只能读取完毕后再去写入.


// i/o  input output  读文件 (以内存为参照物) 写操作

// 读取的时候默认不写编码是buffer类型,如果文件不存在则报错
// 写入的时候默认会将内容以utf8格式写入,如果文件不存在会创建
// 如果采用嵌套的写法,就只能读取完毕后在次写入.能不能边读取 边写入

// 大文件用此方法会导致淹没可用内存 (例如内存8个g 文件3个g, 淹没了3个g)
fs.readFile(path.resolve(__dirname,'package.json',function (err,data){
   if(err) return console.log(err);
    fs.writeFile(path.resolve(__dirname,'./test.js'),data,function (err,data) {
        console.log(data);
    })
}))

大文件用此方法会导致淹没可用内存 (例如内存8个g 文件3个g, 淹没了3个g) 读文件对于内存其实是写操作(把文件buffer写入内存),因此,这种嵌套的方式将导致内存中拥有完整的文件的buffer,再去进行写操作(写操作对于内存其实是读操作)。会极大的浪费内存

所以这种嵌套的读写适合小文件不适合大文件 我们希望能不能边读取 边写入

另一种一种方式是:fs.open、fs.close、fs.read、fs.write

下面我们使用3个字节来实现一个拷贝功能

copy

function copy(source, target, cb) { // 使用3个字节来实现一个拷贝功能
    const BUFFER_SIZE = 3;
    const buffer = Buffer.alloc(BUFFER_SIZE);
    let r_offset = 0;
    let w_offset = 0;
    //  读取一部分数据 就 写入一部分数据
    // w 写入操作 r  读取操作 a 追加操作 r+ 以读取为准可以写入操作  w+ 以写入为准可以执行读取操作
    // 权限  3组 rwx组合  421 = 777(八进制 )  (当前用户的权限 用户所在的组的权限 其他人权限  )
    // 0o666 = 438
    // exe 文件 bat 文件能执行的文件
    // 读取的文件必须要存在,否则会报异常,读取出来的结果都是buffer类型
    // 写入文件的时候文件不存在会创建,如果文件有内容会被清空
    fs.open(source, 'r', function(err, rfd) {
        fs.open(target, 'w', function(err, wfd) {
            // 回调的方式实现功能 需要用递归,因为不用递归无法保证顺序
            // 同步代码 可以采用while循环
            function next(){
                fs.read(rfd, buffer, 0, BUFFER_SIZE, r_offset, function(err, bytesRead) { // bytesRead真正读取到的个数
                    if (err) return cb(err)
                    if (bytesRead) {
                        fs.write(wfd, buffer, 0, bytesRead, w_offset, function(err, written) {
                            r_offset += bytesRead;
                            w_offset += written;
                            next();
                        })
                    }else{
                        fs.close(rfd,()=>{});
                        fs.close(wfd,()=>{})
                        cb();
                    }
                })
            }
            next();
        })
    })
}

这种写法令人头疼,读写耦合在了一起,使用发布订阅可以实现解耦

ReadStream:可读流

可读流, 不是一下子把文件都读取完毕,而是可以控制读取的个数和读取的速率

let rs = new ReadStream('./a.txt',{ // 创建可读流一般情况下不用自己传递参数 fd
    flags:'r',
    encoding:null, // 编码就是buffer
    autoClose:true, // 相当于需要调用close方法
    // emitClose:true, // 触发一个close时间
    // start:1,
    //end:4,// end 是包后的
    highWaterMark: 3// 每次读取的数据个数 默认是64 * 1024 字节
});


// 发布订阅模式

//他会监听用户 ,绑定了data事件,就会触发对应的回调,并且不停的触发data事件


可读流的实现 image.png


rs.on('data',function (chunk) {
    console.log(chunk.toString());
    //每次读取三个字节
    123
    456
    789
    012
    345
    678
    90

});

rs.on('open',function (fd) { // this.emit('newListener') 文件打开触发的事件
   console.log('open',fd) //fd number 类型
})

rs.on('end',function () { // 当文件读取完毕后会触发end事件
    console.log('end');
})

rs.on('close',function () {//当文件close 会触发close 事件
    console.log('close')
});

rs.on('error',function (err) { 
    console.log(err,'err')
})



rs.on('data',function (chunk) {
    console.log(chunk.toString()); 
    rs.pause()// 可以终止data事件的触发
});
//只会打印第一轮,之后data事件的触发就被终止了


 setInterval(() => {
     rs.resume(); // 再次触发data事件
 }, 1000);

// open 和 close 是文件流独有的

// 我们所有的的可读流 都具备 (on('data'),on('end'),on('error'),resume pause))

实现我们自己的ReadStream

const EventEmitter = require('events');

const fs = require('fs')

class ReadStream extends EventEmitter{
    constructor(path, options) {
        super();
        this.path = path;
        this.flags = options.flags || 'r'
        this.encoding = options.encoding || null;
        this.autoClose = options.autoClose || true;
        this.start = options.start || 0;
        this.end = options.end;
        this.highWaterMark = options.highWaterMark || 64 * 1024
        this.flowing = false; // pause resume

        this.open() //文件打开操作 注意这个方法是异步的


        // 当你进行事件绑定时 会触发这个 事件 type 就是相应的eventName的
        this.on('newListener',function (type) {
            if(type === 'data'){
                this.flowing = true;
                // 注意用户监听了data事件 才需要读取
                this.read();
            }
        })
        this.offset = this.start; // 默认start = offset
    }
    pause(){
        this.flowing = false;
    }
    resume(){
        if (!this.flowing){
            this.flowing = true;
            this.read()
        }
    }

    //之所以不在 open 里面调用 read 方法而是 用 发布订阅 模式 是为了 解耦
    read(){
  // 希望在open之后才能拿到fd(open是异步方法)
  // 因为 open 完成后会 emit open 事件
        if (typeof this.fd !=="number"){
            // 只会执行一次的 事件
            // 防止多次触发
            // 如果 open 完成,就会再去触发read 这时 肯定能拿到 this.fd
            // 这样就解决了 read 在执行时 可能 拿不到 fd的尴尬
            return this.once("open",()=>{
                this.read()
            })
        }
        // 如果有start、end,那么总共读取多少 就是 确定的了,
        //
        let howMutchToRead = this.end ? Math.min(this.end - this.offset + 1  , this.highWaterMark) :  this.highWaterMark;
        const buffer = Buffer.alloc(howMutchToRead);
        // 读取文件中内容,每次读取this.highWaterMark个
        // 123   4
        fs.read(this.fd,buffer,0,howMutchToRead,this.offset,(err,bytesRead)=>{
            if(bytesRead){
                this.offset += bytesRead;
                this.emit('data',buffer.slice(0,bytesRead));
                // data 事件可能会修改 flowing
                if(this.flowing){ // 用于看是否递归读取
                    this.read();
                }
            }else{
                this.emit('end');
                this.destroy();
            }
        })
    }
    destroy(err){
        if (err){
            this.emit('error')
        }
        if(this.autoClose){
            fs.close(this.fd,()=> this.emit('close'))
        }
    }
    open(){
        fs.open(this.path,this.flags,(err,fd)=>{
            if (err){
                return this.destroy(err)
            }
            this.fd = fd;
            this.emit('open',fd)
        })
    }
}


module.exports = ReadStream

可写流的使用

const fs = require('fs');
// fs.open fs.write fs.close
// 创建可写流对象
let ws = fs.createWriteStream('./b.txt', {
    flags: 'w',
    encoding: 'utf8',
    autoClose: true,
    start: 0,
    highWaterMark: 3, // 可写流的highWaterMark 和 可读流的不一样 , 我期望用多少内存来写
});
// 在写入的时候 会累计写入的字节数,如果超过预期或者等于预期则返回 false,虽然超过了预期但是不影响写入
let flag = ws.write('1'); // string or buffer 
//返回值是true 或 false// console.log(flag);

// 会发现,当写入字节数(累计的)超出或等于了 highWaterMark 就会返回false
console.log(flag);
flag = ws.write('12122');
console.log(flag);
flag = ws.write('1');
console.log(flag);
flag = ws.write('1');
console.log(flag);




ws.write()是一个并发异步方法(不知道那个先进行写的操作),我们希望将他改为串行异步

由于write方法是异步的,所以如果多个write方法同时操作一个文件,就会有出错的情况

除了第一次的write,将其他的排队,第一个完成后,清空缓存区,如果缓存区过大会导致浪费内存,所以我们会设置一个预期的值,来进行控制,达到预期后就不要在调用write方法了

如下:使用3个字节来实现

const rs = fs.createReadStream('./a.txt',{
    highWaterMark:3 //  读取默认64k
});
const ws = fs.createWriteStream('./b.txt',{
    highWaterMark:2 //  写入默认内存是16k, 预期这么大,超过也行,只是浪费而已
})
// readFile writeFile
rs.on('data',function (data) {
    let flag = ws.write(data);
    if(!flag){
    //如果缓存区已经达到预期就停止读,自然也就不会触发data事件,也就不会再去写
        console.log('吃不下了')
        rs.pause();
    }
})
ws.on('drain',function(){ // 目前所有写入的数据都完毕了
    console.log('吃完了,在喂我吧'); // 接着在真实的往文件里写
    rs.resume();
})

WriteStream的drain 的意思是可写流 内存缓存区的已经写完 一般情况下读取会比写入快很多(读取是从磁盘写入到内存、写入是从内存写入到磁盘)

WriteStream 的实现

可写流因为有一个缓存区,所以实现会比可读流复杂一些,缓存区会使用链表来实现,所以下面会讨论一下链表

/**
 * element 存储的是数据
 * next 存储的是下一个人的指针
 */

class Node {
    constructor(element, next) {
        this.element = element;
        this.next = next;
    }
}
// 存储数据 add 添加 remove 删除  set(设置) get(获取)
class LinkedList {
    constructor() {
        this.head = null;
        this.size = 0;
    }
    _node(index) {
        let current = this.head;
        for (let i = 0; i < index; i++) { //遍历机会
            current = current.next;
        }
        return current;
    }
    add(index, element) { // 这里可以考虑越界条件
        // 1.添加的时候创造一个添加的节点,让这个节点的next指向前一个人的next
        // 2.让前一个人的next指向他自己
        if (arguments.length === 1) { // 都处理成两个参数
            element = index; // 如果只有一个参数,那么传入的是一个内容,那我把内容给element,索引处理成 当前的size
            index = this.size;
        }
        let head = this.head; // 把当前的头拿出来
        // 判断当前 节点是不是第一个,如果是第一个他就是头
        if (index == 0) {
            this.head = new Node(element, head);
        } else {
            // 获取前一个节点
            let prevNode = this._node(index - 1); // 以第二个节点为例 需要找到第一个节点
            // if(!prevNode) return
            prevNode.next = new Node(element, prevNode.next)
        }
        this.size++;
    }
    remove(index) {
        // 链表的删除如何实现 ? 找到删除的前一个,让她的下一个指向,下一个的下一个
        // 如果删除的是头部?
        let removeNode;
        if (index == 0) {
            removeNode = this.head;
            if (removeNode !== null) {
                this.head = this.head.next;
                this.size--;
            }
        } else {
            let prevNode = this._node(index - 1);
            removeNode = prevNode.next;
            prevNode.next = prevNode.next.next;
            this.size--;
        }

        return removeNode
    }

    reverse1() { // 递归变成循环
        function r(head) {
            if (head == null || head.next == null) { // 1。空链表 2.只有一个人,就不用转了
                return head;
            }
            let newHead = r(head.next); // 先从最底层进行反转,所以这里一直往下找,找到最后一个
            head.next.next = head;
            head.next = null;
            return newHead;
        }
        this.head = r(this.head); // 补充了一句 改了头
        return this.head;
        // let head = this.head; // 获取原来的头
        // let newHead = head.next; // 用2 作为新头
        // head.next.next = head; // 将原来的下一个指向为第一个
        // head.next = null // 把老头的next 作为null
        // return newHead //返回新头
    }
    // reverse() {
    //     let head = this.head;
    //     if (head == null || head.next == null) return head;
    //     let newHead = null; // 创建了一个空链表,将以前的链表拷贝过来了
    //     while (head !== null) { // 如果不是null 我就一直搬家
    //         let temp = head.next; // 保留2
    //         head.next = newHead // 让1 变为null
    //         newHead = head; // 让这个新链表的头 等于老链表的头
    //         head = temp; // 把老的指向2
    //     }
    //     this.head = newHead;
    //     return newHead
    // }

    reverse(){
        let head= this.head
        if (head == null || head.next == null ) return head;
        let newHead = null;
        while (head){
            let tem= head.next
            head.next= newHead
            newHead = head
            head=tem
        }
        this.head=newHead
        return newHead
    }
}
module.exports = LinkedList
let ll = new LinkedList();
ll.add(1);
ll.add(2);
ll.add(3);
ll.add(4);
console.dir(ll.reverse(),{depth:100});

// 链表的反转如何实现

// 可写流

WriteStream 的实现

const LinkedList = require('./2.linkList')

// 用链表 来实现队列
class Queue {
    constructor() {
        this.ll = new LinkedList
    }
    poll() { // 删除头部
        let removeNode =  this.ll.remove(0);
        return removeNode && removeNode.element;
    }
    offer(element) { // 添加
        this.ll.add(element)
    }
}
module.exports = Queue;
const EventEmitter = require('events');
const fs = require('fs');
const Queue = require('./queue')
class WriteStream extends EventEmitter {
    constructor(path, options) {
        super();
        this.path = path;
        this.flags = options.flags || 'w';
        this.encoding = options.encoding || 'utf8';
        this.mode = options.mode || 0o666;
        this.autoClose = options.autoClose || true;
        this.start = options.start || 0;
        this.highWaterMark = options.highWaterMark || 16 * 1024;

        this.len = 0; // 用于维持有多少数据没有被写入到文件中的
        this.needDrain = false;
        this.cache = new Queue();
        this.writing = false; // 用于标识是否是第一次写入
        this.offset = this.start; // 偏移量
        this.open();
    }
    open() {
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            this.fd = fd;
            this.emit('open', fd)
        })
    }
    clearBuffer() { // 先写入成功后 调用clearBuffer -》 写入缓存的第一个,第一个完成后,在继续第二个
        let data = this.cache.poll();
        if (data) {
            this._write(data.chunk, data.encoding, data.cb);
        } else {
            this.writing = false;
            if (this.needDrain) {
                this.emit('drain')
            }
        }
    }
    // 切片编程
    write(chunk, encoding = this.encoding, cb = () => {}) { // Writable 类中的
        // 1.将数据全部转化成buffer
        chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
        this.len += chunk.length;
        let returnValue = this.len < this.highWaterMark;
        // 当数据写入后 需要在手动的将 this.len--;
        this.needDrain = !returnValue;
        let userCb = cb;
        cb = () => {
            userCb();
            this.clearBuffer(); // 清空缓存逻辑
        }
        // 此时我需要 判断你是第一次给我的,还是不是第一次
        if (!this.writing) {
            // 当前没有正在写入说明是第一次的
            // 需要真正执行写入的操作
            this.writing = true;
            this._write(chunk, encoding, cb);
        } else {
            this.cache.offer({
                chunk,
                encoding,
                cb
            });
        }
        return returnValue
    }
    _write(chunk, encoding, cb) {
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this._write(chunk, encoding, cb))
        }
        fs.write(this.fd, chunk, 0, chunk.length, this.offset, (err, written) => {
            this.offset += written; // 维护偏移量
            this.len -= written; // 把缓存的个数减少
            cb(); // 写入成功了
        });
    }
}
module.exports = WriteStream

实现pipe

const EventEmitter = require('events');
const fs = require('fs')
class ReadStream extends EventEmitter {
    constructor(path, options = {}) {
        super();
        // 放在实例上
        this.path = path;
        this.flags = options.flags || 'r';
        this.encoding = options.encoding || null;
        this.autoClose = options.autoClose || true;
        this.start = options.start || 0;
        this.end = options.end;
        this.highWaterMark = options.highWaterMark || 64 * 1024
        this.flowing = false; // pause resume
        this.open(); // 文件打开操作 注意这个方法是异步的
        // 注意用户监听了data事件 才需要读取
        this.on('newListener', function(type) {
            if (type === 'data') {
                this.flowing = true;
                this.read();
            }
        })
        this.offset = this.start; // 默认start = offset
    }
    pipe(ws) {
        this.on('data', (data) => {
            let flag = ws.write(data);
            if (!flag) {
                //写不下了,不要再读了
                this.pause();
            }
        })
        ws.on('drain', () => {
            // 写完缓存的再去读
            this.resume();
        })
    }
    resume() {
        if (!this.flowing) {
            this.flowing = true;
            this.read();
        }
    }
    pause() {
        this.flowing = false;
    }
    read() { // once events模块中的绑定一次
        // 希望在open之后才能拿到fd
        if (typeof this.fd !== 'number') {
            return this.once('open', () => this.read())
        }
        let howMutchToRead = this.end ? Math.min(this.end - this.offset + 1, this.highWaterMark) : this.highWaterMark;
        const buffer = Buffer.alloc(howMutchToRead);
        // 读取文件中内容,每次读取this.highWaterMark个
        // 123   4
        fs.read(this.fd, buffer, 0, howMutchToRead, this.offset, (err, bytesRead) => {
            if (bytesRead) {
                this.offset += bytesRead;
                this.emit('data', buffer.slice(0, bytesRead));
                if (this.flowing) { // 用于看是否递归读取
                    this.read();
                }
            } else {
                this.emit('end');
                this.destroy();
            }
        })
    }
    destroy(err) {
        if (err) {
            this.emit('error', err);
        }
        if (this.autoClose) {
            fs.close(this.fd, () => this.emit('close'))
        }
    }
    open() {
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) {
                return this.destroy(err)
            }
            this.fd = fd;
            this.emit('open', fd)
        })
    }

}

module.exports = ReadStream

其实就是结合了可读流和可写流,pipe方法再可读流中实现

pipe的使用

const fs = require('fs');
const ReadStream = require('./ReadStream');
const WriteStream = require('./WriteStream')
// 也可以使用自己实现的
//const rs = fs.createReadStream('./a.txt',{highWaterMark:4});
const rs = new ReadStream('./a.txt',{highWaterMark:4});
const ws = new WriteStream('./b.txt',{highWaterMark:1});
// 对 on('data') write() end() close 的封装

rs.pipe(ws); // 这个方法是同步还是异步? 异步, 缺陷无法看到具体的过程


// pipe的目的是可以 读取一点写入一点 监听可读流的触发事件,
//将获取到的数据写入到可写流中,如果返回false,则暂停读取,读取完毕后触发drain事件,在继续读取,
//直到最终完毕,我们无法去对读取的数据进行操作;如果需要操作读取到的数据 需要使用on('data')

树结构

二叉树

class Node {
    constructor(element, parent) {
        this.element = element;
        this.parent = parent;
        this.left = null;
        this.right = null;
    }
}
class Tree {
    constructor() {
        this.root = null;
    }
    add(element) {
        if (this.root === null) {
           return  this.root = new Node(element);
        }
        // 可以用递归,用循环就可以了
        let currentNode = this.root; // 更新当前节点
        let parent;
        let compare;
        // 遍历 找到 合适的插入的位置
        while (currentNode) {
            compare = element > currentNode.element  ;
            parent = currentNode; // 遍历前先记录节点
            if (compare) { //  作比较 更新节点
                // 接着以右边的为根节点
                currentNode = currentNode.right
            } else {
                currentNode = currentNode.left // 插入8的时候 右边没有人了
            }
        }
        // compare; // 放左还是放右边
        // parent; // 放到谁的身上
        let node = new Node(element, parent)
        if (compare) {
            parent.right = node
        } else {
            parent.left = node
        }
        // 如何实现 左边和右边分开存放
        // let currentNode = this.root;
        // if(currentNode.element < element){ // 比根节点大
        //     currentNode.right = new Node(element,currentNode)
        // }else{
        //     currentNode.left = new Node(element,currentNode)
        // }
    }
}

let tree = new Tree();
[10, 8, 19, 6, 15, 22, 20].forEach(item => {
    tree.add(item);
});
console.dir(tree,{depth:1000});


// 树的遍历 和 文件夹的操作
// 网络 http相关的内容

node的网络

  • 源端口号、目的端口号,指代的是发送方随机端口,目标端对应的端口
  • 32位序列号是用于对数据包进行标记,方便重组
  • 4位首部长度:单位是字节,4位最大能表示15,所以头部长度最大为60
  • URG:紧急新号、ACK:确认信号、PSH:应该从TCP缓冲区读走数据、 RST:断开重新连接、SYN:建立连接、FIN:表示要断开
  • 校验和:用来做差错控制,看传输的报文段是否损坏
  • 紧急指针:用来发送紧急数据使用

一.OSI七层模型 (Open System Interconnection)

OSI七层模型,是理想化的模型,为什么要分层? 将复杂的流程分解为几个功能实现复杂问题简单化

送快递: 1) 准备要发货的内容 2) 打包 3)添加地址信息 4)物流打包发货 5) 找到对应的地址 6)用交通工具传输 7)拆包,发送到客户手中

  • 物理层:( 主要关心如何传输信号。 )传输数据用的都是0、1 二进制数表示。双绞线 (低电平0) (高电平1) 光纤
  • 数据链路层:(主要关心两个设备之间传递数据)建立逻辑链接,将数据组合成数据帧进行传递 MAC头部 (交换机 MAC地址) 帧的最大长度是1500 (MTU Transmission Unit
  • 网络层:(主要关心的是寻址)进行逻辑寻址,定位到对方,找到最短的路(无法通过MAC地址定位到对方) IP头部 (数据包) (路由器)
  • 传输层:(主要提供安全及数据完整性保障 )网络层不可靠,保证可靠的传输 TCP头 (数据段)
  • 会话层:建立和管理会话的
  • 表示层:数据的表示、安全、压缩
  • 应用层:用户最终使用的接口

底层是为了上层提供服务的

二.TCP/IP参考模型 (五层模型)

Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议 ,不仅仅指两个协议(协议簇)

三.什么是协议?

协议就是通信的规则 以http协议当做范例来说(协议就是对数据的封装 + 传输)

  • 数据链路层、物理层 :物理设备
  • 网络层:IP ARP RARPICMP IGMP
    • ARP 协议:Address Resolution Protocol 从ip地址获取mac地址 (局域网) RARP (通信由mac地址通信,通过自己的mac地址,对方的ip,获取对方的mac地址)
    • IP协议:寻址通过路由器查找,将消息发送给对方路由器,通过ARP协议,发送自己的mac地址
  • 传输层: TCPUDP
  • 应用层:HTTPDNS(域名和ip做一个映射、会有缓存)、FTPTFTPSMTPSNMP

四.传输层 TCP

tcp 传输控制协议(Transimision Control Protocal )是一种可靠的、面向连接的协议,传输效率低(为了在不可靠的IP 层的基础上建立可靠的传输层,必然要经历各种检测和重发)。

TCP提供全双工服务、即数据可在同一时间双向传输(应用层websocket也是使用TCP三次握手加一次websocket握手实现的)

1)TCP数据格式

image.png

  • 源端口号、目的端口号,指代的是发送方随机端口,目标端对应的端口
  • 32位序列号是用于对数据包进行标记,方便重组
  • 4位首部长度:单位是字节,4位最大能表示15,所以头部长度最大为60
  • URG:紧急新号、ACK:确认信号、PSH:应该从TCP缓冲区读走数据、 RST:断开重新连接、SYN:建立连接、FIN:表示要断开
  • 校验和:用来做差错控制,看传输的报文段是否损坏
  • 紧急指针:用来发送紧急数据使用

TCP 对数据进行分段打包传输,对每个数据包编号控制顺序,实现重发机制,流量控制避免拥塞

通过**wireshark抓包**,来分析网络底层协议

client.js

const net = require('net');
const socket = new net.Socket();
socket.connect(8080, 'localhost');
socket.on('connect', function(data) {
    socket.write('connect server');
});
socket.on('data', function(data) {
    console.log(data.toString())
})
socket.on('error', function(error) {
    console.log(error);
});

server.js

const net = require('net');
const server = net.createServer(function(socket){
    socket.on('data',function (data) {
        socket.write('server:hello');
    });
    socket.on('end',function () {
        console.log('客户端关闭')
    })
})
server.on('error',function(err){
    console.log(err);
})
server.listen(8080);

image.png

  • 三次握手
    • 1)我能主动给你打电话吗? 2)当然可以啊!那我也能给你打电话吗?
    • 3)可以的呢,建立连接成功!

之所以需要三次握手,是因为我们需要确定客户端和服务端都能确定对方具有发送和接收的能力

  • 第一次握手:客户端发送建立连接信息,服务端收到后确定客户端具有发送能力,随后 发送ack=x+1

  • 第二次握手:客户端收到后就清楚了,服务器端拥有收发能力(由ack=x+1,客户端清楚的知道了服务端收到了第一次握手的信息),但这时,服务器端还不知道客户端是否具有接收的能力

  • 第三次握手 ack=y+1 这样就能确定双方都具有收发能力了(第二次握手时,当客户端收到信息时,对于客户端来说,连接就可以说是已经建立起来了(双方收发能力已确定)),客户端这次握手就可以携带数据了

  • 另外:如果是两次握手,那么就意味着服务器在收到客户端的建立连接请求后,只要进行回应,连接就视为已建立。如果,一个建立链接请求在中间阻塞了,客户端没有收到响应于是重发,服务端响应,连接建立。传送信息后连接关闭。这时,上次的请求到了,服务端以为是正常的,于是回应,可客户端已经关闭,于是服务端就会一直等待双方传输数据、浪费了服务器的性能

之所以四次挥手还有time_wait,是为了确定双方都关闭了收发数据的能力

  • 四次挥手
    • 1)我们分手吧 2)收到分手的信息
    • 3)好吧,分就分吧 4)行,那就到这里了

2)滑动窗口

  • 滑动窗口:TCP是全双工的,所以发送端有发送缓存区;接收端有接收缓存区,要发送的数据都放到发送者的缓存区,发送窗口(要被发送的数据)就是要发送缓存中的哪一部分

  • 核心是流量控制:在建立连接时,接收端会告诉发送端自己的窗口大小(rwnd),每次接收端收到数据后都会再次确认(rwnd)大小,如果值为0,停止发送数据。 (并发送窗口探测包,持续监测窗口大小)

  • Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段 (TCP内部控制)

  • Cork算法当达到MSS(Maximum Segment Size )值时统一进行发送(此值就是帧的大小 - ip头 - tcp头 = 1460个字节)

3)TCP拥塞处理

慢启动、拥塞避免、快重传和快恢复

举例:假设接收方窗口大小是无限的,接收到数据后就能发送ACK包,那么传输数据主要是依赖于网络带宽,带宽的大小是有限的。

  • TCP 维护一个拥塞窗口cwnd (congestion window)变量 ,在传输过程正没有拥塞就将此值增大。如果出现拥塞(超时重传 RTO(Retransmission TimeOut) )就将窗口值减少。
  • cwnd < ssthresh 使用慢开始算法
  • cwnd > ssthresh使用拥塞避免算法
  • ROT时更新 ssthresh值为当前窗口的一半,更新cwnd = 1

1.Tahoe版本

image.png

  • 传输轮次:RTT (Round-trip time) ,从发送到确认信号的时间

  • cwnd控制发送窗口的大小。

2.Reno算法

  • 快重传,可能在发送的过程中出现丢包情况。此时不要立即回退到慢开始阶段,而是对已经收到的报文重复确认,如果确认次数达到3此,则立即进行重传 快恢复算法 (减少超时重传机制的出现),降低重置cwnd的频率。

  • 更新ssthresh值和cwnd值为相同

其他算法

五.DNS解析服务

DNS是Domain Name Service的缩写,DNS服务器进行域名和与之对应的IP地址转换的服务器

  • 顶级域名 .com
  • 二级域名 .com.cn、 三级域名 www.zf.com.cn, 有多少个点就是几级域名

访问过程:我们访问zf.com.cn,会先通过**DNS服务器**查找离自己最近的根服务器,通过根服务器找到.cn服务器,将ip返回给DNS服务器,DNS服务器会继续像此ip发送请求,去查找对应.cn.com对应的ip,获取最终的ip地址。缓存到DNS服务器上

六.HTTP

1.发展历程

  • HTTP/0.9 在传输过程中没有请求头和请求体,服务器响应没有返回头信息,内容采用ASCII字符流来进行传输 HTML
  • HTTP/1.0 增加了请求头和响应头,实现多类型数据传输
  • HTTP/1.1 默认开启持久链接,在一个TCP链接上可以传输多个HTTP请求 , 采用管线化的方式(每个域名最多维护6个TCP持久链接)解决队头阻塞问题 (服务端需要按顺序依次处理请求)。完美支持数据分块传输(chunk transfer),并引入客户端cookie机制、安全机制等。
  • HTTP/2.0 解决网络带宽使用率低 (TCP慢启动,多个TCP竞争带宽,队头阻塞)采用多路复用机制(一个域名使用一个TCP长链接,通过二进制分帧层来实现)。头部压缩(HPACK)、及服务端推送
  • HTTP/3.0 解决TCP队头阻塞问题, 采用QUIC协议。QUIC协议是基于UDP的 (目前:支持和部署是最大的问题)
  • HTTP明文传输,在传输过程中会经历路由器、运营商等环节,数据有可能被窃取或篡改 (安全问题

对比HTTP/1.1 和 HTTP/2 的差异

2.请求过程

image.png

  • http是不保存状态的协议,使用cookie来管理状态 (登录 先给你cookie 我可以看一下你有没有cookie)

  • 为了防止每次请求都会造成无谓的tcp链接建立和断开,所以采用保持链接的方式 keep-alive

  • 以前发送请求后需要等待并收到响应,才能发下一个,现在都是管线化的方式

回顾:

用来解决大文件读取问题的,我们可以自己指定读取的位置和读取的大小,客户端发送给服务端的数据都是一段段,我们接受客户端的数据 ,也是用流的方式。

  • 流的模式
    • 可读流(on('data') on('end') push(数据|null))
    • 可写流 (ws.write() ws.end() string or buffer) 指定写入内容的位置,将内容分开写入
    • tcp 双工流 (能读,能写) socket.write socket.end socket.on('data')
    • process.stdin.on() process.stdout.write()
    • 转化流 transform模式

pipe管道,可以将读取的内容,发送给写入 rs.pipe(ws);
pipe实现的思想 读主要靠的是发布订阅模式 (多个异步如何进行拆分),写入时多个异步并发,根据异步顺序造队列 实现按顺序依次执行

栈 队列 链表 树

  • 栈 队列 链表 线性结构
  • 能手写单向链表、链表时如何反转的
  • 树结构:二叉搜索树、树的遍历(先序遍历 中序遍历 后续遍历。层序遍历(广度遍历))

反转二叉树

文件操作的方法

  • fs.mkdir fs.rmdir fs.stat() statObj.isDirectory() statObj.isFile()
  • fs.readdir fs.unlink.... (fs.stat fs.access)

tcp

  • 七层模型 (真实使用是五层模型),每一层做什么事情的

  • (物、数(一个帧能传递多大))、网 (ip arp)、传(TCP、udp)、(会、表、应)

  • tcp协议组成 http请求最多chrome而言是6个(同一个域名而言)。需要开辟6个tcp通道 三次握手。http2 就一个通道

  • http针对同一个域名最多创建 6个tcp链接来传递数据 (域名切片技术,域名多了会很好吗?DNS解析) 队头阻塞 (tcp竞争带宽) (慢启动)

  • http2 为了减少tcp创建 每个域名只建立一个链接 阻塞问题,http2没有解决这个问题(如果丢包率高,http2 性能还不如http1.0) 头部压缩 (减少慢启动)

  • 三次握手四次断开 (掌握握手和断开的机制 ACK SEQ FIN SYNC LEN)

  • 滑动窗口?(两个窗口,不停的滑动来确定发送的数据的区域和接收方的缓存区域)TCP拥塞控制?

一个tcp包到底能传递多大 帧的长度最大是1500 - ip协议头有20个字节 - tcp 头部最少20个字节(可选部分不填的情况下) =》 所以数据最大1460个字节

从输入url 到 展现网站发生了什么 (域名-> ip地址 dns解析) 建立连接 (三次握手和四次断开) tcp的特点协议(滑动窗口、tcp拥塞处理)-> 网络层寻址 -> mac -> mac 物理层

HTPP

http中的状态码 (一般是客户端和服务端提前约定好的,但是也可以自定义)

  • 1(101websocket)
  • 2 (成功)
  • 3(缓存和重定向)
  • 4(客户端错误)
  • 5 (服务端错误)

2开头 请求成功

  • 200 成功
  • 204 成功了但是没有返回具体内容
  • 206 分片传输 数据较大 我只要部分

3开头,资源相关

  • 301 302 区别 301资源永久重定向了

  • 302:资源暂时重定向了,还可能回来,也可能去其他位置 一心一意 三心二意

  • 304 缓存 代表服务器告诉你本地缓存资源可用,不需要重新拉取

  • 400 参数错误 服务器不知道你发送过来的是什么

  • 401 用户没有权限(没有登录)

  • 403 登录了还是没权限

  • 404 找不到

  • 405 一般是cors的复杂请求 提前发送预检请求 客户端请求方法 服务端不支持

  • 416 请求范围无效

  • 500 服务器内部错误,无法完成请求

  • 502 做代理请求的时候,无效响应

  • 504 网关错误 超时了,无法获取响应

  • 505 http的版本不支持

curl可以发送请求 curl --header "" --data "" -v 网址 , 状态码都是给浏览器来使用的

http中的请求方法

  • 开发的时候 大部分会遵循Restful“风格”,对资源的增删改查,可以使用get post put delete 来表示
  • /user post 增加用户
  • /user get 获取用户 获取所有
  • /user/:id get 表示获取某个用户
  • /user delete 删除用户
  • options 请求 (只有使用cors跨域时出现),只有在“复杂”请求的时候才会出现 (默认get和post都是简单请求(如果自己定义了header信息,这是就会变成复杂请求),如果发送的请求方法不是get和post就是复杂请求) 预检请求的时间可以自己控制 (可以控制发送options间隔时间,服务端可以控制)

get和post区别? get请求没有请求体(而且不安全),post理论上来说数据可以没有大小限制 (相对get请求安全一些 http默认传递数据都是明文的)

遵循规范代码写起来会好理解一些

常见跨域的方案有哪些? jsonp、iframe、"cors后端配置"、"nginx反向代理"、"websocket”、window.name..... 请求的域名和服务器的域名 (包括端口不一致 就会出现跨域)

http中的报文

  • 协议的概念 封装+传输 (http他会对内容进行添加对应的标识) image.png
// http是node内置模块 可以直接来使用

const http = require('http');
const url = require('url');
// request (获取请求的信息) -> response (给浏览器写数据使用response)
// 流:http 内部是基于tcp的(net模块,socket双向通信) http1.1 他是一个半双工的
// 内部基于socket 将其分割出了 request,response 底层实现还是要基于socket

// 底层基于发布订阅模式
// 底层用socket来通信,http会增加一些header信息,请求来了之后需要在socket中读取数据,并解析成请求头
// 学http就是学header, 还有解析请求 ,响应数据

// url 由多部分组成
// http://username:password@www.zhufeng.com:80/a?a=1#aaa
// console.log(url.parse('http://username:password@www.zhufeng.com:80/a?a=1#aaa',true))
const server = http.createServer((req,res) => {
    // 先获取请求行 请求方法 请求路径 版本号
    console.log('请求行-----start---------')
    console.log(req.method); // 请求方法是大写的
    console.log(req.url); // 请求路径是从 路径开始 到hash的前面,默认没写路径就是/,/代表的是服务端根路径
    const {pathname,query} = url.parse(req.url,true);

    console.log(pathname,query); // query就是get请求的参数
    console.log('请求行-----end---------')
    console.log('请求头-----start---------')
    console.log(req.headers); // 获取浏览器的请求头,node中所有的请求头都是小写的
    console.log('请求头-----end---------');

    // post请求和put请求有请求体  req是可读流
    // 大文件上传需要分片,或者用客户端上传
    let chunk = [];
    console.log('读取请求体-----start---------');
    req.on('data',function (data) { // 可读流读取的数据都是buffer类型
        chunk.push(data); // 因为服务端接受到的数据可能是分段传输的,我们需要自己将传输的数据拼接起来
    });
    req.on('end',function () { // 将浏览器发送的数据全部读取完毕
        console.log(Buffer.concat(chunk).toString())
        console.log('读取请求体-----end---------');
    })

    // 响应状态码 ,可以字节设定一般情况不设定
    // res.statusCode = 500;  // 更改浏览器响应的状态
    // res.statusMessage = 'my define';

    // 响应头  res就是一个可写流
    res.setHeader('MyHeader',1);

    // 响应体 (如果是路径 那就把响应内容返回给页面,如果是ajax 则放到ajax中的向应力)
    res.write('hello'); // socket.write
    res.write('world');
    res.end('ok'); // 写完了  end => write + close
});
// server.on('request',function (req,res) {
//     console.log('client come on')
// })
server.listen(4000,function () { // 监听成功后的回调
    console.log('server start 4000')
});
// 每次更新代码需要重新启动服务,才能运行最新代码
// nodemon 开发时可以使用nodemon 监控文件变化 重新启动
// npm install nodemon -g

image.png

结果 image.png

http中的header应用(重要)

1.缓存:304 强制缓存、协商缓存

  • 缓存指代的是静态文件的缓存(如js、图片、文本等),分为强制缓存和协商缓存
  • 命中强制缓存的话就不会再向服务器发送请求
  • 协商缓存:在强制缓存不再鲜活时,会去向服务器发送当前静态文件的某些信息(eTag、修改时间)。服务器会对比然后确定缓存是否有效,有效,就会采用缓存,否则就会重新发送请求拉取新的静态文件

没有缓存方案的服务

image.png

const http=require('http')
const url=require('url')
const path=require('path')
const fs=require('fs')
const mime = require('mime');
// 缓存指代的是静态文件的缓存
// 缓存:强制缓存 (不会再次向服务器发起请求), 对比缓存、协商缓存
const server =http.createServer((req,res)=>{

    let {pathname,query}=url.parse(req.url,true)
    console.log("pathname",pathname)
    let filePath=path.join(__dirname,'public',pathname)
    console.log("filePath",filePath)

    fs.stat(filePath,(err,statObj)=>{
        if (err){
            res.statusCode=404;
            res.end('NOT FOUND')
        }else {
            if (statObj.isFile()){
                res.setHeader('Content-Type',mime.getType((filePath)+'charset=utf-8'))
                fs.createReadStream(filePath).pipe(res)
            }else {
                // 如果是目录 需要找目录下的index.html
                let htmlPath =path.join(filePath,'index.html')

                fs.access(htmlPath,(err)=>{
                    if (err){
                        res.statusCode=404;
                        res.end('NOT FOUND')
                    }else {
                        res.setHeader('Content-Type',mime.getType((filePath)+'charset=utf-8'))
                        fs.createReadStream(htmlPath).pipe(res)
                    }
                })
            }
        }
    })
})
server.listen(3000,()=>{
    console.log(`server start 3000`)
})

第一次发送的请求一定不会走缓存、哪怕之前有缓存

第一次请求获取到html,html中会包含对js、css的引用,当解析DOM 解析到这些的时候就会再次向服务器发送请求 服务器响应、这时我们就可以将请求下来的js、css静态文件缓存在浏览器中。下次再请求同样的文件时就会按照策略决定是否采用缓存

缓存相关的header字段 Cache-Control

  • max-age:一个相对时间,相对该静态文件的缓存到浏览器的时间
res.setHeader('Cache-Control','max-age=10')

意思是10秒内不要再找服务器要这个静态文件了,可以直接采用缓存

  • Expires : 一个绝对时间,在某日某时xxx 之前都可以使用缓存,不向服务器发请求

    注意:Expires的优先级低于 max-age 如果命中了expires,但是max-age过期了,那么仍会视为过期 另外,expires的时间会采用和浏览器的时间进行对比,如果浏览器的本地时间被修改,那么很可能会导致expires 无效。注意,在强制缓存的时间之内,即使服务器的资源修改了,也不会发送请求

res.setHeader('Expires','new Date(Date.now()+ 10 * 1000).toGMTString ')//绝对时间

在浏览器的缓存的类型是由浏览器来控制的,有disk(到磁盘中),memory、我们无法用代码控制 image.png

  • no-cache:每次都会向服务器发送请求,但是允许浏览器复制 响应数据存入缓存。该字段告诉浏览器、缓存服务器,不管本地副本是否过期,使用资源副本前,一定要到源服务器进行副本有效性校验。 在有 no-cache时,不管有没有过期,都要发送校验,如果没有过期,服务器会响应 304 告诉浏览器可以采用缓存 否则就要重新发送请求 相当于强制缓存失效,如果没有协商缓存所需要的条件的话,那么实际上就等同于没有缓存

  • no-store:每次都会向服务器发送请求,同时不允许浏览器复制 响应 数据存入缓存

协商缓存不想写了

内容类型

当content-type为multipart/form-data或者...时,其上传的二进制会包含如下

image.png

前面的------Web........是分隔符,将参数new FormData()的参数分割开来,其中第一份为空、最后一份无意义 下面就是服务端处理文件上传的代码

let boundary = "--" + req.headers['content-type'].split('=')[1];
let lines = buf.split(boundary).slice(1, -1);
const r = {};
lines.forEach(line => {
    debugger
    // 把header 和 内容进行拆分
    let [head, body] = line.split(`\r\n\r\n`);
    head = head.toString();
    let name = head.match(/name="(.+?)"/)[1];
    if (head.includes('filename')) {
        // 文件上传  将文件内容上传到服务器的上传文件夹中
        let buffer = line.slice(head.length + 4, -2);
        let filename = uuid.v4();
        // 你最终创建完名字之后 还会同步到数据库里 ,下次查找数据库,再找到对应的文件名
        fs.writeFileSync(path.join(uploadPath,filename),buffer)
        r[name] = {
            filename,
            size:buffer.length
            // 文件大小 fs.stat
        }
        // 文件名一般都是随机的
    } else { // key:value
        // 直接把信息放到一个对象中即可
        r[name] = body.toString().slice(0, -2);
    }
})
res.end(JSON.stringify(r));

Koa

express 和 koa的对比

  • express他的源码是es5写的 koa 源码是基于es6来写的
  • express 比较全内置了很多功能 koa 内部核心是非常小巧的(我们可以通过扩展的插件来进行扩展)
  • express 和 koa 都是可以自己去使用来实现mvc功能的,没有约束
  • express他处理异步的方式都是回调函数 koa处理异步的方式都是async + await
  • 学会一个另一个就容易很多。用起来很方便

以下代码可以说已经使用了Koa的80%的功能了

const Koa = require('koa')
//
// const Koa = require('./koa/lib/application');

const app= new Koa()


app.use((ctx,next)=>{
    //可以理解为res.end()
    ctx.body='hello1'
    // ctx 
})

app.listen(3000,()=>{
    console.log('启动成功!')
})

//监听端口号,同node的 server=http.createServer((req,res)=>{}) server.listen(3000,()=>{})
// Koa的 listen 函数源码
// listen(...args) {
//     const server = http.createServer(this.handleRequest);
//     server.listen(...args);
// }

在原生node中,我们创建一个服务需要引入 http 来创建服务,引入path来解析文件路径、需要引入url来解析前端请求过来的url、需要引入fs来进行文件的读写。经常性的引包 和初始化令人头疼

Koa认为,这些能力就应该天生就存在,所以Koa在ctx扩展了请求和响应的方法

ctx上的重要属性:

  • ctx.app: 当前应用实例 可以在app上扩展公共方法

  • ctx.request、ctx.response: Koa自己封装的请求和响应对象,比原生的node 的req、res更加强大

    app.use((ctx)=>{
    //可以理解为res.end()
    ctx.body='hello1'
    // console.log(ctx.req.url);
    // console.log(ctx.url);
      //http://localhost:3000/?a=1
    console.log(ctx.req.query)//undefined
    console.log(ctx.request.query) //[Object: null prototype] { a: '1' }
    })
    

    可以看到,封装的属性拥有更好的使用体验

    console.log(ctx.req.url);// /?a=1
    console.log(ctx.request.req.url);// /?a=1
    

    可以看到,封装的request上也有原生的req 当然,直接使用 ctx.url会有更好的使用体验

  • ctx.req、ctx.res 原生node 上的 reqres,koa中对req和res进行一层抽象 叫request和response。在开发的时候 我们尽量避免操作原生的req和res

    console.log(ctx.req.path);// 原生node的req没有path 属性
    console.log(ctx.request.path); // 打印 /
    
    console.log(ctx.request.path); // 打印 /
    console.log(ctx.path);
        console.log(ctx.request.path); // 打印 /
    console.log(ctx.path);
    
    

中间件执行顺序

const Koa = require('koa');
const app = new Koa();

// logger

app.use(async (ctx, next) => {
    console.log("*****************************************")
    console.log("执行第一个中间件 next前")
    await next();
    console.log("*****************************************")
    console.log("执行第一个中间件 next后")
    const rt = ctx.response.get('X-Response-Time');
    console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time

app.use(async (ctx, next) => {
    const start = Date.now();
    console.log("*****************************************")
    console.log("执行X-Response-Time next前")
    await next();
    console.log("*****************************************")
    console.log("执行X-Response-Time next后")
    const ms = Date.now() - start;
    ctx.set('X-Response-Time', `${ms}ms`);
});

// response

app.use(async ctx => {
    ctx.body = 'Hello World';
});

app.listen(5000);

image.png

简单实现自己的Koa

const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
const Stream = require('stream')
const EventEmitter = require('events');


class Application extends EventEmitter {

    module
    exports = Application

    constructor() {
        super(); // EventEmiiter.call(this);
        this.context = Object.create(context); //  实现每次创建一个应用都有自己的全新上下文
        this.request = Object.create(request);
        this.response = Object.create(response);
        this.middlewares = [];
    }

    use(middleware) {
        // 保存用户写的函数
        this.middlewares.push(middleware);
    }

    createContext(req, res) {
        // 这样的话,每次请求拿到的都是一个新的__proto__指向 this.context的上下文,
        // 既能共享上下文的方法,又能实现隔离
        // 用户如果向上下文上加数据,只会加在第一层上,不会共享给所有请求

        let ctx = Object.create(this.context); // 这个目的是为了每次请求的时候 都拥有自己的上下文,而且自己的上下文是可以获取公共上下文声明的变量、属性
        let request = Object.create(this.request);
        let response = Object.create(this.response);


        ctx.request = request; // 上下文中包含着request
        ctx.req = ctx.request.req = req; // 默认上下文中包含着 原生的req

        ctx.response = response;
        ctx.res = ctx.response.res = res; // 这个的目的和request的含义是一致的,就是可以在我们的response对象中 通过this.res 拿到原生res

        return ctx;

    }

    compose(ctx) {
        // 组合是要将 数组里的函数 一个个执行
        let index = -1;
        const dispatch = (i) => {
            // 如果没有中间件 直接成功即可
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))
            index = i;
            if (this.middlewares.length === i) return Promise.resolve();
            //                                                这个函数是next函数
            // 一个promise 在执行的时候 会等待返回的promise执行完毕

            // () => dispatch(1)
            // () => dispatch(1)
            return Promise.resolve(this.middlewares[i](ctx, () => dispatch(i + 1)))
        }
        return dispatch(0);

    }

    handleRequest = (req, res) => { // 每次请求都会执行此方法
        let ctx = this.createContext(req, res);
        res.statusCode = 404;
        // this.fn(ctx);

        this.compose(ctx).then(() => {
            let _body = ctx.body;
            if (_body) {
                if (typeof _body === 'string' || Buffer.isBuffer(_body)) {
                    return res.end(_body);
                } else if (typeof _body === 'number') {
                    return res.end(_body + '');
                } else if (_body instanceof Stream) {
                    // 可以设置成下载头
                    //res.setHeader('Content-Type','application/octet-stream');
                    // 设置不识别的header 也会变成下载文件,设置对了才行
                    // res.setHeader('Content-Disposition','attachment;filename=FileName.txt');
                    return _body.pipe(res);
                } else if (typeof _body == 'object') {
                    return res.end(JSON.stringify(_body));
                }
            } else {
                res.end(`Not Found`)
            }
        }).catch((err) => {
            this.emit('error', err);
        })


      
    } 
    
    listen(...args){
            const server = http.createServer(this.handleRequest)
            server.listen(...args)
        }
}
module.exports= Application

实现每次创建一个应用都有自己的全新上下文,即我多次new Koa(),使用不同的上下文

constructor() {
        super(); // EventEmiiter.call(this);
        this.context = Object.create(context); //  实现每次创建一个应用都有自己的全新上下文
        this.request = Object.create(request);
        this.response = Object.create(response);
        this.middlewares = [];
    }

中间件的作用:

可以扩展 属性和方法

还可以做权限处理,如果有权限 做一件事 没权限 做其他事 鉴权

可以决定是否向下执行

基于promisefs

const fs = require('fs').promises;

处理静态文件下载的 中间件

const path = require('path');
const fs = require('fs').promises;
const mime = require('mime')

// 1.中间件的作用 可以扩展属性和方法
// 2.还可以做权限处理,如果有权限 做一件事 没权限 做其他事 鉴权
// 3.可以决定是否向下执行

function static(dirname) {
    return async (ctx, next) => {
        let filePath = path.join(dirname, ctx.path);
        console.log("***************",filePath)
        // 如果文件路径 不是文件的话 就不能处理了,需要调用下一个中间件,如果自己能处理。就不需要向下执行了
        try {
            const statObj = await fs.stat(filePath);
            if (statObj.isDirectory()) {
                filePath = path.join(filePath, 'index.html')
            }
            ctx.set('Content-Type',mime.getType(filePath)+';charset=utf-8')
            ctx.body = await fs.readFile(filePath)
        } catch {
            await next(); // 自己处理不了 向下执行
        }
    }
}
module.exports = static;

Koa的路由系统: 路由的简单实现

class Layer {
    constructor(method, pathname, callback) {
        this.method = method;
        this.pathname = pathname;
        this.callback = callback
    }
    match(requestPath, requestMethod) {
        return this.pathname === requestPath && requestMethod == this.method
    }
}
class Router {
    constructor() {
        this.stack = [];
    }
    get(pathname, callback) {
        // 调用get 就是来维护映射关系的 
        let layer = new Layer('GET', pathname, callback);
        this.stack.push(layer);
    }
    compose(matchLayers, ctx, next) {
        function dispatch(index) {
            if (index === matchLayers.length) return next(); // 如果走到最后就从路由中间件出去,或者一个没有匹配到也出去
            return Promise.resolve(matchLayers[index].callback(ctx, () => dispatch(index + 1)));
        }
        return dispatch(0);
    }
    routes() { // 中间件
        return async (ctx, next) => {
            // 请求到来时会调用此方法
            let requestPath = ctx.path;
            let requestMethod = ctx.method;

            let matchLayers = this.stack.filter(layer => layer.match(requestPath, requestMethod))
            this.compose(matchLayers, ctx, next);
        }
    }
}

module.exports = Router;

cookie session localStorage sessionStorage 区别

  • 前端存储方式 cookie localStorage sessionStorage indexDb

  • http请求是无状态的(cookie特点可以每次请求的时候自动携带)可以实现用户登录功能. 使用cookie来识别用户

  • 如果单纯的使用cookie,不建议存放敏感信息,如果被劫持到。(cookie是存在客户端,并不安全,用户可以自行篡改)

  • 每个浏览器一般对请求头都有大小限制 cookie 不能大于4k,如果cookie过大,会导致页面白屏。 每次访问服务器 都会浪费流量(合理设置cookie)
    (http-only 其实并不安全 ,在浏览器可以篡改可以模拟)

  • sessionStorage 如果页面不关闭就不会销毁 (单页应用 页面切换,访问时存储滚动条地址、以便切换回来仍在原位)

  • localStorage 特点就是关掉浏览器后数据依然存在,如果不手动清楚一直都在 ,有大小限制5m,每次发请求不会携带 indexDB

  • session特点,在服务器开辟一个空间来存储用户对应的信息(因为放在服务器里,可以存储敏感信息)

  • session基于cookie 的 比cookie安全

  • token -》 jwt -》 jsonwebtoken 不需要服务器存储,没有跨域限制

mongodb的使用

windows安装

  • www.mongodb.com/try/downloa… 4.4.5
  • 需要配置环境变量 C:\Program Files\MongoDB\Server\4.4\bin
  • 安装后 mongod 服务端是自行启动的 , 通过配置文件启动
  • “可视化工具不要安装”

mac安装

  • brew tap mongodb/brew
  • brew install mongodb-community
  • brew services start mongodb-community

链接mongo服务 可以直接使用mongo命令, 需要给mongo设置权限,防止被别人访问

mongo特点 数据库 》 集合 》 文档

可视化工具

  • Robo 3T Robomongo / navicat

配置数据库权限

  • 先创建mongodb的管理员,来管理数据库
show dbs
use admin
db.createUser({user:"jw",pwd:"1234",roles:[{role:"root",db:"admin"}]})


use web
db.createUser({user:"webAdmin",pwd:"1234",roles:[{role:"dbOwner",db:"web"}]})

数据库的基本的增删改查

  • db.collection.insert()
  • db.collection.find()
  • db.collection.update()
  • db.collection.remove()

mongo的默认的使用方式 很多时候并不友好,而且在开发时 我们也不会直接使用命令行来增删改查

mongoose