node的意义
- 浏览器通过事件驱动服务界面上的交互,node通过事件驱动服务IO。
- 单线程三大弱点
- 无法利用多核cpu
- 大量计算会阻塞线程
- 错误会引起退出整个应用
像浏览器的js与ui共用一个线程一样,js长时间执行会导致UI渲染和响应被中断。web worker能够创建工作线程进行计算,以解决js大计算阻塞ui渲染问题。工作线程为了不阻塞主线程,通过消息传递的方式传递运行结果,工作线程也无法操作UI。
node可以通过child_process将计算分发到各个子进程,我们可以通过子进程充分利用硬件资源。
IO密集型
Node面向网络且擅长并行IO,能够有效组织起更多硬件资源。IO密集的又是主要在于Node利用事件循环的处理能力。是否不善于cpu密集
由于js单线程的原因,如果有长时间运行的计算,将会导致cpu时间片不能释放,使得后续io无法发起。但是适当调整和分解大型运算任务为多个小任务,使得运算能够适时释放,不阻塞io调用的发起,这样既能享受并行异步io的好处,又能充分利用cpu。
Node模块机制
- commonjs
commonjs主要分为模块引用、模块定义和模块标识
1、 模块引用
var math = =require('math')
通过require进行模块引用,math则作为模块标识
2、模块定义
上下文提供了exports对象用于导出当前模块的方法或变量,在模块中,还存在一个module对象,它代表模块自身,在Node中,一个文件就代表一个模块。
3、模块标识
- Node模块的实现
在Node引入模块,需要经历以下3个步骤
- 路径分析
- 文件定位
- 编译执行
在Node中有两种模块,一种是核心模块,另一种是文件木块。
核心模块在Node源码的编译过程中,编译进了二进制执行文件。在Node启动时,核心模块被直接加载进内存,所以文件定位和编译执行可以省略掉,并在路径分析中优先判断,所以它的加载速度是最快的。
文件模块则是运行时动态加载,优先级:核心模块 > 文件模块
一. 模块标识符分析
核心模块
相对路径
绝对路径
非路径形式的文件模块
缓存 > 核心模块 > 路径形式的文件模块 > 自定义模块
自定义模块首先查找当前目录的node_modules,再查找父级的node_modules,一直查找下去。
在编译过程中,Node对js的内容进行了包装,并注入对象和方法
((exports, module, require, __filename, __dirname) => {
})()
bin。一些包作者希望可以作为命令行工具使用。配置好bin字段后,通过npm install packname -g命令可以将脚本添加到执行路径中,之后可以通过命令中直接执行。main。模块引入方法require在引入包时,会优先检查这个字段,并将其作为包中其余模块的入口。如果不存在这个字段,则会查找包目录下的index文件作为默认入口。
-g这个全局模式并不准确,-g是将一个包安装为全局可用的可执行指令。它根据包描述文件中的bin字段配置,将实际脚本链接到与Node可执行文件相同的路径下。
异步I/O
从用户体验和资源分配的角度看
浏览器中的js是单线程上执行,而且它还与ui渲染共用一个线程。意味着js执行的时候,ui渲染和响应是停滞的。而采用异步请求,在下载资源期间,js和ui的执行都不会处于等待状态,可以继续响应用户的交互行为。
前端通过异步消除UI阻塞,通过异步,我们还可以享受到并发的优势。从资源分配的角度看
计算机分为IO设备和计算设备
通常情况下IO与cpu计算是可以并行的,单线程中,任意一个略慢的任务都会导致后续执行代码被阻塞。异步IO提出的是期望IO的调用不再阻塞后续运算。
内部完成IO任务另有线程池。Node事件循环
进程启动时,Node会创建一个类似while的循环,每执行一次循环体的过程称为一次tick。每个tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关回调函数。执行。然后进入下个循环,如果不再有事件处理,就退出进程。观察者
每个Tick的过程中,如何判断有事件需要处理呢?
每个事件循环中有一个或者多个观察者,而判断是否有事件处理的过程就是向这些观察者者询问是否有要处理的事件。
各种各样的事件都有对应的观察者,浏览器的事件主要是用户的交互和文件加载。事件来自网络请求、文件IO等,这些事件对应的观察者有文件IO观察者、网络IO观察者等。观察者将事件进行了分类。
事件循环是一个典型的生产者/消费者模型。异步IO,网络请求则是事件的生产者,源源不断的为Node提供各种不同的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。从观察者取出事件的时候是有一个排序的。请求对象
从js层传入的参数和当前方法都被封装在这个请求对象中,然后推入线程池中等待处理。至此js调用立即返回,由js层面发起的异步调用的第一阶段就此结束。执行回调
线程池中的IO
操作调用完毕以后,将线程归还线程池。每次tick的执行中,会检查线程池中是否有执行完的请求,如果存在,则会加入到IO观察者队列中,然后将其当做事件处理。
非IO的异步API
定时器,setImmediate,process.nextTick()
优先级 process.nextTick > io事件 > setImmediate
node无法处理异步回调中抛出的错误
- 事件发布/订阅
let count = 0;
let result = {}
let done = function(key, value){
result[key] = value
count++
if (count == 3) { render(result) }
}
fs.readFile(template_path, 'utf-8', (err, template) => {
done('template', template)
})
db.query(sql, (err, data) => { done('data', data) })
let after = function(times, callback) {
let count = 0, results = {}
return (key, value) => {
result[key] = value
count++
if (count == times) { callback(results) }
}
}
const done = after(3, render)
Promise/Deffred
let Promise = function() {
EventEmitter.call(this)
}
util.inherits(Promise, EventEmitter)
Promise.prototype.then = function(full, error, procrss) {
if (typeof full === 'function') {
this.once('success', full)
}
if (typeof error === 'function') {
this.once('error', error)
}
if (typeof procrss === 'function') {
this.once('procrss', procrss)
}
return this
}
then方法所做的事情是将回调函数存放起来。为了完成整个流程,还需要触发执行回调的地方,实现这些功能的对象叫Deferred,既延迟对象。
let Defrred = function() {
this.state = 'unfulfilled'
this.promise = new Promise()
}
Defrred.prototype.resolve = function(obj) {
this.state = 'fulfilled'
this.promise.emit('success', obj)
}
Defrred.prototype.reject = function(err) {
this.state = 'failed'
this.promise.emit('error', err)
}
let promisify = function (res) {
let deferred = new Deferred()
let result = ''
res.on('data', (chunk) => {
result+=chunk
deferred.progress(chunk)
})
res.on('end', () => {
deferred.resolve(result)
})
res.on('error', (err) => {
deferred.error(err)
})
return deferred.promise
}
Deferred主要用于内部,用于维护异步模型的状态;Promise主要用于外部,通过then方法添加自定义逻辑,保存方法
Deferred.prototype.all = function(promises) {
let count = promises.length
let that = this
let result = []
promises.forEach(function(promise, i){
promise.then(function( data) {
count--
result[i] = data
})
})
}
let Deferred = function () {
this.promise = new Promise()
}
Deferred.prototype.resolve = function (obj) {
let promise = this.promise
let handler
while (handler = promise.queue.shift()) {
let ret = hander.fulfilled(obj)
if (ret && res.isPromise) {
ret.queue = promise.queue
this.promise = ret
return
}
}
}
Deferred.prorotype.reject = function(err) {
let promise = this.promise
let handler
while (handler = promise.queue.shift()) {
if (handler && handler.error) {
}
}
}
Deferred.prototype.callback = function () {
let that = this
return function (err, file) {
if (err) return that.reject(err)
else that.resolve(file)
}
}
let Promise = function () {
this.queue = []
this.isPromise = true
}
Promise.prototype.then = function (fulfillHandler, errorHandler, progreeHandler) {
let hander = {}
if (typeof fulfillHandler == 'function') {
hander.fulfilled = fulfillHandler
}
if (typeof errorHandler == 'function') {
hander.error = errorHandler
}
this.queue.push(handler)
return this
}
let readFile1 = function (file, encoding) {
let deferred = new Deferred()
fs.readFile(file, encoding, deferred.callback())
return deferred.promise
}
let readFile2 = function (file, encoding) {
let deferred = new Deferred()
fs.readFile(file, encoding, deferred.callback())
return deferred.promise
}
let smooth = function (method) {
return (...args) => {
let deferred = new Deferred()
let arrs = args.slice(1).push(deferred.callback)
method(arrs)
return deferred.promise
}
}
要让Promise支持链式执行,主要通过以下两个步骤
- 将所有的回调都存储到队列中
- promise完成时,逐个执行回调,一旦检测到返回了新的promise对象,停止执行,然后将当前的promise改为新的promise。
- 流程控制库
- 尾触发与next
有一类方法需要手工调用才能持续执行后续调用,在通过use方法注册好一系列中间件后,监听端口请求。
let app= = connect()
app.use(connect.staticCache())
app.use(connect.static(__dirname+'/public'))
function (req, res, next) { 中间件 }
每个中间件传递请求对象,响应对象和尾触发函数,通过队列形成一个处理流
connect核心代码如下
function createServer() {
function app(req, res) { app.hanle(req, res) }
utils.merge(app, proto)
utils.merge(app, EvevntEmitter.prototype)
app.route = '/'
app.stack = []
for (let i=0;i
TLS/SSL
- 密钥
TLS/SSL是一个公钥私钥结构,它是一个非对称结构,每个服务器和客户端都有自己的公私钥。公钥用来加密要传输的数据,私钥用来解密收到的数据。所以在安全传输之前服务器和客户端要交换公钥。客户端发送的数据要通过服务器端的公钥加密。
中间人攻击
客户端和服务器在交换公钥的过程中,中间人对客户端扮演服务器角色,对服务器扮演客户端角色,因此客户端和服务器几乎感受不到中间人存在。所以,数据传输过程中我们还需要对公钥进行认证,以确认公钥是出自目标服务器。
为了解决这个问题TLS/SSL引入了数字证书进行认证。与直接使用公钥不同,数字证书中包含了服务器的名称和主机名。
在建立连接之前,会通过证书中的签名确认收到的公钥是来自目标服务器。数字证书
服务器端需要向ca机构申请签名证书。申请之前要创建csr文件。通过csr文件我们能得到ca颁发的签名。最终会被颁发一个带有ca签名的证书。
客户端在发起安全连接前会去获取服务器端的证书,并通过ca的证书验证服务器端证书的真伪。如果是知名ca机构,证书一般预装在浏览器中。session
通过Cookie,浏览器和服务器可以实现状态的记录。session的数据只保留在服务器端,客户端无法修改,这样数据的安全性得到了保障。
如何将每个客户和服务器中的数据一一对应起来
- 基于Cookie来实现用户和数据的映射
将口令放在Cookie,如果口令被篡改,那么就会丢失映射关系,就无法修改服务器存在的数据。
一旦服务器启用session,它将约定一个键值作为session的口令,这个值可以随意约定,并且唯一。
let session = {}
let key = 'session_id'
let EXPIRES = 20 * 60 * 1000
let generate = function () {
let session = {}
session.id = (new Date()).getTime + Math.random()
session.cookie = {
expire:
}
sessions[session.id] = session
return session
}
- session与内存
我们都将Session数据直接存在变量sessions中,它位于内存中。将session存储在redis数据库里。
session 与安全
对session口令使用私钥加密,假如攻击者获取了真实的口令和签名,他就能实现身份的伪装。一种方案是将客户端某些独有信息与口令作为原值,然后签名,这些独有信息包括用户ip和用户代理。
xss漏洞会让别的脚本执行,形成的原因多数是用户的输入没有被转义,而被直接执行。缓存
通常来说缓存只存在于GET请求中
条件请求询问服务器是否有更新版本,本地文件的最后修改时间。如果没有新版本,只需响应一个304.CSRF
用户通过浏览器访问服务端的sessionid是无法被第三方知道的,但csrf不需要知道sessionId就能让用户中招。中间件
let middleware = function (req, res, next) {
...
next()
}
app.use('/user/:username', querystring)
app.use = function (...args) {
let handle = {
path: pathRegxp(args[0]),
stack: args.slice(1)
}
routes.all.push(handle)
}
let handle = function (req, res ,stack) {
return () => {
let middle = stack.shift()
}
}
- 视图渲染
最终视图由模板和数据共同生成
模板是带有特殊标签的html片段,通过与数据的渲染,将数据填充到特殊标签中。
模板文件+数据通过模板引擎生成最终的html代码。
- 模板引擎
我们通过render方法实现模板引擎,他会将
Hello <%= username%> => Hello + obj.username
- 语法分解
提取出普通字符串和表达式,这个过程通常用正则表达式匹配出来 /<%=(.+?)%>/
处理表达式
生成待执行的语句
与数据一起执行,生成最终字符串
const render = (str, data) => {
let tpl = str.replace(/<%=([\s\S]+?)%>/g, (match, code) => {
return ` obj.${code} `
})
tpl = `let tpl=${tpl} \n;return tpl`
let compiled = new Function('obj', tpl)
return compiled(data)
}
为了能够与数据一起执行生成字符串,我们需要将原始的模板字符串转换成一个函数对象。
function (obj) {
let tpl = `Hello ${obj.username} .`
return tpl
}
这个过程称为模板编译,生成的中间函数只与模板字符串相关,具体时间无关。如果每次都生成这个中间函数,就会浪费CPU。为了提升模板渲染的性能速度,我们通常会进行模板预编译。
const render = function (str) {
return compiled(data)
}
代码 - stage - pre - release - pro