深入浅出Nodejs之笔记

深入浅出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台服务器来构建稳定的服务集群

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容