You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
var toString = Object.prototype.toString;
var isType = function (type) {
return function (obj) {
return toString.call(obj) == '[object' + type + ']'
}
}
var isFunction = isType('Function')
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
var data = ''
reader.on('data', function (chunk) {
data += chunk
})
reader.on('end', function () {
console.log(data)
})
buffer对象的长度为11,可读流要读取很多次才能完成完整的读取
宽字节字符串可能存在被截断的情况。
解决乱码
设置编码
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
render.setEncoding('utf8')
const net = require('net');
let server = net.createServer();
server.on('connection', function (socket) {
console.log('connection')
})
server.listen(8000)
var bytes = 1024;
function (req, res) {
var received = 0;
var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;
if (len && len > bytes) {
res.writeHead(413);
res.end();
return;
}
req.on('data', function (chunk) {
received += chunk.length;
if (received > bytes) {
req.destroy();
}
})
handle(req, res);
}
csrf
var generateRandom = function (len) {
return crypto.randomBytes(Math.ceil(len*3/4)).toString('base64').slice(0, len);
}
var token = req.session._csrf || (req.session._crsf = generateRandom(24));
// 做页面渲染的时候服务器端渲染这个_csrf
function (req, res) {
var token = req.session._csrf || (req.session._csrf = generateRandom(24));
var _csrf = req.body._csrf;
if (token !== _csrf) {
res.writeHead(413);
res.end("禁止访问");
} else {
handle(req, res);
}
}
var cp = require('child_process');
var n = cp.fork('./child.js');
n.on('message', function (data) {
console.log('parent data: ' + data.name);
})
n.send({name: 'parent'})
问题(完成后解决)
简介
目标:写一个基于事件驱动 ,非阻塞i/o 的web服务器,以达到更高的性能。构建快速,可伸缩的网络应用平台
js开发性能低,事件驱动应用
node强制不共享任何资源的 单线程 ,单进程系统,包含十分适宜网络的库
应用:
特点:
两个readFile的操作最终时间为最慢的那一个
事件编程方式:轻量级,轻耦合,只关注事务点等优势
单线程
特点:
弱点:
解决:
应用场景:
模块机制
前言:
web 1.0 : JavaScript用于表单校验和网页特效,只有对bom,dom的支持
web 2.0 : 提升了网页的用户体验,bs应用展现出了比cs(需要装客户端)应用优越的地方。h5崭露头角
此过程经历了工具-组件-框架-应用的变迁
js的规范缺陷:
commonjs模块规范
require()
同步,为后端js指定的规范,并不完全适合前端的应用场景
模块实现
模块分为两类:
已被编译进了二进制执行文件,node启动时就被加载进内存,所以1.2步骤可以省略。且加载速度最快
动态加载,速度比核心模块慢
优先从缓存加载
在node中引入模块要经过下面三个步骤
路径分析
..
或者.
相对路劲模块/
开头的绝对路径模块connect
模块如果想加载与核心模块标识符相同的模块,必须选择 不同的标识符 或者 换用路径 的方法
以
.
,..
,/
开头的标识符,会将路径转换成真实路径自定义模块是最费时的
module.paths
模仿搜索路径规则如下:
文件定位
文件扩展名
.js
.node
.json
顺序补齐fs
模块同步阻塞式的判断文件是否存在,如果是.node
和.json
文件,带上扩展名再配合缓存可以加快速度目录和包的处理
package.json
,取出main
属性指定的文件名定位。package.json
, 会将index
作为默认文件名编译执行
node会新建一个模块对象,然后根据路径载入并编译,对应不同扩展名,载入方法不同:
.js
通过fs
同步读取.node
通过dlopen()
加载.json
通过fs读取,再JSON.parse
.js
每一个编译成功的模块都会被绑定在
Module._cache
上编译过程对文件内容进行头尾包装
另外,这样会出错
原因在于,exports对象是通过形参的方式传入的,直接赋值会改变形参的作用,但并不能改变作用域外的值。
js核心模块的编译过程
c/c++核心模块编译过程
c++模块主内完成核心,js主外实现封装
性能优于脚本语言
被编译成二进制文件,一旦node开始执行,就直接加载进缓存
依赖关系:文件模块
<--
核心模 块<--
内建模块包与npm
cnpm搭建私有的npm服务
包结构
package.json
包描述文件bin
存放可执行二进制文件的目录lib
存放js的代码目录doc
存放文档test
存放单元测试用例常用功能
查看帮助
npm help
安装依赖包
npm install --save/--save-dev express
-g是讲一个包安装到全局可用的可执行命令。它根据包描述文件中的bin字段配置,将实际脚本连接到与node可执行文件相同的路径下
如果node可执行文件的位置是
/usr/local/bin/node
,那么模块目录就是/usr/local/lib/node_modules
。最后通过软链接方式将bin字段配置的可执行文件链接到node的可执行目录下本地安装
换源:
npm install underscore --registry=http:registry.url
npm config set registry http:registry.url
npm钩子
发布包
npm adduser
npm publish<folder>
npm owner ls <package_name>
npm owner add <user> <package_name>
npm owner rm <user> <package_name>
6. 分析包
npm ls
模块考察点
前后端共用模块
node模块引入几乎都是同步的,但如果前端模块也采用同步的方式来引入,用户体验会造成问题
AMD规范
需要用define来明确定义一个模块,而在node实现中是隐式包装的。
所有的依赖,通过形参传递到依赖模块内容中
目的是作用域隔离
内容需要返回的方式实现导出
CMD规范
更接近commonjs规范
require,exports, module通过形参传递给模块。
兼容多种模块规范
异步i/o
node面向网络而设计
利用单线程,原理多线程死锁,状态同步问题
利用异步i/o,让单线程原理阻塞,更好的利用cpu
内核在进行文件i/o的操作时,通过文件描述符进行管理,文件描述符类似于应用程序与系统内核之间的凭证。
阻塞i/o造成cpu等待浪费,非阻塞却要 轮询 去确认是否完全完成数据获取
理想非阻塞异步i/o:发起非阻塞调用后,可以直接处理下一个任务,只需i/o完成后通过信号或回调将数据传递给应用程序
显示的异步i/o:通过让部分线程进行阻塞i/p或者非阻塞i/o加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将i/o得到的数据进行传递
为什么要异步i/o
用户体验
如果是同步,js执行ui渲染和响应将处于停滞状态
采用异步,在下载资源期间,js和ui的执行都不会处于等待状态
采用异步方式所花时间为max(m, n)
资源分配
缺点:
单线程同步编程模型会因为阻塞i/o导致性能差,
缺点:
代价在于创建线程和执行期线程上下文切换的开销较大
多线程常面临锁,状态同步问题
优点:
但是能有效提升cpu利用率
node的异步i/o
模型基本要素:事件循环,观察者,请求对象,i/o线程池
node自身其实是多线程的,只是i/o线程使用的cpu较少
每个事件循环中有一个或者多个观察者
异步i/o过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及i/o操作完毕后的回调处理
非i/o得异步api
创建的定时器会被插入到定时器观察者内部的一个红黑树中
每次Tick执行时,会从红黑树中迭代取出定时器对象,检查是否超过定时时间。如果超过,就形成一个时间,它的回调函数将立即执行
时间复杂度O(lg(n))
2. process.nextTick
将回调函数放入队列,在下一轮Tick时取出执行
时间复杂度 0(1)
事件驱动与高性能服务器
服务器模型:
node高性能:
异步编程
函数式编程
优势
难点
异步i/o提交请求和处理结果两个阶段中间,有事件循环的调度。异步方法则通常在提交请求后立即返回,因为一场并不一定发生在这个阶段,所以try/catch在这里无效
try/catch对于callback执行时抛出的异常无能为力
异步编程解决方案
事件发布/订阅模式
利用事件队列解决雪崩问题,once方法
多异步之间的写作方案
Promise/Deferred
Promise/A
只有三种状态:rejected,fullfiled, rejected
只能未完成到完成,或者失败,不能逆反
状态不能更改
流程控制库
内存控制
js在浏览器的应用场景,由于运行时间短,随着进程的推出,内存会释放,几乎没有内存管理的额必要
内存控制正式在海量请求和长时间运行的前提下进行探讨的。
在服务器端,资源寸土寸金
对于性能敏感的服务器端程序,内存管理的好坏,垃圾回收状况的优良,影响很大
js引擎V8(虚拟机)
内存限制
在node中通过js使用内存时,只能使用部分,无法直接操作大内存对象
64位系统下约为1.4GB,32位系统下约为0.7GB
node中使用js对象,都是通过V8来进行分配和管理的
对象分配
js对象通过堆来分配
当在代码中生命变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆得大小超过V8的限制为止
V8为何限制堆得大小:表层原因是起初为浏览器而设计,限制值已经绰绰有余。深层原因是V8的垃圾回收机制的限制,做一次非增量式的垃圾回收时间花销大
垃圾回收机制
V8垃圾回收策略主要基 分代式垃圾回收机制
垃圾回收算法:
将内存分为 新生代 和 老生代
新生代中的对象为存活时间较短的对象,老生代的对象为存活时间较长或常驻内存的对象
Scavenge算法
具体实现主要采用Cheney算法
采用复制的方式实现垃圾回收算法。
将堆内存一分为二。每一份空间成为semispace。处于闲置状态的称为To空间,处于使用状态的称为From空间。
当开始进行垃圾回收时,会检查From空间的存活对象,这些存活对象会被复制到To空间。非存活对象占用空间会被释放
缺点:用空间换时间
当一个对象经过多次复制依然存活时,被认为是生命周期较长的对象。被移到老生代中。称为晋升
对象晋升的条件:
通过检查它的内存地址来判断。如果经历过了,从From复制到老生代
缺点:1. 存活对象较多时,复制存活对象的效率低。 2. 浪费一般空间
Mark-Sweep(标记清除)
Mark-Compat(标记整理)
对象在标记为死亡后,整理过程中,将活着的对象往一端移动。完成后,直接清理掉边界外的内存
在空间不足以对从新生代晋升过来的对象进行分配时才使用
Incremental Marking
延迟清理和增量清理
并行标记和并行清理
小结:
查看垃圾回收日志
node --trace_gc -e "..."
可以了解垃圾回收的运行状况,找出哪些阶段比较费时
node --prof xx.js
会在该目录下生成v8.log文件,得到性能分析数据
node --prof-process isolate-0x103001200-v8.log
由于日志文件不具备可读性,故这样可以统计日志信息
高效使用内存
作用域
内存回收过程:只被局部变量引用的对象存活周期较短,会被分配在新生代的From空间,在作用域释放后,局部变量local失效,引用的对象会在下次垃圾回收时被释放
标识符查找:
js在执行时回去找该变量在哪里定义,在当前作用域没有查到,将会向上级的作用域里查找,直到查到为止
作用域链:
根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。
执行环境:
js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。
变量的主动释放:
全局变量,直到进程退出才释放。引用的对象常驻内存(老生代)。
可以用delete操作和重新赋值(null或者undefined)
实现外部作用域访问内部作用域中变量的方法
作用域中产生的内存占用不会得到释放。除非不再有引用,才会逐步释放
内存指标
进程的内存一部分是rss,其余部分在交换区或者文件系统中
Buffer对象并非通过V8分配,没有堆内存的大小闲置
小结:受V8的垃圾回收限制的主要是V8堆内存
内存泄漏
哪怕一字节的内存泄漏也会造成堆积,垃圾回收过程中将会耗费更多时间进行对象描述,应用响应缓慢,直到进程内存溢出,应用奔溃
原因:
缓存中存储的键越多,长期存活对象也就越多,常驻在老生代
普通对象无过期策略
解决:
缓存限制策略
超过数量,先进先出的方式进行淘汰
设计模块时,应添加清空队列的相应接口
缓存的解决方案
进程间无法共享内存
队列消费速度低于生产速度,将会形成堆积。而js相关作用域也不会得到释放,内存占用不会回落,从而出现内存泄漏
解决方案:
大内存应用
node中大多数模块都有stream应用。由于V8内存限制,采用流实现对大文件的操作
如果不需要进行字符串层面的操作,则不需要V8来处理,尝试进行纯粹的Buffer操作
Buffer
特点
内存分配
乱码
解决乱码
setEncoding的时候,可读流对象在内部设置了一个decoder对象。每次data事件都通过该decoder对象进行Buffer到字符串的解码。
decoder的对象会暂时存储,buffer读取的剩余字节
Buffer与性能
网络编程
前言
在web领域,大多数的编程语言需要专门的web服务器作为容器,如ASP、ASP.NET需要IIS作为服务器,PHP需要打在Apache或Nginx环境等,JSP需要Tomcat服务器等。但对于Node而言,只需要几行代码即可构建服务器,无需额外的容器。
构建TCP服务
TCP
创建TCP服务器端
构建UDP服务
UDP不是面向连接的。
一个套接字可以与多个UDP服务通信,它虽然提供面向事务的简单不可靠信息传输服务,在网络差的情况下存在丢包严重的问题
优点:无连接,资源消耗低,处理快速且灵活
应用:音频,视频,dns服务
HTTP
特点:
http服务端事件
http客户端
示例:
在keepalive的情况下,一个底层会话连接可以多次用于请求。为了重用tcp连接,可以用http.globalAgent客户端代理对象
默认情况下,通过ClientRequest对象对同一个服务器发起的http请求最多可以创建五个连接
如需改变,可在options中传递agent选项
websocket服务
特点:
好处:
构建过程
握手完成后,不再进行http交互,客户端的onopen将会触发执行
当客户端调用send发送数据时,服务端触发onmessage事件;当服务端调用send发送数据时,客户端触发message事件。
当send发送一条数据时,协议可能将这个数据封装为一帧或多帧数据,然后逐帧发送
网络安全
交换公钥过程中,可能遇到中间人攻击,所以应引入数字证书来认证。
创建私钥:
openssl genrsa -out ryans-key.pem 2048
生成csr
openssl req -new -sha256 -key ryans-key.pem -out ryans-csr.pem
生成自签名证书
openssl x509 -req -in ryans-csr.pem -signkey ryans-key.pem -out ryans-cert.pem
验证:
-k忽略掉证书的验证
curl -k https://localhost:2000
构建web应用
基础功能
请求方法
HTTP_Parser在解析请求报文的时候,将报文头抽取出来,设置为req.method。有诸如:
GET, POST, HEAD, PUT, DELETE, OPTIONS, TRACE, CONNECT
路径解析
路径部分存在于报文的第一行的第二部分,如:
GET /path?foo=bar HTTP/1.1
HTTP_Parser将其解析为req.url, 一般而言,完整的url地址如下
http://user:[email protected]:8080/p/a/t/h?query=string#hash
这里hash部分会被丢弃,不会存在于报文的任何地方, 下列的url对象不是报文中的,故有hash
解析出来的url对象
查询字符串
查询字符串,如果键出现多次,那么它的值会是一个数组
cookie
cookie处理:
Set-Cookie: name=vale; Path=/;Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;
path表示cookie影响路径,表示服务器目录下的子html都能访问
expires和max-age表示过期时间,一个是绝对时间,一个是相对时间
httpOnly告知浏览器不能通过document.cookie获取
secure为true表示在https才有效
domain:子域名访问父域名
**性能影响:**大多数cookie并不需要每次都用上,因为这会造成带宽的部分浪费
解决:
session
session的数据只保留在服务器端,客户端无法修改。
应用:
将口令放在cookie中,口令一旦被褚昂爱,就丢失映射关系。通常session的有效期通常短,过期就将数据删除
一旦服务器检查到用户请求cookie中没有携带session_id,它会为之生成一个值,这个值是唯一且不重复的值,并设定超时时间。如果过期就重新生成,如果没有过期,就更新超时时间
原理:检查查询字符串,如果没有值,会生成新的带值的url
隐患
由于session存储在sessions对象中,故在内存中,若数据量加大,会引起垃圾回收的频繁扫描,引起性能问题。
为了利用多核cpu而启动多个进程,用户请求的连接将可能随意分配到各个进程中,node的进程与进程之间不能直接共享内存,用户的session可能会引起错乱
解决方案
将session集中化,将可能分散在多个进程里的数据,统一转移到集中数据存储中。目前常用工具是redis,memcached。node无需在内部维护数据对象。
问题: 会引起网络访问
session与安全
缓存
设置last-modified
缺陷:
设置etag
强制缓存
用expires可能导致浏览器端与服务器端时间不同步带来的不一致性问题
清除缓存
浏览器是根据url进行缓存,那么一旦内容有所更新时,我们就让浏览器发起新的url请求,使得新内容能够被客户端更新。
数据上传
处理json格式
处理xml文件
图片上传
数据上传与安全
在解析表单,json和xml部分,我们采取的策略是先保存用户提交的所有数据,然后再解析处理,最后才传递给业务逻辑。
弊端:数据量大,占内存
解决方案:
限制大小方案代码:
路由解析
文件路径型
mvc工作模式
手工映射
自由映射,从入口程序中判断url,然后执行对应的逻辑。
匹配的时候,能够正则匹配
自然映射
/controller/action/param1/param2/param3
按约定去找controllers目录下的user文件,将其require出来,调用这个文件模块的setting方法,其余的参数直接传递到这个方法中
RESTful(representational state transfer)
需要区分请求方法
一个地址代表了一个资源,对这个资源的操作,主要体现在http请求方法上,不是体现在url上
设计:
POST,GET,PUT,DELETE
中间件
含义:指底层封装细节,为上层提供更方便服务的意义,为我们封装所有http请求细节处理的中间件
中间件性能
缓存需要重复计算的结果,避免不必要的计算。
页面渲染
内容响应
响应头中的content-*字段十分重要。
示例
客户端在接收到后,通过gzip来解码报文体重的内容,用长度校验报文体内容是否正确,然后在以字符集utf-8将解码后的脚本插入到文档节点中
application/json, application/xml, application/pdf
背景:无论响应的内容是什么MIME,只需要弹出并下载它
Content-Disposition
判断是应该将报文数据当做及时浏览的内容,还是可下载的附件。
还能指定保存时使用的文件名
Content-Disposition:attachment;filename="filename.txt"
响应附件api
视图渲染
模板要素:
集成文件系统
这样做每次都需要读取模板文件,因此可设置cache={}
模板性能
进程
一个进程只能利用一个核,如何充分利用多核cpu服务器
单线程上抛出的异常没有被捕获,如何保证进程的健壮性和稳定性
石器时代:同步
一次只为一个请求服务
青铜时代:复制进程
通过进程的赋值同时服务更多的请求和用户。进程赋值会导致内存浪费
白银时代:多线程
一个线程服务一个请求,线程相对于进程的开销要小,线程之间可以共享数据,内存浪费问题得到解决
但是线程上线文切换会产生时间消耗
黄金时代:事件驱动
解决高并发问题
单线程避免不必要的内存开销和上下文切换
php为每个请求都简历独立的上下文
多线程架构
master.js实现进程的复制
worker.js
ps aux | grep worker.js
查看进程的数量通过fork复制的进程都是一个独立的进程,启动多个进程只是为了充分将cpu资源利用起来,而不是为了解决并发问题
创建子进程
cp.spawn('node', ['worker.js']);
sp.exec('node worker.js', () => {})
启动一个子进程来执行可执行文件
创建node子进程只需要指定要执行的javascript文件模块
进程间通信
主线程与工作线程之间通过onmessage和postMessage进行通信,子进程对象则由send方法实现主进程向子进程发送数据
parent.js
child.js
结果
ipc进程间通信(inter-process communication)
node中实现ipc通道的是管道技术,具体由libuv提供
父进程在实际创建子进程之前,会创建ipc通道并监听它,然后才真正创建子进程,并通过环境变量告诉子进程这个ipc通道的文件描述符。
双向通信,在系统内核中完成通信,不用经过实际的网络层
句柄传送
多个进程监听通过端口会抛出EADDRINUSE异常,这是端口被占用的情况。可以通过代理,在代理进程上做适当的负载均衡,使得每个子进程可以较为均衡地执行任务。但是代理进程连接到工作进程的过程需要用掉两个文件描述符
句柄是一种可以用来标识资源的应用,他的内部包含了只想对象的文件描述符。比如句柄可以用来表示一个服务器端socket对象,一个客户端socket对象,一个udp套接字,一个管道等。
发送句柄使得主进程接收到socket请求后,将这个socket直接发给工作进程,而不是重新与工作进程之间建立新的socket连接来转发数据。解决文件描述符的浪费问题
parent.js
child.js
让请求都由子进程处理
parent
child
多个子进程可以同时监听相同端口,再没有EADDRINUSE异常发生
总结:
独立启动的进程中,tcp服务器端socket套接字的文件描述符并不相同,导致监听到相同的端口时会抛出异常
多个应用监听相同端口时,文件描述符同一时间只能被某一个进程所用,所以是抢占式的
进程事件
自动重启
进程退出时,让所有工作进程退出。子进程退出时重新create
在极端情况下,所有工作进程都停止接受新的连接,全出在等待退出的状态。但在等进程完全退出才重启的过程中,所有新来的请求可能存在没有工作进程为新用户服务的情景,这会丢掉大部分请求
因此可在子进程中监听uncaughtException,然后发送自杀信号
负载均衡
node默认提供的机制是采用操作系统的抢占式策略。
新的策略是轮叫调度。工作方式是由主进程接受连接,将其一次分发给工作进程。
状态共享
在多个进程之间共享数据
实现同步:子进程向第三方进行定时轮训
主动通知子进程,轮训。
cluster模块
要创建单机node集群,由于有许多细节需要处理,于是引入cluster,解决多核cpu的利用率问题
原理:cluster模块就是child_process和net模块的组合应用。在fork子进程时,将socket的文件描述符发送给工作进程。通过so_reuseaddr端口重用,从而实现多个子进程共享端口。
产品化
项目工程化
项目的组织能力
部署流程
代码流程--》stage普通测试环境--》pre-release预发布环境--》product实际生产环境
部署操作
node file.js以启动应用,会站住一个命令行窗口,窗口退出进程也退出
nohup node app.js &
不挂断进程的方式bash脚本, 解决进程id不容易查找的问题。重启,中断,启动
性能
动静分离:
让node只处理动态请求,将静态文件引导到专业的静态文件服务器。用nginx或者专业的cdn来处理
cdn缓存,将文件放在离用户尽可能近的服务器
对静态请求使用不同的域名或者多个域名还能消除掉不必要的cookie传输和浏览器对下载线程数的限制
启用缓存
提升服务速度,避免不必要的计算
多进程架构
读写分离
对数据库进行主从设计,这样读取数据操作不再受到写入的影响,降低了性能的影响。
日志
写到磁盘上
数据库写入要经历锁表,日志等操作,如果大量访问会排队,进而内存泄露。
监控报警
监控
通过监控异常日志文件的变动,将新增的异常按异常类型和数量反应出来。
监控访问日志,体现业务qps值,pv/uv,预知访问高峰
在nginx类的反向代理上监控
通过应用自行产生的访问日志来监控
检查操作系统中运行的应用进程数,对于采用多进程架构的web应用,就需要检查工作进程的数量,如果低于预估值,就应当发出报警
监控磁盘的用量,设置警戒值
健康的内存是有升有降的
cpu分为内核态,用户态,iowait等。
用户态占用高: 服务器上应用大量cpu开销
内核态占用高:服务器花费大量时间进程调度或者系统调用。
描述操作系统当前的繁忙程度
指标过高,在node中可能体现在用子进程模块反复启动新的进程
反应磁盘读写情况
流入流量和流出流量
应用状态监控
dns监控
报警的实现
The text was updated successfully, but these errors were encountered: