- 本阶段将带你学习前端圈子中口碑极佳的 React 框架以及它的一些高阶用法、组件库封装、数据流方案、服务端渲染(SSR)、静态站点生成(SSG),同时深入 React 框架内部,剖析 React 部分核心源码和实现,最后还会结合 TypeScript 和蚂蚁金服的 Ant Design 库做出实战。
模块一 Node.js 高级编程(核心模块、模块加载机制)
- Node.js 是现代化前端的依托,已经属于前端开发者的必备技能,通过本模块我们重新认识 Node.js,掌握基于 Node.js 平台的开发,其中包括:文件系统和网络编程,除此之外还会带你开发自己的 Web 服务器
任务一:node基础
- 课程概述
- Nodejs可以做什么
- 轻量级、高性能的Web服务
- 前后端JavaScript同构开发
- 便捷高效的前端工程化
- Nodejs 异步IO和事件驱动
- Nodejs 单线程
- Nodejs 核心模块及API使用
- Nodejs 架构
- Natives modules
- 当前层内容由JS实现
- 提供应用程序可直接调用库,例如fs、path、http等
- JS语音无法直接操作底层硬件设置
- Builtin modules “胶水层”
- V8: 执行JS代码,提供桥梁接口
- V8为Nodejs提供初始化操作,创建了执行上下文环境和作用域
- Libuv: 事件循环、事件队列、异步IO
- 第三方模块:zlib、http、c-ares等
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9HkYAtQT-1631365459610)(./img/1/1630892198124.jpg)]
- 为什么是Nodejs
- IO 是计算机操作过程中最缓慢的环节
- 访问RAM这个级别的设备IO时间消耗为纳秒级别,而在磁盘和网络中访问是毫秒级别
- 在并发处理上,高级语言的实现方式大多采用多线程、或者说多进程的方式。
- 比如采用餐馆的例子,使用多线程,就是有几个人就餐,就准备几个服务员来完成相应的服务。但是如果某个时间点,有很多客户就餐,不可能准备很多的服务员,而无人响应的问题就出现了,大部分时间消耗都来源于客人点菜,服务员其实处于空闲状态。
- Reactor 模式,单线程完成多线程工作。
- 核心思想,只保留一个服务员,客人自己进行点餐,点餐行为结束后呼叫服务员即可
- 是非阻塞的,每个客人进来都有服务员立马给他响应,只不过在他点餐的过程中,这个服务员可能在服务其它客人,使用这个操作,也就避免了多个线程在上下文切换的时候需要考虑的一些如状态保存,时间消耗,以及状态锁的问题。
- 而Node正是基于Reactor模式,再结合JS语言本身所具备的一些单线程,事件驱动的架构和异步编程这样的特性,让单线程可以远离阻塞,通过异步非阻塞的IO来更好的使用CPU资源,并且实现高并发请求的处理
- 这也是为什么历史上尝试着将js移植到其它平台的实现方案其实很多,而Nodejs却是最后最出彩的那个
- 突出优点是IO处理,在非IO的其他领域,也有不适用的地方。比如点餐的客人不需要花时间来思考吃什么,而是立即进行点餐,这个时候,一个服务员显然不够用。对应到程序里,就是CPU密集型
- Nodejs更适用于IO密集型高并发请求
- Nodejs异步IO
- 对于系统而言,IO只有阻塞和非阻塞两种
- 重复调用IO操作,判断IO是否结束
- 常见的轮询技术:read\select\poll\kqueue\event ports
- 期望实现无需主动判断的非阻塞IO
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u4mMbPat-1631365459612)(./img/1/1630895179944.jpg)]
- IO是应用程序的瓶颈所在
- 异步IO提高性能无采用原地等待结果返回
- IO操作属于操作系统级别,平台都有对应实现
- Nodejs单线程配合事件驱动架构及lubuv实现了异步IO
- 事件驱动架构
- 事件驱动架构是软件开发中的通用模式
- 事件驱动、发布订阅、观察者
- 主题发布消息,其它实例接受消息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zgoxsaaa-1631365459613)(./img/1/1630912337185.jpg)]
- Nodejs单线程
- 异步IO、事件驱动、事件循环
- 使用JS实现高效可伸缩的高性能Web服务
- 单线程如何实现高并发
- 异步非阻塞IO配合事件回调通知
- Nodejs 主线程是单线程
- Nodejs平台下的js代码都是v8来执行,而在v8中只有一个主线程来执行js代码,这也是平时说的单线程
- 但是在libuv库中存在线程池,默认情况下有四个线程,网络IO,非网络IO,以及非IO的异步操作,针对网络IO,libuv库就会调用当前平台想对应的IO接口去进行处理,而另外两种就会使用线程池中线程完来完成处理,如果这四个线程不够用,也可以修改响应配置,来增加默认的线程数,但是这个操作一般情况下不需要执行
- Nodejs的优点,单线程也能做到其他语言多线程才能做到的事情,这样一来,也就提高了线程的安全,同样也减少了线程切换导致的cpu开销和内存同步开销,这样的一些问题
- 如果处理cpu密集型的任务,会过多占用cpu,这样一来,后面的逻辑就必须等待,而且单线程也无法体现多核cpu的优势
- 这些问题,在后续的Nodejs版本中也给出了一些解决方案
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rR17MM1r-1631365459615)(./img/1/1630913962608.jpg)]
- Nodejs应用场景
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YrRDX1Ym-1631365459616)(./img/1/1630914076409.jpg)]
- Nodejs实现API服务
- npm init -y
- npm i typescript
- tsc --init
- npm i ts-node -D
- ts-node 文件夹.ts 运行ts文件
- npm i express
- npm i @types/express -D
- tsconfig.json
"resolveJsonModule": true" 在ts文件中持续.json文件导入
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Wide0q4-1631365459617)(./img/1/1630915974223.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y2mSCdmd-1631365459618)(./img/1/1630916300328.jpg)]
- Nodejs全局对象
- 与浏览器平台的window不完全相同
- Nodejs全局对象上挂载许多属性
- 全局对象是JavaScript中的特殊对象
- Nodejs中全局对象是global
- Global的根本作用就是作为宿主
- 全局对象可以看做是全局变量的宿主
- Nodejs 常见全局变量
- __filename: 返回正在执行脚本文件的绝对路径
- __dirname: 返回正在执行脚本所在目录
- timer类函数: 执行顺序与事件循环间的关系
- process: 提供与当前进程互动的接口
- require: 实现模块的加载
- module、exports: 处理模块的导出
- 默认情况(全局环境)this 是空对象,和global并不是一样的
- 只有在模块中 this === global
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rQwrba5a-1631365459619)(./img/1/1630918209628.jpg)]
- 全局变量-process-1
-
获取进程信息 -
资源: cpu 内存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZ6SgWEz-1631365459620)(./img/1/1630918209628.jpg)]
- process.cpuUsage()
- {user: 用户占用, system: 系统占用}
-
运行环境:运行目录、node环境、cpu架构、用户环境、系统平台
- process.cwd() 运行目录
- process.version() node环境
- process.versions()
- process.arch() cpu架构
- process.env 用户环境
- .NODE_ENV
- .PATH
- .USERPROFILE window平台 mac平台 .HOME 获取管理员目录
- process.platform 系统平台
-
运行状态:启动参数,PID、运行时间
- process.argv
- process.argv0
- exexArgv 获取带-- 的参数
- process.pid ppid
- process.uptime()
- 全局变量-process-2 事件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mrNGbKsF-1631365459621)(./img/1/1630921840997.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5GHI9YVF-1631365459621)(./img/1/1630922519514.jpg)]
任务二:核心模块
- 核心模块-path-1
- 核心模块-path-2
-
用于处理文件/目录的路径 -
path 模块常用API
- basename() 获取路径中基础名称,返回最后一个(文件夹名或文件名)
- 可以传第二个参数文件名后缀(.js),匹配上则返回不带后缀的文件名。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zpIjYNlJ-1631365459622)(./img/1/1630936850490.jpg)]
- dirname() 获取路径中目录名称
- extname() 获取路径中扩展名称
- 返回 path 路径中响应文件的后缀名
- 如果 path 路径当中存在多个点,它匹配的是最后一个点,到结尾的内容
- isAbsolute() 获取路径是否为绝对路径,以/开头
- join() 拼接多个路径片段
- 如果传入一个长度为0的路径片段会忽略,如果只传入一个长度为0的片段,返回.
- resolve() 返回绝对路径
- pasre() 解析路径
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5qf6MwaK-1631365459623)(./img/1/1630937453405.jpg)]
- format() 序列化路径,和解析路径相反
- normalize() 规范化路径
- 全局变量之Buffer
- Buffer让JavaScript可以操作二进制
- 二进制数据、流操作、Buffer
- JavaScript语音起初服务于浏览器平台
- Nodejs平台下JavaScript课实现IO
- IO行为操作的就是二进制数据
- Stream 流操作并非Nodejs独创
- 流操作配合管道实现数据分段传输,比如现在看视频是边下载边看
- 数据的端到端传输会有生产者和消费者
- 生成和消费的过程往往存在等待
- 产生等待时数据存放在哪?
- Nodejs中Buffer是一片内存空间
- Nodejs中的js代码都是v8执行完成的,因此所有的内存消耗都是属于v8的堆内存,而buffer是v8之外的一片空间
- buffer 空间申请不是v8完成的,但使用层面上,又是由js代码编写控制的,因此在空间回收的时候还是由v8的gc管理和回收
- Buffer 总结
- 无需 require 的一个全局变量
- 实现Nodejs平台下的二进制数据操作
- 不占据V8堆内存大小的内存空间
- 内存的使用由Node来控制,由V8的GC回收
- 一般配合Stream 流使用,充当数据缓冲区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6LF8GAt-1631365459623)(./img/1/1630941003778.jpg)]
- 创建Buffer
- Buffer 是Nodejs的内置类
- 创建Buffer实例
- alloc: 创建指定字节大小的buffer
- allocUnsafe: 创建指定大小的buffer (不安全)
- 在内存中,只要有空闲空间,就会拿过来使用。在空间回收的时候,由于算法的不同,并不能保证空间能得到一个时时的回收,比如一些垃圾的区域,已经没有人用了,但是由于一些空间碎片的存在,里面的数据还是在的,但的确也是没有对象指向它来做引用,所以也有在一瞬间把这样的空间拿过来创建一个新的空间,不会影响最终的数据填充,最终要的是一个内存的大小。但终归是不干净的
- from:接收数据,创建buffer
- buffer是一个类,为什么不直接使用new 操作创建?
- 在nodejs的v6版本之前,是可以直接通过new操作来实例化,但是给到对象的权限太大了,所以在后续的高版本nodejs中做了处理,不推荐直接实例化来创建buffer对象
- Buffer实例方法
- fill: 使用数据填充buffer
- 如果给定的数据不够,则会反复填入,如果超出,则最多写满buffer
- 第二个参数是下标从哪里开始执行填充
- 第三个参数为结束位置,顾头不顾尾
- 注意一下,如果写入数字,会转化为16进制,然后toString(uft8)会转化为对应的字符
- write: 向buffer中写入数据
- 和fill差不多,但是不会重复写入
- 第三个参数为写入的长度
- toString: 从buffer中提取数据
- 第一个参数是编码格式,默认 uft-8
- 第二个参数从哪个下标位置开始截取
- 第三个是结束位置,顾头不顾尾
- slice: 截取buffer
- 第一个参数从哪个下标位置开始截取
- 第二个是结束位置,顾头不顾尾
- 第一个参数可以是一个负数,从后往前截取
- indexOf: 在buffer中查找数据
- 返回找到的位置,没有找到返回-1
- 第二个参数是开始查找的位置
- copy: 拷贝buffer中的数据
- a.copy(b) 将 a的数据拷贝到b中,a、b都是buffer对象
- 第二个参数是从容器的第几个位置开始写入
- 第三个参数是从原buffer的什么位置开始读取
- 第四个参数是读取的结束位置
- Buffer静态方法
- concat:将多个buffer拼接成一个新的buffer,参数是一个数组
- isBuffer:判断当前数据是否为buffer
- Buffer-split实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OPP7NKFq-1631365459624)(./img/1/1630984327668.jpg)]
- 核心模块之FS
- FS是内置核心模块,提供文件系统操作的API
- 代码层面上fs分为基本操作类和常用API
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ywrQ0WU0-1631365459625)(./img/1/1630994000853.jpg)]
- 文件操作API
-
文件读写与拷贝操作
- readFile: 从指定文件中读取数据
- wrireFile: 向指定文件中写入数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zLGMG63o-1631365459626)(./img/1/1630996549607.jpg)]
- appendFile: 追加的方式向指定文件中写入数据
- copyFile: 将文件中的数据拷贝至另一个文件
- watchFile: 对指定文件进行监控
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9fpH0XgE-1631365459627)(./img/1/1630996850109.jpg)]
- md转html实现
const fs = require('fs')
const path = require('path')
const marked = require('marked')
const browserSync = require('browser-sync')
let mdPath = path.join(__dirname, process.argv[2])
let cssPath = path.resolve('github.css')
let htmlPath = mdPath.replace(path.extname(mdPath), '.html')
fs.watchFile(mdPath, (curr, prev) => {
if (curr.mtime !== prev.mtime) {
fs.readFile(mdPath, 'utf-8', (err, data) => {
let htmlStr = marked(data)
fs.readFile(cssPath, 'utf-8', (err, data) => {
let retHtml = temp.replace('{{content}}', htmlStr).replace('{{style}}', data)
fs.writeFile(htmlPath, retHtml, (err) => {
console.log('html 生成成功了')
})
})
})
}
})
browserSync.init({
browser: '',
server: __dirname,
watch: true,
index: path.basename(htmlPath)
})
- 文件打开与关闭
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CKcmgTpm-1631365459627)(./img/1/1631020460185.jpg)]
- 大文件读写操作
- 先把源文件读取到缓冲区,再从缓冲区把文件读取写入到新文件,实现大文件的读写操作
const fs = require('fs')
let buf = Buffer.alloc(10)
buf = Buffer.from('1234567890')
fs.open('b.txt', 'w', (err, wfd) => {
fs.write(wfd, buf, 2, 4, 0, (err, written, buffer) => {
console.log(written, '----')
fs.close(wfd)
})
})
- 文件拷贝自定义实现
const fs = require('fs')
let buf = Buffer.alloc(10)
const BUFFER_SIZE = buf.length
let readOffset = 0
fs.open('a.txt', 'r', (err, rfd) => {
fs.open('b.txt', 'w', (err, wfd) => {
function next () {
fs.read(rfd, buf, 0, BUFFER_SIZE, readOffset, (err, readBytes) => {
if (!readBytes) {
fs.close(rfd, ()=> {})
fs.close(wfd, ()=> {})
console.log('拷贝完成')
return
}
readOffset += readBytes
fs.write(wfd, buf, 0, readBytes, (err, written) => {
next()
})
})
}
next()
})
})
- 目录操作API
- 常见目录操作API
- access: 判断文件或目录是否具备有操作权限
- stat: 获取目录及文件信息
- mkdir: 创建目录
- rmdir: 删除目录
- readdir: 读取目录中内容
- unlink: 删除指定文件
- 目录创建之同步实现
const fs = require('fs')
const path = require('path')
function makeDirSync (dirPath) {
let items = dirPath.split(path.sep)
for(let i = 1; i <= items.length; i++) {
let dir = items.slice(0, i).join(path.sep)
try {
fs.accessSync(dir)
} catch (err) {
fs.mkdirSync(dir)
}
}
}
makeDirSync('a\\b\\c')
- 目录创建之异步实现
const fs = require('fs')
const path = require('path')
const {promisify} = require('util')
const access = promisify(fs.access)
const mkdir = promisify(fs.mkdir)
async function myMkdir (dirPath, cb) {
let parts = dirPath.split('/')
for(let index = 1; index <= parts.length; index++) {
let current = parts.slice(0, index).join('/')
try {
await access(current)
} catch (err) {
await mkdir(current)
}
}
cb && cb()
}
myMkdir('a/b/c', () => {
console.log('创建成功')
})
- 目录删除之异步实现
const { dir } = require('console')
const fs = require('fs')
const path = require('path')
function myRmdir (dirPath, cb) {
fs.stat(dirPath, (err, statObj) => {
if (statObj.isDirectory()) {
fs.readdir(dirPath, (err, files) => {
let dirs = files.map(item => {
return path.join(dirPath, item)
})
let index = 0
function next () {
if (index == dirs.length) return fs.rmdir(dirPath, cb)
let current = dirs[index++]
myRmdir(current, next)
}
next()
})
} else {
fs.unlink(dirPath, cb)
}
})
}
myRmdir('tmp', () => {
console.log('删除成功了')
})
- 模块化历程
- 传统开发常见问题
- 命名冲突和污染
- 代码冗余,无效请求多
- 文件间的依赖关系复杂
- 项目难以维护不方便复用
- 模块就是小而精且利于维护的代码片段
- 利用函数、对象、自执行函数实现分块
- 常用模块化规范
- Commonjs 规范
- 是一个超集,是语言层面的规范,类似于ES,模块化只是众多规范中的一种。
- 还可以实现IO流,二进制操作,或者buffer操作规范等等,在node平台下进行开发
- 不过Commonjs模块的加载都是同步完成的,这样就不适合在浏览器端使用,因为在后端运行js代码,模块一般从磁盘中读取,所以速度并不会受到太大的影响。而这种加载机制如果放在浏览器平台下,肯定就会出现问题,因此后来出现了AMD规范
- AMD规范
- 实现异步模块加载的规范
- 最经典的是require.js
- CMD 规范
- 整合了Commonjs与AMD的特点,专门用于实现浏览器平台下模块的加载
- 这种规范下最经典的代码sea.js
- ES modules 规范
- 模块化规范
- 模块化是前端走向工程化中的重要一环
- 早起JavaScript语言层面没有模块化规范
- Commonjs、AMD、CMD、都是模块化规范
- ES6中将模块化纳入标准规范
- 当下常用规范是Commonjs于ESM
- CommonJS规范
- CommonJS规范主要应用于Nodejs
- 它的出现是为了弥补JavaScript没有模块化标准的缺陷,它的制定者希望通过它倒逼浏览器作出一些改变,从而实现JS代码能到处运行,还能够具备开发大型应用的能力,由于浏览器平台本身具备的一些特点,例如数据一半是通过网络进行传输的,而且还存在单线程阻塞的加载方式。因此让Commonjs规范不能适用于浏览器平台
- CommonJS是语言层面上的规范 (是一个超集)
- CommonJS规范
- Nodejs于CommonJS
- 任意一个文件就是一模块,具有独立作用域
- 使用require导入其它模块
- 将模块ID转入requite实现目标模块定位
- module属性
- 任意js文件就是一个模块,可以直接使用module属性
- id: 返回模块标识符,一般是一个绝对路径
- filename: 返回文件模块的绝对路径
- loaded: 返回布尔值,表示模块是否完成加载
- parset: 返回对象存放调用当前模块的模块
- children: 返回数组,存放当前模块调用的其它模块
- exports: 返回当前模块需要暴露的内容
- paths: 返回数组,存放不同目录下的node_modules位置
- module.exports鱼exports有何区别
- 不能直接给exports赋值,这样做就等于切断了exports和module.exports的联系,它就变成了一个局部变量,无法向外部提供数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kXWSvIol-1631365459628)(./img/1/1631068944501.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sq7dNbFX-1631365459629)(./img/1/1631068982450.jpg)]
- require属性
- 基本功能是读入并且执行一个模块文件
- resolve: 返回模块文件绝对路径
- extensions: 依据不同后缀名执行解析操作
- main: 返回主模块对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v5YEhLry-1631365459629)(./img/1/1631069257756.jpg)]
- Nodejs与CommonJS
- 使用module.exports与require实现模块导入与导出
- module 属性及其常见信息获取
- exports 导出数据及其鱼module.exports区别
- CommonJS 规范下的模块同步加载
- require.main 返回主入口模块对象
- 模块分类及加载流程
- 模块加载源码分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IEvXmKCV-1631365459631)(./img/1/1631083665447.jpg)]
- 模块外面包裹了一个函数,执行时(在vm沙箱中执行)通过 .call调用,修改了this的指向(exports),所以模块中this不指向global
- VM模块使用
const fs = require('fs')
const vm = require('vm')
let age = 33
let content = fs.readFileSync('test.txt', 'utf-8')
vm.runInThisContext("age += 10")
console.log(age)
- 模块加载模拟实现-1
- 模块加载模拟实现-2
const { dir } = require('console')
const fs = require('fs')
const path = require('path')
const vm = require('vm')
function Module (id) {
this.id = id
this.exports = {}
console.log(1111)
}
Module._resolveFilename = function (filename) {
let absPath = path.resolve(__dirname, filename)
if (fs.existsSync(absPath)) {
return absPath
} else {
let suffix = Object.keys(Module._extensions)
for(var i=0; i<suffix.length; i++) {
let newPath = absPath + suffix[i]
if (fs.existsSync(newPath)) {
return newPath
}
}
}
throw new Error(`${filename} is not exists`)
}
Module._extensions = {
'.js'(module) {
let content = fs.readFileSync(module.id, 'utf-8')
content = Module.wrapper[0] + content + Module.wrapper[1]
let compileFn = vm.runInThisContext(content)
let exports = module.exports
let dirname = path.dirname(module.id)
let filename = module.id
compileFn.call(exports, exports, myRequire, module, filename, dirname)
},
'.json'(module) {
let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))
module.exports = content
}
}
Module.wrapper = [
"(function (exports, require, module, __filename, __dirname) {",
"})"
]
Module._cache = {}
Module.prototype.load = function () {
let extname = path.extname(this.id)
Module._extensions[extname](this)
}
function myRequire (filename) {
let mPath = Module._resolveFilename(filename)
let cacheModule = Module._cache[mPath]
if (cacheModule) return cacheModule.exports
let module = new Module(mPath)
Module._cache[mPath] = module
module.load()
return module.exports
}
let obj = myRequire('./v')
let obj2 = myRequire('./v')
console.log(obj.age)
- 事件模块
- 核心模块之Events
- 通过EventEmitter类实现事件统一管理
- events 与 EventEmitter
- node.js是基于事件驱动的异步操作架构,内置events模块
- events模块提供了EventEmitter类
- node.js 中很多内置核心模块继承EventEmitter
- EventEmitter常见API
- on: 添加当事件被触发时调用的回调函数
- emit: 触发事件,按照注册同步调用每个事件监听器
- once: 添加当事件在注册之后首次被触发时调用的回调函数
- off: 移除特定的监听器
- const EventEmitter = require(‘event’)
- Nodejs是一个事件驱动的架构,采用异步编程会有一个小的问题,怎么在一个合适的时间拿到异步操作返回的数据,会在会见触发之后,自动执行回调操作,这样里面的数据也就会被返回了。具体来说就是node中有一个event-loop事件循环,每次发布或者触发一个事件的时候,都会把回调放到eventloop中,当主线程执行完成之后,就会执行eventloop或者说是事件环中的事件
- 发布订阅
-
EventEmitter源码调试 -
EventEmitter模拟
function MyEvent () {
this._events = Object.create(null)
}
MyEvent.prototype.on = function (type, callback) {
if (this._events[type]) {
this._events[type].push(callback)
} else {
this._events[type] = [callback]
}
}
MyEvent.prototype.emit = function (type, ...args) {
if (this._events && this._events[type].length) {
this._events[type].forEach((callback) => {
callback.call(this, ...args)
})
}
}
MyEvent.prototype.off = function (type, callback) {
if (this._events && this._events[type]) {
this._events[type] = this._events[type].filter((item) => {
return item !== callback && item.link !== callback
})
}
}
MyEvent.prototype.once = function (type, callback) {
let foo = function (...args) {
callback.call(this, ...args)
this.off(type, foo)
}
foo.link = callback
this.on(type, foo)
}
let ev = new MyEvent()
let fn = function (...data) {
console.log('事件1执行了', data)
}
ev.once('事件1', fn)
ev.emit('事件1', '前')
- 浏览器中的事件环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dkX4Jl4H-1631365459633)(./img/1/1631092680221.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xp2T1K82-1631365459633)(./img/1/1631092768284.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3HjmuaTe-1631365459634)(./img/1/1631093067545.jpg)]
- Nodejs中的事件环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-20wNEUGq-1631365459634)(./img/1/1631096824829.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22umu0lF-1631365459635)(./img/1/1631096900028.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0YiMFjCP-1631365459636)(./img/1/1631097000135.jpg)]
- 微任务有两个执行时机,一个是同步任务执行完成,一个是每次宏任务队列切换的间隙
- 同样是微任务,process.nextTick 优先级高于 promsie
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uy1PuV66-1631365459636)(./img/1/1631097361115.jpg)]
- Nodejs事件环理解
- 旧版Node是只有宏任务队列切换的时候,才会去执行微任务,和浏览器不同,浏览器是每个宏任务切换的时候都是去执行一次微任务队列
- 新版node,改为和浏览器保持一致
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1pn1KLp7-1631365459637)(./img/1/1631097959033.jpg)]
- Nodejs与浏览器事件环区别
- 任务队列数不同
- 浏览器中只有两个任务队列(宏任务和微任务)
- Nodejs中有6个事件队列(还不算微任务队列)
- Nodejs微任务执行时机不同
- 二者都会在同步代码执行完毕后执行微任务
- 浏览器平台下每当一个宏任务执行完毕后就清空微任务
- Nodejs平台在事件队列切换时会去清空微任务
- 微任务优先级不同
- 浏览器事件环中,微任务存放于事件队列,先进先出
- Nodejs中process.nextTick优先promsie.then
- Nodejs事件环常见问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1oJrEMto-1631365459637)(./img/1/1631098643660.jpg)]
- 第一个例子,执行顺序不固定,因为setTimeout 不传参数是0,但是这个并不一定稳定在0秒后加入队列,可能出现延迟
- 第二个,因为在 fs.readFile的回调函数会在poll事件队列中进行执行,执行后把setTimeout加入timers队列中,把setImmediate加入check队列中。根据事件环,在本轮中,poll事件队列执行完成后,会执行 check队列,所以 setImmediate 会优先 setTimeout 输出
- 核心模块之stream
-
ls|grep*.js 可以在linux中过滤出js文件,通过流的方式 -
Node.js 诞生之初就是为了提高IO性能 -
文件操作系统和网络模块实现了流接口 -
Ndoe.js 中的流就是处理流式数据的抽象借口 -
应用程序中为什么使用流来处理数据?
- 同步读取资源文件,用户需要等待数据读取完成
- 资源文件最终一次性加载至内存,开销较大
- v8引擎默认的内存大小是1G多点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HBOP6aAR-1631365459638)(./img/1/1631167544996.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cZ2q9vHm-1631365459639)(./img/1/1631167790781.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9kykN70i-1631365459639)(./img/1/1631167828544.jpg)] -
流处理数据的优势
- 时间效率:流的分段处理可以同时操作多个数据chunk
- 空间效率:同一时间流无须占据大内存空间
- 使用方便:流配合管理,扩展程序变得简单
-
Node.js 内置了stream,它实现了流操作对象 -
Node.js 中流的分类
- Readable: 可读流,能够实现数据的读取
- Writeable: 可写流,能够实现数据的写操作
- Duplex: 双工流,即可读又可写
- Tranform: 转换流,可读可写,还能实现数据转换
-
Node.js 流特点
- Stream模块实现了四个具体的抽象
- 所有流都继承自EventEmitter
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yGtF2FqE-1631365459640)(./img/1/1631169304892.jpg)]
- stream之可读流
- 生产供程序消费数据的流
- 自定义可读流
- 继承stream里的Readable
- 重写 _read 方法调用push产出数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DgAlrRLJ-1631365459641)(./img/1/1631169786455.jpg)]
- 自定义可读流问题
- 底层数据读取完成之后如何处理?
- 数据读取完成之后,给push传入null值,表示数据已经读取完成
- 消费者如何获取可读流中的数据?
- 提供了两个事件,分别为 Readable 和 data 事件
- 消费数据为什么存在二种方式
- 主要为了满足不同的使用场景
- 有些时候只是想要按需读取一定量的数据,有些时候需要源源不断的把底层数据读出
- 流动模式、暂停模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JpY60Is1-1631365459642)(./img/1/1631170479725.jpg)]
- 消费数据
- readable事件:当流中存在可读取数据时触发
- data事件: 当流中数据块传给消费者后触发
- 可读流总结
- 明确数据生产与消费流程
- 利用API实现自定义的可读流
- 明确数据消费的事件使用
const {Readable} = require('stream')
let source = ['lg', 'zce', 'syy']
class MyReadable extends Readable{
constructor(source) {
super()
this.source = source
}
_read() {
let data = this.source.shift() || null
this.push(data)
}
}
let myReadable = new MyReadable(source)
myReadable.on('data', (chunk) => {
console.log(chunk.toString())
})
- stream之可写流
- 自定义可写流
- 继承stream 模块的Writeable
- 重写 _write方法,调用write执行写入
- 可写流事件
- pipe事件: 可读流调用pipe()方法时触发
- unpipe事件: 可读流调用unpipe()方法时触发
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eiuRlDYn-1631365459642)(./img/1/1631171915988.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mmKIFi9v-1631365459643)(./img/1/1631172031036.jpg)]
const {Writable} = require('stream')
class MyWriteable extends Writable{
constructor() {
super()
}
_write(chunk, en, done) {
process.stdout.write(chunk.toString() + '<----')
process.nextTick(done)
}
}
let myWriteable = new MyWriteable()
myWriteable.write('拉勾教育', 'utf-8', () => {
console.log('end')
})
- stream之双工和转换流
- 可读、可写、双工、转换是单一抽象具体实现
- 流操作的核心功能就是处理数据
- Node.js 诞生的初衷就是解决密集型IO事务
- Node.js 中处理数据模块继承了流和EventEmitter
- Duplex是双工流,既能生产又能消费
- 自定义双工流
- 继承Deplex类
- 重写 _read 方法,调用push生产数据
- 重写 _write方法,调用write消费数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q2FbC8NG-1631365459644)(./img/1/1631172781264.jpg)]
- 自定义转换流,底层数据是可以通信的
- 继承Transform 类
- 重写 _transform 类,调用push和callback
- 重写 _flush方法,处理剩余数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T9qeJHkd-1631365459644)(./img/1/1631172911339.jpg)]
- 文件可读流创建和消费
const fs = require('fs')
let rs = fs.createReadStream('test.txt', {
flags: 'r',
encoding: null,
fd: null,
mode: 438,
autoClose: true,
start: 0,
highWaterMark: 4
})
rs.on('open', (fd) => {
console.log(fd, '文件打开了')
})
rs.on('close', () => {
console.log('文件关闭了')
})
let bufferArr = []
rs.on('data', (chunk) => {
bufferArr.push(chunk)
})
rs.on('end', () => {
console.log(Buffer.concat(bufferArr).toString())
console.log('当数据被清空之后')
})
rs.on('error', (err) => {
console.log('出错了')
})
-
文件可读流事件与应用 -
文件可写流
const fs = require('fs')
const ws = fs.createWriteStream('test.txt', {
flags: 'w',
mode: 438,
fd: null,
encoding: "utf-8",
start: 0,
highWaterMark: 3
})
let buf = Buffer.from('abc')
ws.write("2")
ws.end('拉勾教育')
ws.on('error', (err) => {
console.log('出错了')
})
- write执行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cQLWL1pe-1631365459645)(./img/1/1631176336702.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jfXVQZdl-1631365459646)(./img/1/1631176438958.jpg)]
- 控制写入速度
let fs = require('fs')
let ws = fs.createWriteStream('test.txt', {
highWaterMark: 3
})
let source = "拉勾教育".split('')
let num = 0
let flag = true
function executeWrite () {
flag = true
while(num !== 4 && flag) {
flag = ws.write(source[num])
num++
}
}
executeWrite()
ws.on('drain', () => {
console.log('drain 执行了')
executeWrite()
})
- 背压机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjvJ3tK0-1631365459646)(./img/1/1631177714203.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyxsgDQ2-1631365459647)(./img/1/1631178245929.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXJfUcl1-1631365459648)(./img/1/1631178365132.jpg)]
- 数据读取的速度大于写入的速度,虽然有缓冲区,但缓冲区有大小限制。如果不实现背压机制,就可能出现:内存溢出、GC频繁调用、其它进程变慢
let fs = require('fs')
let rs = fs.createReadStream('test.txt', {
highWaterMark: 4
})
let ws = fs.createWriteStream('test1.txt', {
highWaterMark: 1
})
let flag = true
rs.pipe(ws)
-
模拟文件可读流01 -
模拟文件可读流02 -
模拟文件可读流03
const fs = require('fs')
const EventEmitter = require('events')
class MyFileReadStream extends EventEmitter{
constructor(path, options = {}) {
super()
this.path = path
this.flags = options.flags || "r"
this.mode = options.mode || 438
this.autoClose = options.autoClose || true
this.start = options.start || 0
this.end = options.end
this.highWaterMark = options.highWaterMark || 64 * 1024
this.readOffset = 0
this.open()
this.on('newListener', (type) => {
if (type === 'data') {
this.read()
}
})
}
open() {
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) {
this.emit('error', err)
}
this.fd = fd
this.emit('open', fd)
})
}
read() {
if (typeof this.fd !== 'number') {
return this.once('open', this.read)
}
let buf = Buffer.alloc(this.highWaterMark)
let howMuchToRead
howMuchToRead = this.end ? Math.min(this.end - this.readOffset + 1, this.highWaterMark) : this. highWaterMark
fs.read(this.fd, buf, 0, howMuchToRead, this.readOffset, (err, readBytes) => {
if (readBytes) {
this.readOffset += readBytes
this.emit('data', buf.slice(0, readBytes))
this.read()
} else {
this.emit('end')
this.close()
}
})
}
close() {
fs.close(this.fd, () => {
this.emit('close')
})
}
}
let rs = new MyFileReadStream('test.txt', {
end: 7,
highWaterMark: 3
})
rs.on('data', (chunk) => {
console.log(chunk)
})
- 链表结构
- 为什么不采用数组存储数据?
- 数组缺陷
- 数组存储长度具有上限
- 数组存在塌陷问题,数组在执行插入或者删除操作都时候都会去移动其它元素的位置
- 在js中,数组被实现为一个对象,效率会低一些
- 链表是一系列节点的集合
- 每个节点都具有指向下一个节点的属性
- 链表分类
- 双向链表 比较常用,查询速度会更快一些
- 单向链表
- 循环链表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ruKaMiz4-1631365459648)(./img/1/1631197861234.jpg)]
-
单向链表实现-1 -
单向链表实现-2 -
单向链表实现-3 -
单向链表实现队列
class Node{
constructor(element, next) {
this.element = element
this.next = next
}
}
class LinkedList{
constructor(head, size) {
this.head = null
this.size = 0
}
_getNode(index) {
if (index < 0 || index >= this.size) {
throw new Error('越界了')
}
let currentNode = this.head
for (let i = 0; i < index; i++) {
currentNode = currentNode.next
}
return currentNode
}
add(index, element) {
if (arguments.length == 1) {
element = index
index = this.size
}
if (index < 0 || index > this.size) {
throw new Error('cross the border')
}
if (index == 0) {
let head = this.head
this.head = new Node(element, head)
} else {
let prevNode = this._getNode(index - 1)
prevNode.next = new Node(element, prevNode.next)
}
this.size++
}
remove(index) {
let rmNode = null
if (index == 0) {
rmNode = this.head
if (!rmNode) {
return undefined
}
this.head = rmNode.next
} else {
let prevNode = this._getNode(index -1)
rmNode = prevNode.next
prevNode.next = rmNode.next
}
this.size--
return rmNode
}
set(index, element) {
let node = this._getNode(index)
node.element = element
}
get(index) {
return this._getNode(index)
}
clear() {
this.head = null
this.size = 0
}
}
class Queue{
constructor() {
this.linkedList = new LinkedList()
}
enQueue(data) {
this.linkedList.add(data)
}
deQueue() {
return this.linkedList.remove(0)
}
}
const q = new Queue()
q.enQueue('node1')
q.enQueue('node2')
let a = q.deQueue()
a = q.deQueue()
a = q.deQueue()
console.log(a)
-
文件可写流实现-1 -
文件可写流实现-2 -
文件可写流实现-3
const fs = require('fs')
const EventsEmitter = require('events')
const Queue = require('./linkedlist')
class MyWriteStream extends EventsEmitter{
constructor(path, options={}) {
super()
this.path = path
this.flags = options.flags || 'w'
this.mode = options.mode || 438
this.autoClose = options.autoClose || true
this.start = options.start || 0
this.encoding = options.encoding || 'utf8'
this.highWaterMark = options.highWaterMark || 16*1024
this.open()
this.writeoffset = this.start
this.writing = false
this.writeLen = 0
this.needDrain = false
this.cache = new Queue()
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err)
}
this.fd = fd
this.emit('open', fd)
})
}
write(chunk, encoding, cb) {
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
this.writeLen += chunk.length
let flag = this.writeLen < this.highWaterMark
this.needDrain = !flag
if (this.writing) {
this.cache.enQueue({chunk, encoding, cb})
} else {
this.writing = true
this._write(chunk, encoding, () => {
cb()
this._clearBuffer()
})
}
return flag
}
_write(chunk, encoding, cb) {
if (typeof this.fd !== 'number') {
return this.once('open', ()=>{return this._write(chunk, encoding, cb)})
}
fs.write(this.fd, chunk, this.start, chunk.length, this.writeoffset, (err, written) => {
this.writeoffset += written
this.writeLen -= written
cb && cb()
})
}
_clearBuffer() {
let data = this.cache.deQueue()
if (data) {
this._write(data.element.chunk, data.element.encoding, ()=>{
data.element.cb && data.element.cb()
this._clearBuffer()
})
} else {
if (this.needDrain) {
this.needDrain = false
this.emit('drain')
}
}
}
}
const ws = new MyWriteStream('./f9.txt', {})
ws.on('open', (fd) => {
console.log('open---->', fd)
})
let flag = ws.write('1', 'utf8', () => {
console.log('ok1')
})
flag = ws.write('10', 'utf8', () => {
console.log('ok1')
})
flag = ws.write('拉勾教育', 'utf8', () => {
console.log('ok3')
})
ws.on('drain', () => {
console.log('drain')
})
- pipe方法使用
- highWaterMark fs文件可读流默认缓冲区是64kb,fs文件可写流是16kb
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F4GNVN6w-1631365459649)(./img/1/1631202119971.jpg)]
const fs = require('fs')
const myReadStream = require('./ReadStream')
const rs = new myReadStream('./f9.txt')
const ws = fs.createWriteStream('./f10.txt')
rs.pipe(ws)
任务三:通信
- 通信基本原理
- 主机之间需要有传输介质
- 主机上必须有网卡设备
- 网卡可以完成信号的调制与解调制,其中把二进制转换为高低电压的过程就是数据的调制过程
- A、B两主机通信,A主机把包装好的数据经过A的网卡之后完成调制,处理成电信号,再经过网线,就传递到了B主机上,B主机的网卡进行解调制,拿到封装后的数据,再由B主机本身的一个通信体系进行拆包解包,最终在某个应用程序中获取到这个数据
- A、B主机通信前有一个协商网络速率的工作,这是因为A、B两台主机的网卡可能速率可以不一样,每秒传输的电信号也就不一样了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OvX2iD8X-1631365459649)(./img/1/1631235862486.jpg)]
- 网络通讯方式
- 常见通讯方式
- 如何建立多台主机互连
- 通过交换机可以让多台设备连接,当通过交换机把多台主机连接在一起之后,也就形成了一个网络,而这个网络就是局域网
- 如何定位局域网中的其它主机?
- 通过Mac地址来唯一标识一个主机,称之为物理地址
- A主机可以把消息发送给交换机,交换机在获取这个消息后再去通知其他机器,以广播的方式,B主机会发现它找的是自己,而其它主机会把这个消息当垃圾丢弃
- 这种模式的问题
- 交换机的进口数量有上限
- 局域网存在大量主机会造成广播风暴
- 不同局域网的主机,要通过路由器来完成交换
- 1局域网的A主机要访问2局域网的B主机,不仅需要知道B的网卡地址,还需要知道目标再哪个网络里,这里就需要用到IP地址
- 数据再发送的时候不仅包含程序接受的数据,还有原Mac地址,原IP地址,目标Mac,目标IP等等。这些内容,会先组装成一个大的数据,然后被发送到路由上边,然后路由器是可以识别要在的主机在不在这一个局域网内的,如果它不在这个局域网内,那么会按照路由表上记录的路由信息来帮助我们通过网络路由找到对应的网络,之后再把数据发送过去,发送到对应的网络,再由那边的交换机进行最后的定位
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R5HMvLdB-1631365459650)(./img/1/1631235986206.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S0KkkBfv-1631365459651)(./img/1/1631236477193.jpg)]
- 网络层次模型
- 最常见的是OSI七层模型,和TCP/IP四层模型
- 这里提到的TCP/IP不单指这两个协议,只是在OSI七层模型上进行了一些简化,然后形成了另外的所谓网络通信模型。
- 不论采用哪一种模型,都是对通信过程进行分层,每层当中也存在着很多的协议,例如HTTP就属于应用层协议,而TCP、UDP就是传输层协议
- OSI 七层模型
- 应用层: 用户与网络的接口,以及应用程序与网络的接口
- 可以利用不同协议完成用户请求的各种服务
- Http协议完成网站服务
- Ftp协议完成文件传输服务
- ssh 完成远程登陆服务
- 表示层: 数据加密、转换、压缩
- 会话层: 控制网络链接建立与终止
- 传输层: 控制数据传输的可靠性
- 基于端口的协议层,所以数据在封装的时候还必须携带目标程序所占用的端口号,例如访问网站携带的80端口
- 网络层: 明确目标网络
- 通过IP来找目标网络,常见的就是IP协议,通过IP地址可以确定源和目标的网络
- 数据链路层: 确定目标主机
- 已经确定了目标网络,进入了某一个局域网内,接下里,通过mac地址确定目标主机
- 最常见的就是Arp寻址协议
- 物理层: 各种物理设备和标准
- 而TCP/IP 就是把前三层进行合并,统一叫应用层。再把数据链路层和物理层合并叫接入层,网络层改名叫主机层
- 数据封装与解封装
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vUvC98Dy-1631365459652)(./img/1/1631239602007.jpg)]
- TCP三次握手与四次挥手
- TCP 协议
- TCP属于传输层协议,基于端口
- TCP是面向连接的协议
- TCP用于处理实时通信,具有数据传输可靠性高的特点,因为有这个优点。在传输速度上,相对于UDP会低一些
- 主机之间要想通信想要先建立双向数据通道
- TCP 的握手和挥手本质上都是四次
- 常见控制字段
- SYN = 1 表示请求建立连接
- FIN = 1 表示请求断开链接
- ACK = 1 表示数据信息确认
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dii8CvcW-1631365459653)(./img/1/1631239853355.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nPywXlgf-1631365459654)(./img/1/1631240175151.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XfhCbJZT-1631365459654)(./img/1/1631240796781.jpg)]
- 创建TCP通信
- Net 模块实现了底层通信接口
- 可以实现基于流操作的TCP,或者IPC的客户端和服务端
- 通信过程
- 创建服务端: 接收和回写客户端数据
- 创建客户端: 发送和接收服务端数据
- 数据传输: 内置服务事件和方法读写数据
- 通信事件
- listening事件: 调用server.listen 方法之后触发
- connection事件: 新的链接建立是触发
- close事件: 当server关闭时触发
- error事件: 当错误出现的时候触发
- 通信事件&方法
- data事件: 当接收到数据的时候触发该事件
- write方法: 在socket上发送数据,默认UT8编码
- end操作: 当socket的一端发送FIN包时触发,结束可读端
- TCP粘包及解决
- 发送端累积数据统一发送
- 接收端缓冲数据之后再消费
- 好处是可以减少IO操作带来的性能消耗,对数据的使用就会产生粘包的问题
- 数据是被放在缓存中的,在什么条件下才执行发送呢?
- TCP 拥塞机制决定发送时机
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qfIkh2Bd-1631365459655)(./img/1/1631255430651.jpg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1bluyG6k-1631365459655)(./img/1/1631255704246.jpg)]
- 最常见,最简单的解决方案就是把数据发送的间隔拉长,那么前后两条数据就不会跟那么紧,产生粘包现象了
- 封包拆包实现
- 使用封包拆包实现解决数据粘包的问题,就是按照约定好的自定义规则,先对数据进行的打包,将来使用数据的时候,再按照指定的规则把数据拆包
- 数据传输过程
- 进行数据编码,获取二进制数据包
- 按规则拆解数据,获取指定长度的数据
- Buffer 数据读写
- writeInt16BE: 将value从指定位置写入
- readInt16BE: 从指定位置开始读取数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1njwXn3a-1631365459656)(./img/1/1631255959110.jpg)]
class MyTransformCode{
constructor() {
this.packageHeaderLen = 4
this.serialNum = 0
this.serialLen = 2
}
encode(data, serialNum) {
const body = Buffer.from(data)
const headerBuf = Buffer.alloc(this.packageHeaderLen)
headerBuf.writeInt16BE(serialNum || this.serialNum)
headerBuf.writeInt16BE(body.length, this.serialLen)
if (serialNum == undefined) {
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 = MyTransformCode
const MyTransform = require('./myTransform.js')
let ts = new MyTransform()
let str1 = '拉勾教育'
let encodeBuf = ts.encode(str1, 1)
let len = ts.getPackageLen(encodeBuf)
console.log(len)
- 封包解决粘包
const net = require('net')
const MyTransform = require('./myTransform.js')
let overageBuffer = null
let ts = new MyTransform()
const client = net.createConnection({
host: 'localhost',
port: 1234
})
client.write(ts.encode('拉勾教育1'))
client.write(ts.encode('拉勾教育2'))
client.write(ts.encode('拉勾教育3'))
client.write(ts.encode('拉勾教育4'))
client.write(ts.encode('拉勾教育5'))
client.on('data', (chunk) => {
if (overageBuffer) {
chunk = Buffer.concat([overageBuffer, chunk])
}
let packageLen = 0
while(packageLen = ts.getPackageLen(chunk)) {
const packageCon = chunk.slice(0, packageLen)
chunk = chunk.slice(packageLen)
const ret = ts.decode(packageCon)
console.log(ret)
}
overageBuffer = chunk
})
const net = require('net')
const MyTransform = require('./myTransform.js')
const server = net.createServer()
let overageBuffer = null
let ts = new MyTransform()
server.listen('1234', 'localhost')
server.on('listening', () => {
console.log('服务端运行在 localhost:1234')
})
server.on('connection', (socket) => {
socket.on('data', (chunk) => {
if (overageBuffer) {
chunk = Buffer.concat([overageBuffer, chunk])
}
let packageLen = 0
while(packageLen = ts.getPackageLen(chunk)) {
const packageCon = chunk.slice(0, packageLen)
chunk = chunk.slice(packageLen)
const ret = ts.decode(packageCon)
console.log(ret)
socket.write(ts.encode(ret.body, ret.serialNum))
}
overageBuffer = chunk
})
})
- http 协议
const net = require('net')
let server = net.createServer()
server.listen(1234, () => {
console.log('服务端启动了....')
})
server.on('connection', (socket) => {
socket.on('data', (data) => {
console.log(data.toString())
})
socket.end('test http request')
})
const http = require('http')
let server = http.createServer((req, res) => {
console.log('1111')
})
server.listen(1234, () => {
console.log('server is running......')
})
- 获取 http 请求信息
const http = require('http')
const url = require('url')
const server = http.createServer((req, res) => {
let {pathname, query} = url.parse(req.url, true)
console.log(pathname, '----', query)
console.log(req.method)
let arr = []
req.on('data', (data) => {
arr.push(data)
})
req.on('end', () => {
console.log(Buffer.concat(arr).toString())
})
})
server.listen(1234, () => {
console.log('server is start......')
})
-
设置 http 响应 -
代理客户端 -
代理客户端解决跨域
const http = require('http')
let options = {
host: 'localhost',
port: 1234,
path: '/',
method: 'POST'
}
let server = http.createServer((request, response) => {
let req = http.request(options, (res) => {
let arr = []
res.on('data', (data) => {
arr.push(data)
})
res.on('end', () => {
let ret = Buffer.concat(arr).toString()
response.setHeader('Content-type', 'text/html;charset=utf-8')
response.end(ret)
})
})
req.end('拉勾教育')
})
server.listen(2345, () => {
console.log('本地的服务端启动了')
})
- Http 静态服务
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)
pathname = decodeURIComponent(pathname)
let absPath = path.join(__dirname, pathname)
fs.stat(absPath, (err, statObj) => {
if(err) {
res.statusCode = 404
res.end('Not Found')
return
}
if (statObj.isFile()) {
fs.readFile(absPath, (err, data) => {
res.setHeader('Content-type', mime.getType(absPath) + ';charset=utf-8')
res.end(data)
})
} else {
fs.readFile(path.join(absPath, 'index.html'), (err, data) => {
res.setHeader('Content-type', mime.getType(absPath) + ';charset=utf-8')
res.end(data)
})
}
})
})
server.listen(1234, () => {
console.log('server is start.....')
})
- lgserve 命令行配置
- bin > wwww.js
- main.js
- npm init -y
- package.json > “bin”: { “lgserver”: “bin/www.js” }
- 在 wwww.js 首行编写: #! /usr/bin/env node
- 在 bin目录下执行: npm link
- 直接执行全局命令 lgserver
- npm install commander 可以设置命令帮助说明文档
- lgserver --help
#! /usr/bin/env node
const {program} = require('commander')
let options = {
'-p --port <dir>': {
'description': 'init server port',
'example': 'lgserve -p 3306'
},
'-d --directory <dir>': {
'description': 'init server directory',
'example': 'lgserve -d c:'
}
}
function formatConfig (configs, cb) {
Object.entries(configs).forEach(([key, val]) => {
cb(key, val)
})
}
formatConfig(options, (cmd, val) => {
program.option(cmd, val.description)
})
program.on('--help', () => {
console.log('Examples: ')
formatConfig(options, (cmd, val) => {
console.log(val.example)
})
})
program.name('lgserve')
let version = require('../package.json').version
program.version(version)
let cmdConfig = program.parse(process.argv)
let Server = require('../main.js')
new Server(cmdConfig).start()
-
lgserve 启动web服务 -
lgserve 处理文件资源
- npm install mime 通过文件后缀获取返回给浏览器的content-type
- lgserve 处理目录资源
- npm install ejs 拿数据在模版中进行渲染
- lgserve 模板数据渲染
const http = require('http')
const url = require('url')
const path = require('path')
const fs = require('fs').promises
const {createReadStream} = require('fs')
const mime = require('mime')
const ejs = require('ejs')
const {promisify} = require('util')
function mergeConfig (config) {
return{
port: 1234,
directory: process.cwd(),
...config
}
}
class Server{
constructor(config) {
this.config = mergeConfig(config)
}
start() {
let server = http.createServer(this.serveHandle.bind(this))
server.listen(this.config.port, () => {
console.log('服务端已经启动了.......')
})
}
async serveHandle(req, res) {
let {pathname} = url.parse(req.url)
pathname = decodeURIComponent(pathname)
let abspath = path.join(this.config.directory, pathname)
try {
let statObj = await fs.stat(abspath)
if (statObj.isFile()) {
this.fileHandle(req, res, abspath)
} else {
let dirs = await fs.readdir(abspath)
dirs = dirs.map((item) => {
return{
path: path.join(pathname, item),
dirs: item
}
})
let renderFile = promisify(ejs.renderFile)
let parentpath = path.dirname(pathname)
let ret = await renderFile(path.resolve(__dirname, 'template.html'), {
arr: dirs,
parent: pathname == '/' ? false : true,
parentpath: parentpath,
title: path.basename(abspath)
})
res.end(ret)
}
} catch (err) {
this.errorHandle(req, res, err)
}
}
errorHandle(req, res, err) {
console.log(err)
res.statusCode = 404
res.setHeader('Content-type', 'text/html;charset=utf-8')
res.end('Not Found')
}
fileHandle(req, res, abspath) {
res.statusCode = 200
res.setHeader('Content-type', mime.getType(abspath) + ';charset=utf-8')
createReadStream(abspath).pipe(res)
}
}
module.exports = Server
|