Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

深入浅出nodejs读书笔记 #8

Open
2 of 4 tasks
UNDERCOVERj opened this issue Mar 24, 2018 · 0 comments
Open
2 of 4 tasks

深入浅出nodejs读书笔记 #8

UNDERCOVERj opened this issue Mar 24, 2018 · 0 comments

Comments

@UNDERCOVERj
Copy link
Owner

UNDERCOVERj commented Mar 24, 2018

问题(完成后解决)

  • 导出function为什么用module.exports不用exports
  • util.inherits
  • promise/deferred怎样实现的
  • requirejs的应用

简介

目标:写一个基于事件驱动非阻塞i/o 的web服务器,以达到更高的性能。构建快速,可伸缩的网络应用平台

js开发性能低,事件驱动应用

node强制不共享任何资源的 单线程 ,单进程系统,包含十分适宜网络的库

应用:

  1. 访问本地文件
  2. 搭建websocket服务端
  3. 连接数据库
  4. web workers多进程(不处理ui)

特点:

  1. 依旧基于作用域和原型链
  2. 异步i/o

两个readFile的操作最终时间为最慢的那一个

  1. 事件和回调函数

事件编程方式:轻量级,轻耦合,只关注事务点等优势

  1. 单线程

    特点:
    1. js与其他线程是无法共享任何状态
    2. 不用像多线程一样处处在意状态的同步
    3. 没有死锁
    4. 没有线程上下文交换带来的性能上的开销
    弱点:
    1. 无法利用多核cpu
    2. 错误会引起整个应用退出,应用的健壮性值得考研
    3. 大量计算占用cpu导致无法调用异步i/o
    4. js与ui共用一个线程,长时间执行会导致ui的渲染和响应被中断
    解决:
    1. web workers能够创建工作线程来进行计算,以解决js大计算阻塞ui渲染的问题
    2. child_process子进程,将计算分发到各个子进程,可以将大量计算分解掉

应用场景:

  1. i/o密集型,利用事件循环的处理能力
  2. cpu非密集型,i/o阻塞造成的性能浪费远比cpu的影响小
  3. 分布式应用,利用高效并行i/o,可以高效使用数据库

模块机制

前言:

  • web 1.0 : JavaScript用于表单校验和网页特效,只有对bom,dom的支持

  • web 2.0 : 提升了网页的用户体验,bs应用展现出了比cs(需要装客户端)应用优越的地方。h5崭露头角

此过程经历了工具-组件-框架-应用的变迁

js的规范缺陷:

  1. 没有模块系统
  2. 标准库较少
  3. 没有标准接口
  4. 缺乏包管理系统

commonjs模块规范

  1. 模块引入 require()
  2. 提供exports对象用于导出当前模块的方法或者变量
  3. 模块标识,就是require的参数,必须驼峰命名,相对路径或者绝对路径,可以没有后缀

同步,为后端js指定的规范,并不完全适合前端的应用场景

模块实现

模块分为两类:

  1. node提供的 核心模块

已被编译进了二进制执行文件,node启动时就被加载进内存,所以1.2步骤可以省略。且加载速度最快

  1. 用户编写的 文件模块

动态加载,速度比核心模块慢

优先从缓存加载

  • node缓存的是 编译执行后的对象
  • 不论核心模块还是用户模块,对应相同模块的二次加载都是缓存优先

在node中引入模块要经过下面三个步骤

  1. 路径分析

    1. 标识符分析:
      1. 核心模块
      2. .. 或者 . 相对路劲模块
      3. / 开头的绝对路径模块
      4. 非路径形式的模块,如自定义的 connect 模块
    • 如果想加载与核心模块标识符相同的模块,必须选择 不同的标识符 或者 换用路径 的方法

    • .../ 开头的标识符,会将路径转换成真实路径

    • 自定义模块是最费时的

      module.paths模仿搜索路径

      规则如下:

      1. 当前文件目录下的node_modules
      2. 父目录下的node_modules
      3. 沿路径向上逐级递归直到根目录下的node_modules
  2. 文件定位

    1. 文件扩展名

      • .js .node .json顺序补齐

      • fs模块同步阻塞式的判断文件是否存在,如果是.node.json 文件,带上扩展名再配合缓存可以加快速度

    2. 目录和包的处理

      • 如果得到的是一个目录,则会被当做包来处理。这时先进入包目录,查找 package.json ,取出 main 属性指定的文件名定位。
      • 如果找不到这个文件或者没有 package.json , 会将 index 作为默认文件名
  3. 编译执行

node会新建一个模块对象,然后根据路径载入并编译,对应不同扩展名,载入方法不同:

  • .js 通过 fs 同步读取
  • .node 通过 dlopen()加载
  • .json 通过fs读取,再 JSON.parse
  • 其余扩展名都被当做 .js

每一个编译成功的模块都会被绑定在 Module._cache

编译过程对文件内容进行头尾包装

// 通过vm原生模块runInThisContext方法执行,不污染全局
(function (exports, require, module, __filename, __dirname) {
    
})

另外,这样会出错

exports = function () {
    // My class
}

原因在于,exports对象是通过形参的方式传入的,直接赋值会改变形参的作用,但并不能改变作用域外的值。

js核心模块的编译过程

  1. 转存为c/c++代码
  2. 编译js核心模块

c/c++核心模块编译过程

  1. 内建模块的组织方式

c++模块主内完成核心,js主外实现封装

性能优于脚本语言

被编译成二进制文件,一旦node开始执行,就直接加载进缓存

  1. 内建模块导出

依赖关系:文件模块 <-- 核心模 块<-- 内建模块

包与npm

cnpm搭建私有的npm服务

包结构

  • package.json 包描述文件

    • name:包名,不允许出现空格
    • description:包简介
    • version:版本号
    • keywords:关键词数组
    • maintainers:包维护者列表,每个维护者有name,email,web
    • dependencies:所需要的依赖包列表
    • devDependencies:只在开发时需要的依赖
    • scripts:脚本说明对象
    • main:模块引入方法require在引入包时,会优先检查这个字段,并将其作为包中其余模块的入口
    • bin:一些包作者希望包可以作为命令行工具,配置好bin后,通过npm install package_name -g将脚本添加到执行路径中,之后可以再命令行直接执行
  • bin 存放可执行二进制文件的目录

  • lib 存放js的代码目录

  • doc 存放文档

  • test 存放单元测试用例

常用功能

  1. 查看帮助npm help

  2. 安装依赖包npm install --save/--save-dev express

    1. 全局安装

    -g是讲一个包安装到全局可用的可执行命令。它根据包描述文件中的bin字段配置,将实际脚本连接到与node可执行文件相同的路径下

    如果node可执行文件的位置是/usr/local/bin/node ,那么模块目录就是/usr/local/lib/node_modules 。最后通过软链接方式将bin字段配置的可执行文件链接到node的可执行目录下

    1. 本地安装

      换源:

      1. npm install underscore --registry=http:registry.url
      2. npm config set registry http:registry.url
  3. npm钩子

  4. 发布包

    1. 编写模块
    2. 初始化包描述文件
    3. 注册包仓库账号 npm adduser
    4. 上传包 npm publish<folder>
    5. 管理包权限

    npm owner ls <package_name>

    npm owner add <user> <package_name>

    npm owner rm <user> <package_name>
    6. 分析包 npm ls

模块考察点

  1. 良好的测试
  2. 良好的文档
  3. 良好的测试覆盖率
  4. 良好的编码规范
  5. 更多条件

前后端共用模块

node模块引入几乎都是同步的,但如果前端模块也采用同步的方式来引入,用户体验会造成问题

AMD规范

需要用define来明确定义一个模块,而在node实现中是隐式包装的。

所有的依赖,通过形参传递到依赖模块内容中

define(['dep1', 'dep2'], function (dep1, dep2) {
    return function () {}   
})

目的是作用域隔离

内容需要返回的方式实现导出

define(function () {
    var exports = {};
    exports.sayHello = function () {
        ...
    }
    return exports
    
})

CMD规范

更接近commonjs规范

define(function (require, exports, module) {
    // ...
})

require,exports, module通过形参传递给模块。

兼容多种模块规范

;(function (name, definition) {
    var hasDefine = typeof define === 'function';
    var hasExports = typeof module !== 'undefined' && module.exports;
    if (hasDefine) { // AMD或者CMD
        define(definition);  
    } else if(hasExports) { // 定义为普通模块
        module.exports = definition()
    } else {
        this[name] = definition()
    }
})('hello', function () {
  var hello = function () {}  
  return hello
})

异步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

  1. 用户体验

    如果是同步,js执行ui渲染和响应将处于停滞状态

    采用异步,在下载资源期间,js和ui的执行都不会处于等待状态

    采用异步方式所花时间为max(m, n)

  2. 资源分配

    • 单线程串行依次执行

    缺点:

    单线程同步编程模型会因为阻塞i/o导致性能差,

    • 多线程并行完成

    缺点:

    代价在于创建线程和执行期线程上下文切换的开销较大

    多线程常面临锁,状态同步问题

    优点:

    但是能有效提升cpu利用率

node的异步i/o

模型基本要素:事件循环,观察者,请求对象,i/o线程池

node自身其实是多线程的,只是i/o线程使用的cpu较少

  1. 事件循环
  2. 观察者

每个事件循环中有一个或者多个观察者

  1. 请求对象

异步i/o过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及i/o操作完毕后的回调处理

  1. 执行回调

非i/o得异步api

  1. 定时器,setTimeout和setInterval

创建的定时器会被插入到定时器观察者内部的一个红黑树中

每次Tick执行时,会从红黑树中迭代取出定时器对象,检查是否超过定时时间。如果超过,就形成一个时间,它的回调函数将立即执行

时间复杂度O(lg(n))
2. process.nextTick

将回调函数放入队列,在下一轮Tick时取出执行

时间复杂度 0(1)

事件驱动与高性能服务器

服务器模型:

  • 同步式。一次只能处理一个请求,其他请求都在等待
  • 每进程/每请求。为每个请求启动一个进程,这样可以处理多个请求,但是系统资源只有那么多,所以不具备扩展性
  • 每线程/每请求。为每个请求启动一个线程来处理。当大并发请你去到来时,内存将用光。

node高性能:

  • node通过实践驱动的方式处理请求,无须为每一个请求创建额外的对应线程
  • 省掉创建和销毁线程的开销。
  • 线程少,上线文切换的代价少

异步编程

函数式编程

  1. 高阶函数,将函数作为输入或返回值
  2. 偏函数,创建一个调用另外一部分--参数或变量已预置的函数---的函数的用法。
var toString = Object.prototype.toString;
var isType = function (type) {
    return function (obj) {
        return toString.call(obj) == '[object' + type + ']'
    }
}
var isFunction = isType('Function')

优势

  1. 基于事件驱动的非阻塞i/o模型
  2. 使cpu与i/o并不相互依赖等待
  3. 并行带来的想象空间更大,延展开来是分布式和云

难点

  1. 异常处理

异步i/o提交请求和处理结果两个阶段中间,有事件循环的调度。异步方法则通常在提交请求后立即返回,因为一场并不一定发生在这个阶段,所以try/catch在这里无效

try/catch对于callback执行时抛出的异常无能为力

  1. 回调炼狱
  2. 阻塞代码,由于没有sleep,用setTimeout代替
  3. 多线程编程:web workers和child_process
  4. 异步转同步

异步编程解决方案

  1. 事件发布/订阅模式

    1. 继承events模块
    var events = require('events');
    function Stream () {
        events.EventEmitter.call(this)
    }
    util.inherits(Stream, events.EventEmitter)
    
    1. 利用事件队列解决雪崩问题,once方法

    2. 多异步之间的写作方案

      1. 利用哨兵变量
      2. EventProxy
  2. Promise/Deferred

    1. Promise/A

      • 只有三种状态:rejected,fullfiled, rejected

      • 只能未完成到完成,或者失败,不能逆反

      • 状态不能更改

  3. 流程控制库

    1. 尾触发和next
    2. async的parallel,waterful等方法
    3. step
    4. wind

内存控制

  • js在浏览器的应用场景,由于运行时间短,随着进程的推出,内存会释放,几乎没有内存管理的额必要

  • 内存控制正式在海量请求和长时间运行的前提下进行探讨的。

  • 在服务器端,资源寸土寸金

  • 对于性能敏感的服务器端程序,内存管理的好坏,垃圾回收状况的优良,影响很大

js引擎V8(虚拟机)

内存限制

在node中通过js使用内存时,只能使用部分,无法直接操作大内存对象

64位系统下约为1.4GB,32位系统下约为0.7GB

node中使用js对象,都是通过V8来进行分配和管理的

对象分配

js对象通过堆来分配

当在代码中生命变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆得大小超过V8的限制为止

V8为何限制堆得大小:表层原因是起初为浏览器而设计,限制值已经绰绰有余。深层原因是V8的垃圾回收机制的限制,做一次非增量式的垃圾回收时间花销大

垃圾回收机制

V8垃圾回收策略主要基 分代式垃圾回收机制

垃圾回收算法:

  1. V8的内存分带

将内存分为 新生代老生代

新生代中的对象为存活时间较短的对象,老生代的对象为存活时间较长或常驻内存的对象

  1. Scavenge算法

    • 具体实现主要采用Cheney算法

    • 采用复制的方式实现垃圾回收算法。

    • 将堆内存一分为二。每一份空间成为semispace。处于闲置状态的称为To空间,处于使用状态的称为From空间。

    • 当开始进行垃圾回收时,会检查From空间的存活对象,这些存活对象会被复制到To空间。非存活对象占用空间会被释放

    • 缺点:用空间换时间

    • 当一个对象经过多次复制依然存活时,被认为是生命周期较长的对象。被移到老生代中。称为晋升

    • 对象晋升的条件:

      1. 一个对象经历过Scavenge回收

      通过检查它的内存地址来判断。如果经历过了,从From复制到老生代

      1. To空间的内存占用比超过限制25%

缺点:1. 存活对象较多时,复制存活对象的效率低。 2. 浪费一般空间

  1. Mark-Sweep(标记清除)

    • 遍历堆中的所有对象,标记存活对象。在清除阶段只清除没有被标记的对象。
    • 标记清除后 内存空间出现不连续 的状态,如果需要分配一个大对象,就无法完成
  2. Mark-Compat(标记整理)

    • 对象在标记为死亡后,整理过程中,将活着的对象往一端移动。完成后,直接清理掉边界外的内存

    • 在空间不足以对从新生代晋升过来的对象进行分配时才使用

  3. Incremental Marking

    • 上述基本算法都需要将应用逻辑暂停下来,执行完垃圾回收后再恢复,这种行为成为 全停顿
    • 全堆垃圾回收的标记,清理,整理等动作造成停顿
    • 将一口气完成的标记改为增量标记,拆分成许多小“步进”
  4. 延迟清理和增量清理

  5. 并行标记和并行清理

小结:

  • web服务器的会话实现,一般通过内存来存储,但在访问了大的到时候会导致老生代中的存活对象骤增,不尽造成清理/整理过程费时,还会造成内存紧张,甚至溢出

查看垃圾回收日志

node --trace_gc -e "..."

可以了解垃圾回收的运行状况,找出哪些阶段比较费时

node --prof xx.js

会在该目录下生成v8.log文件,得到性能分析数据

node --prof-process isolate-0x103001200-v8.log

由于日志文件不具备可读性,故这样可以统计日志信息

高效使用内存

  1. 作用域

    • 函数调用,被调用时创建对应作用域,执行结束后作用域摧毁。
    var foo = function () {
        var local = {};
    }
    foo();
    

    内存回收过程:只被局部变量引用的对象存活周期较短,会被分配在新生代的From空间,在作用域释放后,局部变量local失效,引用的对象会在下次垃圾回收时被释放

    • with
    • 全局作用域

标识符查找:

js在执行时回去找该变量在哪里定义,在当前作用域没有查到,将会向上级的作用域里查找,直到查到为止

作用域链:

根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。

执行环境:

js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。

变量的主动释放:

全局变量,直到进程退出才释放。引用的对象常驻内存(老生代)。

可以用delete操作和重新赋值(null或者undefined)

  1. 闭包

实现外部作用域访问内部作用域中变量的方法

作用域中产生的内存占用不会得到释放。除非不再有引用,才会逐步释放

内存指标

进程的内存一部分是rss,其余部分在交换区或者文件系统中

$ node
> process.memoryUsage()
{
    rss:  // 常驻内存
    heapTotal: // 总申请的内存量
    heapUsed:  // 使用中的内存量
}
 
> os.totalmem()  // 总内存
> os.freemem()  // 闲置内存

Buffer对象并非通过V8分配,没有堆内存的大小闲置

小结:受V8的垃圾回收限制的主要是V8堆内存

内存泄漏

哪怕一字节的内存泄漏也会造成堆积,垃圾回收过程中将会耗费更多时间进行对象描述,应用响应缓慢,直到进程内存溢出,应用奔溃

原因:

  1. 缓存

缓存中存储的键越多,长期存活对象也就越多,常驻在老生代

普通对象无过期策略

var cached = {};
function get (key) {
    if (cached[key]) {
        return cached[key]
    } else {
        
    }
}
function set (key, value) {
    cached[key] = value;
}

解决:

  • 缓存限制策略

    超过数量,先进先出的方式进行淘汰

    设计模块时,应添加清空队列的相应接口

  • 缓存的解决方案

    进程间无法共享内存

    1. 将缓存转移到外部,减少常驻内存的对象的数量,让垃圾回收更高效
    2. 进程之间可以共享缓存
  1. 队列消费不及时

队列消费速度低于生产速度,将会形成堆积。而js相关作用域也不会得到释放,内存占用不会回落,从而出现内存泄漏

解决方案:

  • 表层:换用消费速度更高的技术
  • 深度:监控队列的长度
  • 任意异步调用都应该包含超时机制
  1. 作用域未释放

大内存应用

node中大多数模块都有stream应用。由于V8内存限制,采用流实现对大文件的操作

如果不需要进行字符串层面的操作,则不需要V8来处理,尝试进行纯粹的Buffer操作

Buffer

特点

  1. Buffer 类的实例类似于 整数数组 ,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存
  2. Buffer 的大小在被创建时确定,且无法调整。
  3. 性能相关部分由c++实现,非性能相关由js实现

内存分配

  • 在node的c++层面实现内存的申请,在js中分配内存
  • 使用slab分配机制
    • 预先申请,事后分配
    • slab状态:
      1. full,完全分配状态
      2. partial,没有分配诶状态
      3. empty,没有被分配状态
    • 同一个slab可能分配给多个buffer对象
    • 分配大Buffer对象,直接由c++层面提供的内存,而无需细腻的分配操作

乱码

  1. 缓冲器的大小取决于传递给流构造函数的 highWaterMark 选项
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)
})
  1. buffer对象的长度为11,可读流要读取很多次才能完成完整的读取
  2. 宽字节字符串可能存在被截断的情况。

解决乱码

  1. 设置编码
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});
render.setEncoding('utf8')

setEncoding的时候,可读流对象在内部设置了一个decoder对象。每次data事件都通过该decoder对象进行Buffer到字符串的解码。

decoder的对象会暂时存储,buffer读取的剩余字节

  1. 将小buffer对象合并
const fs = require('fs');
var reader = fs.createReadStream('./text.md', {highWaterMark: 11});

var chunks = [];
var size = 0;
reader.on('data', function (chunk) {
	chunks.push(chunk);
	size += chunk.length;
})
reader.on('end', function () {
	var buf = Buffer.concat(chunks, size);
	console.log(buf.toString())
})

Buffer与性能

  • 通过预先转换静态内容为Buffer对象,可以有效地减少cpu的重复使用,节省服务器资源
  • highWaterMark值的大小与读取速度的关系:该值越大,读取速度越快

网络编程

前言

在web领域,大多数的编程语言需要专门的web服务器作为容器,如ASP、ASP.NET需要IIS作为服务器,PHP需要打在Apache或Nginx环境等,JSP需要Tomcat服务器等。但对于Node而言,只需要几行代码即可构建服务器,无需额外的容器。

构建TCP服务

  • TCP

    • 面向连接的协议
    • 创建会话的过程,服务端和客户端分别提供一个套接字,共同形成连接。
    • 如果客户端要与另一个TCP服务通信,需要另创建一个套接字来完成连接
  • 创建TCP服务器端

const net = require('net');
let server = net.createServer();
server.on('connection', function (socket) {
    console.log('connection')
}) 
server.listen(8000)
  • TCP服务的事件
    • 服务器事件
      1. listening,在调用server.listen绑定端口或者Domain Socket后出发
      2. connection,每个客户端套接字连接到服务器端时触发,简洁写法为通过net.createServer,最后一个参数传递
      3. close,当服务器关闭时触发。server.close后,服务器将停止接受新的套接字连接
      4. error,当服务器发生异常时触发
    • 连接事件
      1. data,当一端调用write发送数据时,另一端会触发data事件
      2. end,当任意一端发送FIN数据时触发
      3. connect,用于客户端,当套接字与服务的连接成功时触发
      4. drain,当任意一端调用write发送数据时,当前这段会触发者事件
      5. error
      6. close,当套接字完全关闭时,触发
      7. timeout,当连接被闲置时触发

构建UDP服务

UDP不是面向连接的。

一个套接字可以与多个UDP服务通信,它虽然提供面向事务的简单不可靠信息传输服务,在网络差的情况下存在丢包严重的问题

优点:无连接,资源消耗低,处理快速且灵活

应用:音频,视频,dns服务

  • 创建UDP
const dgram = require('dgram');
const server = dgram.createSocket('udp4')
server.on('error', (err) => {
  console.log(`服务器异常:\n${err.stack}`);
  server.close();
});

server.on('message', (msg, rinfo) => {
  console.log(`服务器收到:${msg} 来自 ${rinfo.address}:${rinfo.port}`);
});

server.on('listening', () => {
  const address = server.address();
  console.log(`服务器监听 ${address.address}:${address.port}`);
});
server.bind(1000)
  • UDP套接字事件
    • message,当UDP套接字侦听网卡端口后,接收到消息时触发该事件
    • listening
    • close
    • error

HTTP

特点:

  1. 基于请求响应式,以一问一答的方式实现服务,虽然基于TCP会话,但是本身却并无会话的特点
  2. 浏览器,其实是一个HTTP的代理,用户的行为将会通过它转化为HTTP请求报文发送给服务端,服务端处理请求后,发送响应报文给代理,代理在解析报文后,将用户需要的内容呈现在界面上。
  3. TCP服务以connection为单位进行服务,HTTP服务以request为单位进行服务。http是将connection到request进行了封装
  4. 一旦开始了数据发送,writeHead和setHeader将不再生效。
res.writeHead(()
res.write() // 发送数据
res.end()
  • http服务端事件

    • connection,在http请求前,建立tcp时触发
    • request,当请求数据发送到服务端,在解析出http请求头后触发
    • close,当tcp连接断开
    • checkContinue,和request事件互斥。当客户端在发送较大数据的时候,并不会将数据直接发送,而是先发送一个头部带Expect:100-continue的请求到服务器,这是服务器会触发checkContinue
    • connect, 当客户端发起CONNECT请求时触发,而发起CONNECT请求通常在http代理时出现。
    • upgrade,当客户端要求升级连接的协议时,需要和服务端协商
    • clientError,连接的客户端触发error事件,传递到服务端
  • http客户端

示例:

var req = http.request(options, function (res) {
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log(chunk)
    })
})
  • http代理

在keepalive的情况下,一个底层会话连接可以多次用于请求。为了重用tcp连接,可以用http.globalAgent客户端代理对象

默认情况下,通过ClientRequest对象对同一个服务器发起的http请求最多可以创建五个连接

如需改变,可在options中传递agent选项

var agent = new http.Agent({
    maxSockets: 10
})
var options = {
    hostname: '127.0.0.1',
    port: 1334,
    path: '/',
    method: 'GET',
    agent: agent
}
  • http客户端事件
    • response:客户端在请求后得到服务端响应时触发
    • socket:当底层连接池中建立的连接分配给当前请求对象时触发
    • connect: 当客户端向浏览器发起CONNECT请求时,如果服务器端响应了200状态码,客户端会触发该事件
    • upgrade,客户端向服务器发起upgrade请求时,如果服务端响应了101 switching protocol状态
    • continue,客户端向服务端发起Expect:100-continue以试图发送大数据量

websocket服务

特点:

  1. 基于事件编程模型(事件驱动)
  2. 长连接
  3. 更接近于传输层协议,分为握手(由http完成)和数据传输两部分

好处:

  1. 客户端与服务端只建立一个TCP连接,可以使用更少的连接
  2. websocket服务端可以推送数据到客户端,比http请求响应模式更灵活,更高效
  3. 更轻量级的协议头,减少数据传送量

构建过程

  1. 握手
  2. 数据传输

握手完成后,不再进行http交互,客户端的onopen将会触发执行

当客户端调用send发送数据时,服务端触发onmessage事件;当服务端调用send发送数据时,客户端触发message事件。

当send发送一条数据时,协议可能将这个数据封装为一帧或多帧数据,然后逐帧发送

网络安全

  1. tls/ssl

交换公钥过程中,可能遇到中间人攻击,所以应引入数字证书来认证。

创建私钥:

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

验证:

const https = require('https');
const fs = require('fs');
const options = {
	key: fs.readFileSync('./ryans-key.pem'),
	cert: fs.readFileSync('./ryans-cert.pem')
}
https.createServer(options, function (req, res) {
	res.writeHead(200);
	res.end('hello world')
}).listen(2000)

-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对象

Url {
  protocol: 'https:',
  slashes: true,
  auth: 'user:pass',
  host: 'sub.host.com:8080',
  port: '8080',
  hostname: 'sub.host.com',
  hash: '#hash',
  search: '?query=string',
  query: 'query=string',
  pathname: '/p/a/t/h',
  path: '/p/a/t/h?query=string',
  href: 'https://user:[email protected]:8080/p/a/t/h?query=string#hash' }

查询字符串

查询字符串,如果键出现多次,那么它的值会是一个数组

foo=bar&foo=baz
var query = url.parse(req.url, true).query;
{
    foo: ['bar', 'baz']
}

cookie

cookie处理:

  1. 服务器向客户端发送cookie
  2. 浏览器将cookie保存
  3. 之后每次浏览器都会将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并不需要每次都用上,因为这会造成带宽的部分浪费

解决:

  1. 减少cookie体积,设置path和domain
  2. 为不需要cookie的组件换个域名
  3. 减少dns查询

session

session的数据只保留在服务器端,客户端无法修改。

应用:

  1. 基于cookie来实现用户和数据的映射

将口令放在cookie中,口令一旦被褚昂爱,就丢失映射关系。通常session的有效期通常短,过期就将数据删除

一旦服务器检查到用户请求cookie中没有携带session_id,它会为之生成一个值,这个值是唯一且不重复的值,并设定超时时间。如果过期就重新生成,如果没有过期,就更新超时时间

var sessions = {};
var key = 'session_id';
var EXPIRES = 20*60*1000;
var generate  = function () {
	var session = {};
	session.id = (new Date().getTime()) + Math.random();
	session.cookie = {
		expire: (new Date()).getTime() + EXPIRES
	}
	sessions[session.id] = session
}

function (req, res) {
	var id = req.cookies[key];
	if (!id) {
		req.session = generate();
	} else {
		var session = sessions[id];
		if (session) {
			if (session.cookie.expire > new Date().getTime()) {
				session.cookie.expire = new Date().getTime() + EXPIRES;
				req.session = session;
			} else {
				delete sessions[id];
				req.session = generate();
			}
		} else {
			req.session = generate();
		}
	}
}
  1. 通过检查字符串来实现浏览器端和服务器端数据的对应

原理:检查查询字符串,如果没有值,会生成新的带值的url


var getURL = function (_url, key, value) {
	var obj = url.parse(_url, true);
	obj.query[key] = value;
	return url.format(obj);
}

function (req, res) {
	var redirect = function (url) {
		res.setHeader('Location', url);
		res.writeHead(302);
		res.end();
	}
	var id = req.query[key];
	if (!id) {
		var session = generate();
		redirect(getURL(req.url), key, session.id);
	} else {
		var session = sessions[id];
		if (session) {
			if (session.cookie.expire > new Date().getTime()) {
				session.cookie.expire = new Date().getTime() + EXPIRES;
				req.session = session;
				handle(req, res);
			} else {
				delete sessions[id];
				var session = generate();
				redirect(getURL(req.url), key, session.id)
			}
		} else {
			var session = generate();
			redirect(getURL(req.url), key, session.id)
		}
	}
}

隐患

由于session存储在sessions对象中,故在内存中,若数据量加大,会引起垃圾回收的频繁扫描,引起性能问题。

为了利用多核cpu而启动多个进程,用户请求的连接将可能随意分配到各个进程中,node的进程与进程之间不能直接共享内存,用户的session可能会引起错乱

解决方案

将session集中化,将可能分散在多个进程里的数据,统一转移到集中数据存储中。目前常用工具是redis,memcached。node无需在内部维护数据对象。

问题: 会引起网络访问

session与安全

  1. 将口令通过私钥加密,使得伪造的成本较高

缓存

  1. 添加expires或者cache-control到报文头中
  2. 配置etags
  3. 让ajax可缓存

设置last-modified

var handle = function (req, res) {
	fs.stat(filename, function (err, stat) {
		var lastModified = stat.mtime.toUTCString();
		if (lastModified === req.headers['if-modified-since']) {
			res.writeHead(304, 'Not Modified');
			res.end()
		} else {
			fs.readFile(filename, function (err, file) {
				var lastModified = stat.mtime.toUTCString();
				res.setHeader('Last-modified', lastModified);
				res.writeHead(200, 'ok');
				res.end(file);
			})
		}
	})
}

缺陷:

  1. 文件的时间戳改动但内容不一定改动
  2. 时间戳只能精确到秒级别

设置etag


var getHash = function (str) {
	var shasum = crypto.createHash('sha1');
	return shasum.update(str).digest('base64');
}

var handle = function (req, res) {
	fs.readFile(filename, function (err, file) {
		var hash = getHash(file);
		var noneMatch = req['if-none-match'];
		if (hash === noneMath) {
			res.writeHead(304, "Not Modified");
			res.end()
		} else {	
			res.setHeader("ETag", hash);
			res.writeHead(200, "ok");
			res.end(file);
		}
	})
}

强制缓存

var handle = function (req, res) {
	fs.readFile(filename, function (err, file) {
		res.setHeader("Cache-Control", "max-age=" + 10*365*24*60*60*1000);
		res.writeHead(200, "ok");
		res.end(file);
	})
}

用expires可能导致浏览器端与服务器端时间不同步带来的不一致性问题

清除缓存

浏览器是根据url进行缓存,那么一旦内容有所更新时,我们就让浏览器发起新的url请求,使得新内容能够被客户端更新。

数据上传

var hasBody = function (req) {
	return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
}

function (req, res) {
	if (hasBody(req)) {
		var buffers = [];
		req.on('data', functino (chunk) {
			buffers.push(chunk);
		})
		req.on('end', function () {
			req.rawBody = Buffer.concat(buffers).toString(); // 拼接buffer
			handle(req, res);
		})
	} else {
		handle(req, res);
	}
}

处理json格式

// application/json;charset=utf-8;
var mime = function (req) {
	var str = req.headers['content-type'] || '';
	return str.split(';')[0]
}

var handle = function (req, res) {
	if (mime(req) === 'application/json') {
		try {
			req.body = JSON.parse(req.rawBody);
		} catch(e) {
			res.writeHead(400);
			res.end("Invalid JSON");
			return 
		}
	}
	todo(req, res)
}

处理xml文件

var xml2js = require('xml2.js');
var handle = function (req, res) {
	if (mime(req) === 'appliction/xml') {
		xml2js.parseString(req.rawBody, function (err, xml) {
			if (err) {
				res.writeHead(400);
				res.end('Invalid XML');
				return;
			}
			req.body = xml;
			todo(req, res);
		})
	}
}

图片上传

var formidable = require('formidable'),
    http = require('http'),
    util = require('util'),
    fs = require('fs');

http.createServer(function(req, res) {
  if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
    // parse a file upload
    var form = new formidable.IncomingForm();
    form.parse(req, function(err, fields, files) {
    	fs.renameSync(files.upload.path,"./tmp/text.jpeg"); // 另存图片
		res.writeHead(200, {'content-type': 'text/plain'});
		res.write('received upload:\n\n');
		res.end(util.inspect({fields: fields, files: files}));
    });

    return;
  }

  if (req.url == '/')

  // show a file upload form
  res.writeHead(200, {'content-type': 'text/html'});
  res.end(
    '<form action="/upload" enctype="multipart/form-data" method="post">'+
    '<input type="text" name="title"><br>'+
    '<input type="file" name="upload" multiple="multiple"><br>'+
    '<input type="submit" value="Upload">'+
    '</form>'
  );
}).listen(8080);

数据上传与安全

  1. 内存限制

在解析表单,json和xml部分,我们采取的策略是先保存用户提交的所有数据,然后再解析处理,最后才传递给业务逻辑。

弊端:数据量大,占内存

解决方案:

  1. 限制上传内容的大小,一旦超过限制停止接收数据,并相应400状态码
  2. 通过流式解析,将数据导向到磁盘中,node只保存文件路径等小数据

限制大小方案代码:

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);
}
  1. 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);
    }
    
}

路由解析

文件路径型

  1. 静态文件,其url的路径与网站目录的路径一致,无需转换。
  2. 动态文件,根据路径执行动态脚本,原理: web服务器根据url路径找到对应的文件,如index.asp或者index.php。根据后缀寻找脚本的解析器,并传入http请求的上下文。然而node中无需按这种方式

mvc工作模式

  1. 路由解析,根据url寻找到对应的控制器和行为
  2. 行为调用相关的模型,进行数据操作
  3. 数据操作结束后,调用视图和相关数据进行页面渲染,输出到客户端

手工映射

自由映射,从入口程序中判断url,然后执行对应的逻辑。

匹配的时候,能够正则匹配

自然映射

/controller/action/param1/param2/param3

按约定去找controllers目录下的user文件,将其require出来,调用这个文件模块的setting方法,其余的参数直接传递到这个方法中

RESTful(representational state transfer)

需要区分请求方法

一个地址代表了一个资源,对这个资源的操作,主要体现在http请求方法上,不是体现在url上

设计:

POST,GET,PUT,DELETE

POST /user/add?username=jack
GET /user/remove?username=jack

中间件

含义:指底层封装细节,为上层提供更方便服务的意义,为我们封装所有http请求细节处理的中间件

中间件性能

  1. 编写高效的中间件

缓存需要重复计算的结果,避免不必要的计算。

  1. 合理使用路由,是的不必要的中间件不参与请求处理过程

页面渲染

内容响应

响应头中的content-*字段十分重要。

示例

Content-Encoding:gzip
Content-Length:21170
Content-Type:text/javascript;charfset=utf-8

客户端在接收到后,通过gzip来解码报文体重的内容,用长度校验报文体内容是否正确,然后在以字符集utf-8将解码后的脚本插入到文档节点中

  1. MIME

application/json, application/xml, application/pdf

  1. 附件下载

背景:无论响应的内容是什么MIME,只需要弹出并下载它

Content-Disposition

判断是应该将报文数据当做及时浏览的内容,还是可下载的附件。

inline // 内容只需查看
attachment // 数据可以存为附件

还能指定保存时使用的文件名

Content-Disposition:attachment;filename="filename.txt"

响应附件api

res.sendfile = (filepath) => {
	fs.stat(filepath, (err, stat) => {
		let stream = fs.createReadStream(filepath);
		res.setHeader("Content-Type", mime.lookup(filepath));
		res.setHeader("Content-length", stat.size);
		res.setHeader("Content-Disposition", 'attachment;filename="'+ path.basename(filepath) +'"')
		res.writeHead(200);
		stream.pipe(res);
	})
}
  1. 响应json
res.json = function (json) {
    res.setHeader("Content-Type", "application/json");
    res.writeHead(200);
    res.end(JSON.stringify(json))
}
  1. 响应跳转
res.redirect = function (url) {
    res.setHeader('Location', url);
    res.writeHead(200);
    res.end('redirect to' + url)
}

视图渲染

res.render = function (view, data) {
    res.setHeader("Content-Type", "text/html");
    res.writeHead(200);
    var html = render(view, data);
    res.end(html)
}

模板要素:

  1. 模板语言
  2. 包含模板语言的模板文件
  3. 拥有动态数据的数据对象
  4. 模板引擎
    1. 语法分解
    2. 处理表达式
    3. 生成待执行的语句
    4. 与数据一起执行,生成最终字符串
  5. 模板安全,防止xss,就是转译
function render (str, data) {
    var tpl = str.replace(/<%=([\s\S]+?)%>/g, function (match, code) {
        return "' + obj." + code + "+ '";
    })
    tpl = "var tpl = '" + tpl + "'\nreturn tpl;";
    var compiled = new Function('obj', tpl);
    return compiled(data);
}

集成文件系统

fs.readFile('file/path', 'utf8', function (err, txt) {
    if(err) {
        res.writeHead(500, {'Content-Type': 'text/html'});
        res.end('模板文件错误');
        return;
    }
    res.writeHead(200, {"Content-Type": "text/html"});
    var html = render(compile(text), data);
    res.end(html);
})

这样做每次都需要读取模板文件,因此可设置cache={}

模板性能

  1. 缓存模板文件
  2. 缓存文件编译后的函数

进程

一个进程只能利用一个核,如何充分利用多核cpu服务器

单线程上抛出的异常没有被捕获,如何保证进程的健壮性和稳定性

石器时代:同步

一次只为一个请求服务

青铜时代:复制进程

通过进程的赋值同时服务更多的请求和用户。进程赋值会导致内存浪费

白银时代:多线程

一个线程服务一个请求,线程相对于进程的开销要小,线程之间可以共享数据,内存浪费问题得到解决

但是线程上线文切换会产生时间消耗

黄金时代:事件驱动

解决高并发问题

单线程避免不必要的内存开销和上下文切换

php为每个请求都简历独立的上下文

多线程架构

master.js实现进程的复制

let fork = require('child_process').fork;

let cpus = require('os').cpus();

for (let i = 0; i < cpus.length; i++) {
	fork('./worker.js');
}

worker.js

const http = require('http');
http.createServer((req, res) => {
	res.writeHead(200, {"Content-Type": "text/plain"});
	res.end('hello')
}).listen(parseInt(Math.random()*10000), '127.0.0.1')

ps aux | grep worker.js查看进程的数量

lejunjie          3306   0.0  0.0  4267752    868 s001  S+   11:18上午   0:00.00 grep worker.js
lejunjie          3171   0.0  0.3  4893888  21656 s000  S+   11:18上午   0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie          3170   0.0  0.3  4893888  21632 s000  S+   11:18上午   0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie          3169   0.0  0.3  4893888  21708 s000  S+   11:18上午   0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js
lejunjie          3168   0.0  0.3  4893888  21664 s000  S+   11:18上午   0:00.13 /Users/lejunjie/.nvm/versions/node/v8.11.1/bin/node ./worker.js

通过fork复制的进程都是一个独立的进程,启动多个进程只是为了充分将cpu资源利用起来,而不是为了解决并发问题

创建子进程

  1. spawn,启动一个子进程来执行命令

cp.spawn('node', ['worker.js']);

  1. exec,情动一个子进程来执行命令

sp.exec('node worker.js', () => {})

  1. execFile

启动一个子进程来执行可执行文件

  1. fork

创建node子进程只需要指定要执行的javascript文件模块

进程间通信

主线程与工作线程之间通过onmessage和postMessage进行通信,子进程对象则由send方法实现主进程向子进程发送数据

parent.js

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'})

child.js

process.on('message', function (data) {
	console.log('child: ' + data.name);
})
process.send({name: 'child'})

结果

child: parent
parent data: child

ipc进程间通信(inter-process communication)

node中实现ipc通道的是管道技术,具体由libuv提供

父进程在实际创建子进程之前,会创建ipc通道并监听它,然后才真正创建子进程,并通过环境变量告诉子进程这个ipc通道的文件描述符。

双向通信,在系统内核中完成通信,不用经过实际的网络层

句柄传送

多个进程监听通过端口会抛出EADDRINUSE异常,这是端口被占用的情况。可以通过代理,在代理进程上做适当的负载均衡,使得每个子进程可以较为均衡地执行任务。但是代理进程连接到工作进程的过程需要用掉两个文件描述符

句柄是一种可以用来标识资源的应用,他的内部包含了只想对象的文件描述符。比如句柄可以用来表示一个服务器端socket对象,一个客户端socket对象,一个udp套接字,一个管道等。

发送句柄使得主进程接收到socket请求后,将这个socket直接发给工作进程,而不是重新与工作进程之间建立新的socket连接来转发数据。解决文件描述符的浪费问题

parent.js

const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');

var server = require('net').createServer();
server.on('connection', (socket) => {
	socket.end('handled by parent');
})
server.listen(1338, () => {
	child1.send('server', server);
	child2.send('server', server);
})

child.js

process.on('message', (m, server) => {
	if (m === 'server') {
		server.on('connection', function (socket) {
			socket.end('handled by child , pid is' + process.pid);
		})
	}
})

让请求都由子进程处理

parent

const cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');

var server = require('net').createServer();
server.on('connection', (socket) => {
	socket.end('handled by parent');
})
server.listen(1338, () => {
	child1.send('server', server);
	child2.send('server', server);
	server.close();
})

child

var http = require('http');
var server = http.createServer((req, res) => {
	res.writeHead(200, {"Content-Type": "text/plain"});
	res.end("handled by child, pid is" + process.pid);
})
process.on('message', (m, tcp) => {
	if (m === 'server') {
		tcp.on('connection', function (socket) {
			server.emit('connection', socket);
		})
	}
})

多个子进程可以同时监听相同端口,再没有EADDRINUSE异常发生

总结:

  1. 发送到ipc管道的实际是要发送的句柄文件描述符
  2. 连接了ipc通道的子进程可以读取到父进程发来的消息,将字符串还原成对象,才出发message时间将消息体传递给应用层使用
  3. 并非任意类型的句柄都能在进程之间传递,除非有完整的发送和还原的过程
  4. 多个进程监听同个端口不引起EADDRINUSE异常的原因

独立启动的进程中,tcp服务器端socket套接字的文件描述符并不相同,导致监听到相同的端口时会抛出异常

多个应用监听相同端口时,文件描述符同一时间只能被某一个进程所用,所以是抢占式的

进程事件

  1. error,当子进程无法被复制创建,无法被杀死,无法发送消息时触发
  2. exit,子进程退出时触发
  3. close,在子进程的标准输入输出终止时触发该事件
  4. disconnect,在父进程或子进程中调用disconnect方法时触发

自动重启

进程退出时,让所有工作进程退出。子进程退出时重新create

const cp = require('child_process');

var server = require('net').createServer();

var cpus = require('os').cpus();
var workers = {};
function create () {
	var worker = cp.fork('./child.js');
	worker.on('exit', function () {
		console.log('worker: ' + worker.pid + 'exited');
	})
	worker.send('server', server);
	workers[worker.pid] = worker;
	console.log('create worker pid: ' + worker.pid);
}
for (var i = 0; i < cpus.length; i++) {
	create();
}

process.on('exit', function () {
	for (var pid in workers) {
		workers[pid].kill();
	}
})

在极端情况下,所有工作进程都停止接受新的连接,全出在等待退出的状态。但在等进程完全退出才重启的过程中,所有新来的请求可能存在没有工作进程为新用户服务的情景,这会丢掉大部分请求

因此可在子进程中监听uncaughtException,然后发送自杀信号

process.on('uncaughtException', function (err) {
    process.send({act: 'suicide'});
    worker.close(function () {
        process.exit(1);
    })
})

负载均衡

node默认提供的机制是采用操作系统的抢占式策略。

新的策略是轮叫调度。工作方式是由主进程接受连接,将其一次分发给工作进程。

状态共享

在多个进程之间共享数据

  1. 第三方数据存储

实现同步:子进程向第三方进行定时轮训

  1. 主动通知

主动通知子进程,轮训。

cluster模块

要创建单机node集群,由于有许多细节需要处理,于是引入cluster,解决多核cpu的利用率问题

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);

  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  cluster.on('listening', () => {
    console.log('listening')
  })
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是一个 HTTP 服务器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);
  console.log(`工作进程 ${process.pid} 已启动`);
}
process.on('exit', () => {
  console.log('exit')
})

原理:cluster模块就是child_process和net模块的组合应用。在fork子进程时,将socket的文件描述符发送给工作进程。通过so_reuseaddr端口重用,从而实现多个子进程共享端口。

产品化

项目工程化

项目的组织能力

  1. 目录结构
  2. 构建工具
  3. 编码规范
  4. 代码审查

部署流程

代码流程--》stage普通测试环境--》pre-release预发布环境--》product实际生产环境

部署操作

node file.js以启动应用,会站住一个命令行窗口,窗口退出进程也退出

nohup node app.js & 不挂断进程的方式

bash脚本, 解决进程id不容易查找的问题。重启,中断,启动

性能

动静分离:

让node只处理动态请求,将静态文件引导到专业的静态文件服务器。用nginx或者专业的cdn来处理

cdn缓存,将文件放在离用户尽可能近的服务器

对静态请求使用不同的域名或者多个域名还能消除掉不必要的cookie传输和浏览器对下载线程数的限制

启用缓存

提升服务速度,避免不必要的计算

多进程架构

读写分离

对数据库进行主从设计,这样读取数据操作不再受到写入的影响,降低了性能的影响。

日志

写到磁盘上

数据库写入要经历锁表,日志等操作,如果大量访问会排队,进而内存泄露。

  1. 访问日志
  2. 异常日志

监控报警

监控

  1. 日志监控

通过监控异常日志文件的变动,将新增的异常按异常类型和数量反应出来。

监控访问日志,体现业务qps值,pv/uv,预知访问高峰

  1. 响应时间

在nginx类的反向代理上监控

通过应用自行产生的访问日志来监控

  1. 进程监控

检查操作系统中运行的应用进程数,对于采用多进程架构的web应用,就需要检查工作进程的数量,如果低于预估值,就应当发出报警

  1. 磁盘监控

监控磁盘的用量,设置警戒值

  1. 内存监控

健康的内存是有升有降的

  1. cpu占用监控

cpu分为内核态,用户态,iowait等。

用户态占用高: 服务器上应用大量cpu开销

内核态占用高:服务器花费大量时间进程调度或者系统调用。

  1. cpu load监控(cpu平均负载)

描述操作系统当前的繁忙程度

指标过高,在node中可能体现在用子进程模块反复启动新的进程

  1. i/o负载

反应磁盘读写情况

  1. 网络监控

流入流量和流出流量

  1. 应用状态监控

  2. dns监控

报警的实现

  1. 邮件报警
  2. 短信报警
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant