深入浅出Nodejs之笔记

作者: lmmy123 | 来源:发表于2019-12-02 19:59 被阅读0次

    深入浅出Nodejs

    模块机制

    • Commonjs规范

    • node的模块实现

      步骤:

      • 路径分析

      • 文件定位

      • 编译执行

        核心模块在node进程启动时,会直接加载进内存中,所以文件定位和编译执行两步可以省略,加载速度快

        文件模块则是运行时动态加载,需要经过上面3个步骤

        1. 优先从缓存加载

          会对引入过的模块进行缓存

        2. 路径分析和文件定位

          核心模块/ 以./ 等开头的相对路径文件/以/开头的绝对路径

          模块的路径转为真实路径,作为索引

          module.paths,依次向上查找node_modules目录

        3. 模块编译

          // 针对js模块进行一层包装
          (function(exports, require, module, __filename, __dirname) {
              var math = require('math')
              exports.area = function(radius) {
                  return radius
              }
          })
          

          exports 和 module.exports的区别

          exports是module.exports的引用

          exports === module.exports

          exports返回的是模块函数,使用点运算符

          module.exports返回的是模块对象本身,返回的是一个类

          exports.age = 18
          moudle.exports = {name: 18}
          
          
          require('.a.js')
          // 引入的是 module.exports
          
    • npm包规范

      • 包结构

        Package.json / bin / lib / doc / test

      • 常用功能

    • 局域npm

    • AMD / CMD

      是解决common js 在客户端中异步加载的场景

      • amd 依赖前置,声明时指定依赖

        definde(['dep1, dep2'], function(dep1, dep2) {
            
        })
        
      • cmd, 依赖前置,支持动态引入

        define(function(require, exports, module){
            ...
            require('./xx.js')
        })
        
    • umd

      兼容多种模块规范

      ;(function(root, factory){
          if(typeof exports === 'object' && typeof module === 'object')
            module.exports = factory(require("vue"));
        else if(typeof define === 'function' && define.amd)
              //// AMD环境 CMD环境
            define("components", ["vue"], factory);
        else if(typeof exports === 'object')
              //// 定义为 通Node模块
            exports["components"] = factory(require("vue"));
        else
              // 将模块的执行结  在window 量中 在  器中this  window对象
            root["components"] = factory(root["Vue"]);
      })(window, function(){...})
      

    异步I/O

    • 单线程

      无法利用多核cpu的优势和阻塞带来的延迟

    • 多进程

      进程创建和上下文切换的开销,状态同步和锁的问题

    node的方案:

    ​ 单线程 + 非阻塞异步 I/O

    ​ child_process / cluster模块提供多进程,可以利用多核cpu

    非阻塞I/O,为了获取i/o响应的数据,需要重复调用i/o是否完成 — 轮询

    • read

      重复检查i/o状态是否完成数据的读取

    • select

      在read基础上的改进

      通过对文件描述符上的事件状态来判断

    • poll

      效率最高的i/o事件通知机制

      事件订阅,事件通知,执行回调的方式

    理想的非阻塞异步I/O

    • 事件循环 event loop

    • 观察者

    • 请求对象

    node event loop

    process.nextTick 会优于其他microtask执行

    • timer

      执行定时器

    • i/o

    • Idle /prepare

    • poll

      1. 回到timer阶段,执行到时的回调

      2. 执行poll队列中的事件

        poll 中没有定时器的情况下

        • poll队列不为空, 便利回调队列并同步执行
        • poll队列为空,
          • 有setImmediate需要执行, 进入check阶段执行setImmediate
          • 没有setImmediate,会等待回调被加入到队列中并立即执行
      fs.readFile(__filename, () => {
          setTimeout(() => {
              console.log('timeout')
          }, 0)
          
          setImmediate(() => {
            console.log('setImmediate')
        })
      })
      // 执行顺序 setImmediate -》 timeout
      // readFile的回调在poll阶段执行
      // 发现setImmediate,去执行 setImmediate
      // 再到timer阶段执行回调
      
    • check

    • Close

    异步编程

    函数式编程
    • 高阶函数

    • 偏函数,将多参数的函数,返回一个预设部分参数的函数。可接受剩下参赛的函数

      柯里化,是偏函数的一种,将多参数的函数,变为接受单一参数的函数

      function curry(fn) {
          var content = this
          var args = [...arguments].slice(1)
          return function(){
              var finalArgs = args.concat([...arguments])
              fn.apply(content, finalArgs)
          }
      }
      

      多线程:缺点,上下文切换开销,死锁,同步问题

    • node的优势

      基于事件驱动的非阻塞I/O模型

      借用v8高性能引擎

    • 异步编程难点

      1.**异常处理 **

      • try/catch/final不适用异步编程错误捕获

      • 第一参数异常

        node在处理异常形成的约定,回调函数的一个参数为err

        fs.readFile('dd.js', function(err, data) {
            // err就是捕获的异常信息
        })
        

      2.函数嵌套过深

      3.阻塞代码

      ​ 需要阻塞代码,不能达到真正的线程沉睡,cpu资源会一直为其服务

      4.多线程编程

      ​ 发挥多核cpu的优势,借助web worker模式,node提出child_process,cluster 模块,用于多线程编程

      5.异步转同步

    • 异步编程的解决方案

      • 事件发布/订阅模式

        • node中event模块就是发布/订阅模式的实现案例
        • 解耦业务逻辑,添加侦听器,当emit事件时,添加在这个事件的侦听器进行变化
        • 事件发布/订阅模式的特点为执行流程需要被预先设定
      • promise/deferred模式

        • 执行流程不需要被预先设定

          $.get('/api').then(res=>{...})
          
        • promise/deferred模式发布在commonjs规范中,已经抽象出promise/A,promise/B等模型规范

          Promise/A规范

          定义里3中状态(pendding, resolve,rejected)

          promise对象具备then方法即可

          状态不可逆

          • Promise 用于外部,通过then方法收集事件,
          • Deferred 延迟对象,用于内部,维护状态修改,触发相应状态事件
        • then链式调用

    • 流程控制库

      • 中间件middleware

      • 尾触发与next

    • 异步并发控制

      避免高并发

      • 通过队列来控制并发量,设定阀值,超出部分放入队列中,等调用结束后,从队列中取出并执行
      • 超时控制,设置时间阀值

    内存控制

    V8的垃圾回收机制和内存限制
    • v8的内存限制

      64位约1.4GB,32位约0.7GB,导致node无法直接操作大内存对象

    • v8的对象分配

      process.memoryUsage() / 查看v8堆内存信息

      调整内存限制的大小 --max-old-space-size --max-new-space-size=1024

    • v8垃圾回收机制

      • v8主要的垃圾回收算法

        分代式垃圾回收机制

        • 按对象的存活时间分为老生代和新生代

        • Scavenge算法(新生代)

          新生代中的对象主要通过scavenge算法进行垃圾回收

          采用了cheney算法,一种采用复制的方式实现的垃圾回收算法,将内存一分位二,from和to空间,把from空间中存活的对象复制到to空间,from中非存活的对象被回收,让后将to空间中的对象复制到from空间,to空间清空

          缺点是,将内存一分为二,牺牲了空间换时间,所以非常适合新生代中

          回收过程中,新生代的对象会晋升为老生代中(满足已经被scavenge过等等条件)

        • 标记清除 && 标记整理(老生代)

          遍历对象,标记活着的对象,最后清除未被标记的对象

          垃圾回收后,有可能造成内存空间不连续的状态(空间碎片),Mark-Compact(标记整理)来解决

          Mark-Compact:回收过程中,将活着的对象向一端移动,形成连续新存储

        • incremental marking(增量标记)

          垃圾回收会造成js运行停顿

          增量标记将垃圾回收拆分为许多小的‘步进’,每完成一段,将执行栈交换回js运行应用,然后再执行垃圾回收。。。,直到垃圾回收完成

    • 查看垃圾回收日志

      node --trace_gc xxxxxxx

    高效实用内存
    • 作用域(scope)

      全局作用域,函数作用域,with作用域

      作用域链查找,一直向上查找,直到全局作用域

      全局作用域直到进程退出才能释放变量

    • 闭包(closure)

      实现外部作用域可以访问内部作用域中变量的函数或方法叫做闭包

      作用域一直在内存中占用,不会释放

    buffer对象不经过v8的内存分配机制,不会有堆内存的大小限制,利用堆外内存可以突破内存限制的问题

    内存泄漏
    • 缓存

      缓存的对象会长驻内存中,由于js对象没有过期策略,会导致缓存长期存在

      解决方法:限度缓存对象的大小,加上完善的过期策略(设置过期时间或设定对象大小的阀值)

      模块也有缓存机制

      node的解决方法:

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

      消费速度跟不上生成速度,造成队列的累积

      • 监控队列的长度,一旦堆积,触发报警系统
      • 任意异步调用都包含超时机制
    • 作用域未释放

      闭包。。。全局变量

    内存泄漏排查
    • node-heapdump插件
    • node-memwatch插件
    大内存应用

    不可避免还是会操作大文件的场景,使用stream模块,继承EventEmitter,具备事件功能

    由于v8的内存限制,使用fs.createReadStream/createWriteStream 替代fs.readFile/writeFile

    不考虑字符串的情况下,使用buffer, buffer不受v8堆内存的限制

    Buffer

    • 类似于数组的二进制数据,他的元素为16进制的两位数,即0到255的数值
    内存分配

    buffer对象不占用v8的堆内存中,便于处理大量的字节数据,在c++层申请内存,在js中分配内存的策略

    slab分配机制

    buffer的转换

    • 字符串转buffer __new Buffer(str, [encoding])
    • buffer转字符串 —— buf.toString([encoding], [start], [end])
    • 判读是否支持的编码类型 —— Buffer.isEncoding(encoding)

    buffer的拼接

    setEncoding(encoding)
    

    buffer与性能

    通过预先将静态内容转为buffer对象,可以有效减少cpu的重复使用,节省服务器资源

    网络编程

    node提供了net,dgram,http,https模块用于搭建服务器

    • tcp

      传输层控制协议

      面向连接的协议,需要3次握手建立连接, 在创建回话的过程中,服务端和客户端分别提供一个套接字,这两个套接字共同形成一个连接

      开启keepalive,一个tcp会话可以用于多次请求和响应

      var net = require('net')
      var server = net.createServer(function(socket) {
          socket.on('data', function(data) {
              socket.write('hello word')
          })
          socket.on('end', function(){
              console.log('connect abort')
          })
          socket.write('sdjfsdjfs')
      })
      server.listen(8124, function(){
          console.log(serve bound)
      })
      
      // 客户端
      var net = require('net')
      var client = net.connect({port: 8124}, function() {
          console.log('client connected')
        client.write('word\r\n')
      })
      client.on('data', function(data){
          console.log(data)
      })
      client.on('end', function() {
          console.log('client disconnected')
      })
      
      • 服务器事件

        • listen事件
        • connection事件
        • close事件
        • error事件
      • 连接事件

        服务器可以同时与多个客户端保持连接,对每个连接而言是典型的可写可读stream对象,stream对象用于服务器端和客户端之间的通信

        • data

          当一端调用了write()发送数据时,另一端会触发data事件

        • end

        • connect 用于客户端连接

        • drain 当一端调用write()发送数据时,当前这端会触发drain事件

        • error 异常事件

        • close 当套接字完全关闭时,会触发

        • timeout 当一定时间后连接不再活跃时,该事件将会被触发,通知用户当前该连接已经被闲置

        tcp 中的Nagle算法

        针对小数据包,采用延迟,合并数据包,达到一定数量或时间后发出,以此优化网络

        tcp默认开启nagle算法,可以调用socket.setNoDelay(true)去掉

    • UDP

      用户数据包协议

      不是面向连接的,无需连接

      一对多,多对多

      安全性,可靠性低

      适用于现在的直播,视频

      // 创建udp套接字
      var dgram = require('dgram')
      var server = dgram.createSocket('upd4')
      server.on('message', function(msg, rinfo) {
          console.log('server got:' + msg + 'from' + rinfo.address + rinfo.port)
      })
      server.on('listening', function() {
          var address = server.address()
          console.log('server listening' + address.address + address.port)
      })
      server.bind(41234)
      
      // 客户端
      var dgram = require('dgram')
      var message = Buffer.from('深入浅出node.js')
      var client = dgram.createSocket('upd4')
      client.send(message, 0, message.length, 41234, 'localhost', function(err, bytes){
          client.close()
      })
      

      send方法将信息发送到服务端

      socket.send(buf, offset, length, port, address, [callback])

      • 套接字事件
        • message
        • listening
        • close
        • error
    • Http

      应用层协议,基于tcp协议

      基于请求响应式,一问一答实现服务

      http服务端的事件

      • connection事件 tcp连接后触发
      • request事件
      • close事件
      • checkContinue事件
      • connect事件 客户端发起请求时触发
      • upgrade事件 客户端升级连接的协议时,服务端接受到时触发
      • clientError事件 连接的客户端触发error事件,服务端接受到错误时触发

      http客户端

      ​ http.request(options, connect) 构造http客户端

      var options = {
          hostname: '127.0.0.1',
          port: 1234,
          path: '/',
          method: 'GET',
          headers: {},
          // auth basic认证, 这个值将被计算成请求头中的authorization部分
      }
      var req = http.request(options, function(res) {
          res.setEncoding('utf-8')
          res.on('data', function(chunk) {
              console.log(chunk)
          })
      })
      req.end()
      

      http代理

      new http.Agent({
        maxSockets: 10, //当前连接池中使用的连接数
          requests: 5 // 处于等待状态的请求数
      })
      

      http客户端事件

      • response
      • socket 服务端响应了200状态码,客户端将会触发该事件
      • upgrade
      • continue 服务端响应100 continue,客户端将触发该事件

      构建websocket服务

      • 客户端和服务端只要建立一个tcp连接
      • server push,双向通信
      • 更轻量的协议头,减少数据传送量
      // websocket在客户端中应用
      var socket = new WebSocket('ws://127.0.0.1:1200/updates')
      socket.onopen = function() {
          
      }
      socket.onmessage = function(event){
          
      }
      

      websocket的握手

      • Sec-WebSocket-Key 用于安全校验, 值为base64编码的字符串

      Comet 长轮询

      网络服务与安全

      • SSL(secure sockets layer)安全套接层,一种安全协议,在传输层对网络连接加密
      • 最初ssl应用在web上,后期标准化,称为TLS(transport layer security)安全传输层协议

      node提供的3个模块

      • crypto 加密解密

        SHA1, MD5

      • tls

        提供了与net模块类似的功能,区别在与它建立在TLS/SSL加密的TCP连接上

        TLS/SSL

        是一个公钥/私钥的结构,它是一个非对称的结构

        每个客户端和服务端都有自己的公私钥,公钥用来加密,私钥用来解密

        建立安全传输之前,客户端和服务器端之间需要互换公钥,客户端发送数据需要服务端的公钥加密,服务端收到后用自己的私钥解密,反之亦然

        node在底层采用的是openssl实现TLS/SSL的,为此生成公钥私钥可以通过openssl完成

        // 生成服务器端私钥
        > openssl genrsa -out server.key 1024
        // 生成客户端私钥
        > openssl genrsa -out client.key 1024
        
        //生成公钥
        > openssl rsa -in server.key -pubout -out server.pem
        
        > openssl rsa -in client.key -pubout -out client.pem
        
        

        中间人攻击

        在客户端和服务器端在交换公钥的过程中,有可能受到中间人攻击,中间人对客户端扮演服务端的角色,对服务端扮演客户端角色,分别获取相应的公钥,存在安全威胁

        解决方法:

        ​ 需要对公钥进行认证,确认得到的公钥出自目标服务器

        ​ 数字证书,其中包含了服务器的名称和主机名,服务器公钥,签发机构的信息和签名

        ​ CA(数字证书认证中心),作用为站点颁发证书,且这个证书中具有ca通过自己公钥和私钥实现的签名,服务器需要通过自己的私钥生成CSR(证书签名请求文件)

        ​ 中小企业多半采用自签名证书,就是自己扮演CA机构

        // 扮演ca机构 生成私钥,生成csr文件,通过私钥自签名生成证书的过程
        > openssl genrsa -out ca.key 1024
        > openssl req -new -key ca.key -out ca.csr
        > openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt
        

        服务器申请签名证书之前创建自己的csr文件

        > openssl req -new -key server.key -out server.csr
        

        申请签名,需要ca的证书和私钥参与,最后颁发一个带有ca签名的证书

        > openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt
        

        客户端发送请求前去获取服务器的证书,并通过ca的证书验证真伪

    • Https

      https就是工作在TLS/SSL上的Http

      需要私钥和签名证书

      // 创建https
      var https =require('https')
      var fs = require('fs')
      
      var options = {
          key: fs.readFileSync('./keys/server.key'),
          cert: fs.readFileSync('./keys/server.crt')
      }
      
      https.createServer(options, function(req, res) {
          res.writeHead(200)
          res.end('hello word\n')
      }).listen(8000)
      
      // https 客户端
      var https = require('https')
      var fs = require('fs')
      
      var options = {
          hostname: 'localhost',
          port: 8000,
          path: '/',
          method: 'GET',
          key: fs.readFileSync('./keys/client.key'),
          cert: fs.readFileSync('./keys/client.crt'),
          ca: [fs.readFileSync('./keys/ca.crt')]
      }
      options.agent = new https.Agent(options)
      
      var req = https.request(options, function(res) {
          res.setEncoding('utf-8')
          res.on('data', function(d) {
              console.log(d)
          })
      })
      req.end()
      req.on('error', function(e) {
          console.log(e)
      })
      

    构建web应用

    • 请求方法

      req.method

    • 路径解析

      req.url

      var pathname = url.parse(req.url).pathname

    • 查询字符串

      var url = require('url')
      var querystring = require('querystring')
      var query = querystring.parse(url.parse(req.url).query)
      // 更简洁实现
      // var query = url.parse(req.url, true).query
      // {foo: 'bar', baz: 'val'}
      // 如果键值出现多次,会解析为数组
      // foo=bar&foo=baz
      // {foo: ['bar', 'baz']}
      
    • Cookie

      http是无状态协议,cookie用来保存状态

      服务器生成cookie,发送给浏览器,浏览器保存本地,每次发送请求,携带cookie

      // 生成cookie res.setHeader('Set-Cookie': '')
      Set-Cookie: name=value; Path=/;Expires=Sun,23-Apr-23 09:01:35 GMT; Domain=.domain.com;
      // 设置HttpOnly, 防止cookie篡改
      // 设置Secure 为true,只能在https中有效
      
      • Path: cookie影响到的路径

      • expires 和Max-age 设置过期时间,如果没设置,默认cookie只存活在会话器,关闭浏览器,cookie丢失

        expires UTC时间格式, 当服务器时间和浏览器时间不一致时,会有偏差

        max-age:多久过期

      Cookie 性能影响

      一旦cookie过多,造成请求头部体积过大,造成待宽的浪费

      设置domain,限定使用的域

      为静态文件使用不同的域名,不需要发送cookie,缺点是,多个域名就多一次dns查询,好在dns可以缓存

      前端可以通过document.cookie修改cookie

      目前的应用场景:广告和在线统计领域最为依赖cookie

    • Session

      解决cookie的缺点:1请求携带cookie,造成请求头过大,2.前后端都可以篡改cookie, 有安全隐患

      session保留在服务器端,客户端无法修改

      如何将每个客户和服务器中的数据一一对应起来

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

        服务器生成session,将sessionId放在cookie中,发送客户端

        session是有有效期设置的一般20分钟

        客户端每次发送请求携带sessionId,通过cookie发送

        服务端接收后校验,如果过期,则重新生成

      • 通过查询字符串来实现浏览器和服务器数据的对应

        检查请求的查询字符串,如果没有值,会先生成新的带值的url

        然后跳转,让客户端重新发起请求

      session与内存

      ​ 统一集中存储在redis中

      session与安全

      ​ 将口令进行签名

      • xss

        跨站脚本攻击

        反射型 诱导用户点击恶意链接,注入恶意脚本

        存储型

        在页面中注入恶意脚本代码,发送给服务器,其他用户访问时,接受到恶意脚本,产生安全隐患,比如在评论区注入脚本

        解决方法:

        • 开启浏览器csp(内容安全策略),本质是建立白名单,规定浏览器只能执行特定来源的代码

          通过Content-Security-Policyhttp来开启

        • cookie设置httpOnly

        • 输入输出字符进行转译过滤

    • 缓存

      get请求缓存

      检查本地文件是否有缓存

      强缓存

      • expires

        utc格式时间字符串

        缺陷:浏览器时间和服务器时间可能不一致

      • Cache-Control

        public/private/no-cache/no-store/max-age

      检查服务端文件是否有缓存

      协商缓存 发送请求,服务器检查请求头部是否有缓存标识,命中返回304

      • Last-modified / if-modified-since

        时间搓改动但内容未必修改

        时间搓只能到秒,更新频繁,无法生效

      • Etag / If-None-Match

        唯一标识符

        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.headers['if-none-match']
                if(hash == noneMatch) {
                    res.writeHead(304, 'not Modified')
                    res.end()
                }else{
                    res.setHeader('Etag', hash)
                    res.writeHead(200, 'OK')
                    res.end(file)
                }
            })
        }
        

        正常链接进入: 匹配强缓存,再匹配协商缓存

        页面刷新:跳过强缓存,匹配协商缓存

        强制刷新:跳过缓存策略

        勾选disable cache 或请求头中设置no-cache,跳过缓存策略

      清除缓存

      • Url

      • url中跟随版本号

      • url中跟随hash ,hash发生变化,发送新的请求,推荐

      http://url.com/?hash=adjfisdfsd
      
    • Basic认证

      当客户端和服务端进行请求时,允许通过用户名和密码实现的一种身份认证方式

      检查请求头中的Authorization字段,该字段由认证方式和加密值构成

      Authorization: Basic dXNlcjpwYXNz
      // dXNlcjpwYXNz 是由用户名和密码结合并base64编码的值
      

      如果首次访问网页,请求头中没有携带认证内容,那么浏览器会响应401未授权的状态码

      缺点:虽然经过base64加密传送,但是安全系数低

      优点:兼容性好,几乎所有的浏览器都支持

    • 数据上传

      contetn-length代表报文的长度

    • 表单数据

      Content-Type: application/x-www.form-urlencoded

    • 附件上传

      content-Type: multipart/form-data; boundary=AaBo3x

      boundary=AaBo3x指定每部分的分界符,AaBo3x是随机生成的一段字符串

    <form action='/upload' enctype='multipart/form-data'></form>
    
    • 数据上传与安全

      • 内存限制

        避免大体积数据上传,内存被占光

        限制上传内容的大小,超过限制,停止接受

        通过流式解析,将数据流导向磁盘中,node中只保留文件路径信息

      • csrf

        跨站请求伪造,利用已经登录的用户信息,发送恶意的请求,获取用户信息的一种攻击方式

        用户验证

        服务器refrere check

        token验证

    • 路由解析

      • 文件路径型

        • 静态文件
        • 动态文件
      • MVC 将业务逻辑按职责分离

        • 控制器(Controller),行为的集合
        • 模型(Model),数据相关的操作和封装
        • 视图(View),视图的渲染

        路由解析-》对应的控制器中的行为-》调用相关的模型,进行数据操作-》视图更新

      • RESTful

    • 中间件

      var middleware = function(req, res, next) {
          ...
          next()
      }
      
    • 异常处理

      由于异步方法中的异常不能直接捕获,需要通过next(err) 向外传出

    • 中间件与性能

      • 编写高效的中间件

      • 合理使用路由

        配置路径,app.use('/public', staticFile)

    页面渲染

    • 内容响应

      响应报头中的Content-*字段

      Content-Encoding: gzip

      Content-length: 22217

      Content-Type: text/javascript;charset=utf-8

      客户端接收这个报文后,通过gzip来解码报文体的内容,用长度校验内容是否正确,让后以字符集utf-8解码后内容插入到文档中

      • MIME

      • 附件下载

        客户端不用打开它,之需弹出下载,

        Content-Disposition: inline// 代表内容只需要即时查看; attachment//代码数据可以存为可下载的附件

        res.sendfile = function(filePath) {
            fs.stat(filepath, function(err, stat) {
                var 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)
            })
        }
        
      • 响应json

      • 响应跳转

        res.redirect = function(url) {
            res.setHeader('Location', url)
            res.writeHead(302)
            res.end('Redirect to' + url)
        }
        
    • 视图渲染

      将数据和模版文件结合,通过模版引擎渲染成最终的html页面

      早期的模版语言 jsp, asp, php

      破局者: Mustache, 以{{ }}为标志的一套模版语言

    //简易模版函数,主要是正则匹配
    var render = function(str, data) {
        var tpl = str.replace(/<%=(.*)&>/g, function(str, code) {
            return " '+ obj." + code + "+ '"
        })
        tpl = "var tpl = '" + tpl + "'\nreturn tpl;"
        // Function中tpl为模版, obj为参数
        var complied = new Function('obj', tpl)
        return complied(data)
    }
    

    ​ 1.with的应用

    var complie = function(str, data) {
        var tpl = str.replace(/<%(.*)%>/g, function(all, code) {
            return "'+" + code + "+ '"
        })
        tpl = "tpl = '" + tpl + "'"
        tpl = 'var tpl = "";\nwith(obj) {' + tpl + '}\nreturn top;'
        return new Function('obj', tpl)
    }
    

    ​ new Function ([arg1[, arg2 [,...argN]]], fnBody)

    ​ 2.模版安全

    ​ 转译函数

    var escape = function(html) {
        return String(html).replace(/&(?!\w+;)/g, '&amp;')
                            .replace(/</g, '&lt;')
                            .replace(/>/g, '&gt;')
                            .replace(/"/g, '&quot;').replace(/'/g, '&#039')
    }
    

    ​ 3.模版逻辑

    ​ 4. 集成文件系统, 引入缓存,避免多次重复编译

    ​ 5. 子模版 include

    • Bigpipe

      翻译为风笛, 是用于调用限流的

      用于将页面分割为多个部分,先向用户输出没有数据的布局,将每个部分逐步输出到前端,再最终渲染填充框架,完成整个页面渲染

      • 页面布局框架(无数据)
      • 后端持续性的数据输出
      • 前端渲染

    玩转进程

    ​ 进程: cpu资源分配的最小单位 (工厂)

    ​ 线程: cpu调度的最小单位 (工人)

    nodejs : v8引擎,单线程

    单线程的缺点:不能发挥多核cpu的优势,抛出的异常未被捕获处理,会造成进程退出,健壮性和稳定性低

    优点: 没有多线程上下文切换的问题,提高cpu的使用率,没有锁,线程同步问题

    服务模型的变迁

    • 石器时代: 同步,阻塞,已淘汰

    • 青铜时代: 复制进程

      通过进程的复制同时服务更多的请求和用户,每个连接需要一个进程服务

    • 白银时代: 多线程

      线程之间共享数据, 建立线程池,减少创建和销毁线程的开销

      多线程上下文切换问题,大并发量时,会暴露一些问题

    • 黄金时代: 单线程+事件驱动

      node/nginx

      解决高并发问题

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

    • 多进程架构

      解决单线程对多核cpu使用不足的问题,每个进程利用一个cpu

      node中提供了child_process /curster模块

      主从模式(Master-Worker模式):主进程和工作进程, 典型的分布式架构中用于处理并行业务的模式,主进程不负责具体的业务处理,复制调度和管理工作进程, 工程进程负责具体的业务处理

      • 创建子进程

        Child_process spawn()/exec()/fork()/execFile()

      • 进程间的通信

        主线程和工作线程之间通过

        onmessage()

        postMessage()

        子进程对象则通过send() 和message()事件

      • IPC通道

        进程间通信(ipc),让不同的进程之间通信

        node中实现ipc的事pipe技术

      • 句柄传递

        多个进程监听同一个端口,会报端口被占用的错误,

        现在采用的是主进程监听主端口(如80),主进程对外接收所有的网络请求,再将这些请求分别代理到不同端口的进程上

        通过代理,解决端口不能重复被监听,可以适当的负载均衡

        child.send(message, [sendHandle]) //第二个可选参数就是句柄

        句柄是一种可以用来标识资源的引用,内部包含了指向对象的文件描述符

        可以去掉代理这种方案,使主进程接收到socket请求后,将这个socket直接发送给工作进程

        var child = require('child_process').fork('child.js')
        var server = require('net').createServer()
        server.on('connection', function(socket) {
            socket.end('handle by parent\n')
        })
        server.listent(1337, function(){
            child.send('server', server) //将server传递给子进程
        })
        
        
        // 子进程
        process.on('message', function(m, server) {
            if(m === 'server') {
                server.on('connection', function(socket) {
                    socket.end('handle by children \n')
                })
            }
        })
        
        • 句柄的发送与还原
        • 端口共同监听
    • 集群稳定之路

      • 进程事件

        • send() /message()

        • error

        • exit 子进程退出时触发,正常退出,第一个参数为退出码,被kill()杀死,会得到第二个参数,代表杀死进程的信号

          process.exit(1)

          process.kill(process.pid, 'SIGTERM')

        • close 子进程的标准输入输出流中止时触发

      • 自动重启

        // master.js
        var fork = require('child_process').fork
        var cpus = require('os').cpus
        
        var server = require('net').createServer()
        server.listen(1337)
        
        var workers = {}
        var createWorker = function() {
            
            // 限量重启
            if(xxx){
                // 发送giveup事件,不再重启
                process.emit('giveup')
                return
            }
            
            var worker = fork(__dirname + '/worker.js')
            // 接收到自杀信号后,启动新的进程, 保持总是有新的工作进程存在,可以处理请求
            worker.on('message', function(message) {
                if(message.act === 'suicide') {
                    createWorker()
                }
            })
            // 退出时重新启动新的进程
            worker.on('exit', function() {
                delete workers[worker.pid]
                createWorker()
            })
            // 句柄转发
            worker.send('server', server)
            worker[worker.pid] = worker
            
        }
        
        for(var i=0;i<cpus.length; i++) {
            createWorker()
        }
        // 进程自己退出时, 让所有工作进程退出
        process.on('exit', function() {
            for(var pid in workers) {
                workers[pid].kill()
            }
        })
        
        // worker.js
        var http = require('http')
        var server = http.createServer(function(req, res) {
            res.writeHead(200, {'Content-Type': 'text/plain'})
            res.end('handled by child')
        })
        
        var worker
        process.on('message', function(m, tcp) {
            if(m === 'server') {
                worker = tcp
                worker.on('connection', function(socket) {
                    server.emit('connection', socket)
                })
            }
        })
        //报错处理
        process.on('uncaughtException', function(err) {
            // 记录日志
            logger.error(err)
            process.send({act: 'suicide'}) //向主进程发送‘自杀’信号
            // 停止接收新的连接
            worker.close(function() {
                process.exit(1) // 退出进程
            })
            
            // 5秒后退出进程
            setTimeout(function() {
                process.exit(1)
            }, 5000)
        })
        

    负载均衡

    多进程之间监听相同的端口,使用户请求能够分散到多个进程上进行处理,保证多个进程处理的工作量公平的策略就叫负载均衡

    将cpu资源都调用起来

    node默认提供的机制是采用操作系统的抢占式策略,就是闲置的进程对请求进行抢夺,谁抢到谁服务,它的繁忙由cpu和i/o构成,影响抢占的是cpu的繁忙度,有可能存在cpu空闲,但是i/o忙的情况,这样去抢占服务,会形成负载不均衡

    node v0.11提供了新的策略, Round-Robin(轮叫调度),由主进程接收请求服务,依次发给工作进程

    //启用round-robin
    cluster.schedulingPolicy = cluster.SCHED_RR
    // 不启用
    cluster.schedulingPolicy = cluster.SCHED_NONE
    

    状态共享

    • 第三方数据存储(数据库, 磁盘, redis等)

    • 主动通知, 进程通知

    cluster模块

    ​ 创建单机node集群

    //cluster.js
    var cluster = require('cluster')
    cluster.setupMaster({
        exec: 'worker.js'
    })
    var cpus = require('os').cpus()
    for(var i = 0; i< cpus.length; i++) {
        cluster.fork()
    }
    

    执行node cluster.js, 和上面用child_process创建子进程集群效果相同

    Cluster.setupMaster() /cluster.fork()创建子进程

    cluster原理

    • cluster模块是child_process 和net模块的组合应用
    • 内部隐式创建tcp服务

    cluster 事件

    • fork 复制一个工作进程后触发该事件
    • online
    • listening
    • disconnect 主进程和工作进程之间ipc通道断开后会触发
    • exit 进程退出
    • setup cluster.setupMaster()执行后触发

    测试

    • 单元测试

      代码规范:

      • 单一职责
      • 接口抽象
      • 层次分离

      单元测试介绍

      • 断言

        assert模块

        Should.js 断言库

        var assert = require('assert')
        assert.equal(Math.max(1, 100), 100) // 如果不满足期望,则会抛出AssertionError异常,整个程序会停止执行
        
        • ok() 判断结果是否为真

        • equal 是否相等

        • notEqual() 是否不相等

        • deepEqual() 是否深度相等

        • strictEqual() 是否严格相等

        • throws() 判断代码块是否抛出异常

        • doesNotThrow() 判断代码块是否没有抛出异常

        • ifError() 判断实际值是否为一个假值(null, undefined, 0, '', false)

    • 测试框架, 用来管理测试用咧和生成测试报告

      mocha

      测试风格

      • tdd(测试驱动开发)

      • bdd(行为驱动开发)

        describe("#indexOf()", function() {
            it('should return -1 when not present', function() {
                [1,2,3].indexOf(4).should.equal(-1);
            })
            it('shoudl return index when present', function(){
                [1,2,3].indexOf(1).should.equal(0);
                [1,2,3].indexOf(2).should.equal(1);
                [1,2,3].indexOf(3).should.equal(2);
            })
        })
        
    异步测试
    

    产品化

    项目工程化

    项目的组织能力

    • 目录结构

    • 构建工具

      合并,压缩文件,打包应用,编译模块等

      grunt,webpack

    • 编码规范

      文档式约定——靠自觉

      代码提交时强制检查——考工具

      jsLint/EsLint

    • 代码审查

      代码托管平台gitlab/github

      git拉取分支,完成编程,合并到主支

    • 部署流程

      开发-》审查-》合并-》部署

    • 性能

      • 动静分离

        node处理静态文件的能力不算突出,将图片,脚本样式等静态文件托管到nginx或cdn静态服务器上,node只处理动态请求即可

        单独部署到静态服务器的好

      • 启用缓存

        避免重复的请求和计算

        redis

      • 多进程架构

        利用多核cpu的优势,保障服务更健壮,持续化的服务

        cluster模块,child_process

        社区中提供了pm,forever,pm2模块

      • 读写分离

        针对数据库,读取速度远远高于写入速度,写入的时候会锁表,会影响读取速度

        将数据库读写分离,主从设计

      • 日志

        建立健全的排查和跟踪机制

        还原问题现场,定位问题

        分割日志

      • 监控报警

        日志监控

        响应时间

        进程监控

        磁盘监控

        内存监控

        cpu占用监控

        网络流量监控

        应用状态监控

        dns监控

      • 报警系统

        邮件报警/短信或电话报警

        稳定性

        • 多机器

          更多的硬件资源

        • 多机房

          解决地理位置带来的网络延迟问题,在容灾方面,机房之间互为备份

        • 容灾备份至少4台服务器来构建稳定的服务集群

    相关文章

      网友评论

        本文标题:深入浅出Nodejs之笔记

        本文链接:https://www.haomeiwen.com/subject/cqofgctx.html