前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入浅出NodeJS随记 (四)

深入浅出NodeJS随记 (四)

原创
作者头像
邱邱邱邱yf
发布2021-12-10 16:39:08
3710
发布2021-12-10 16:39:08
举报
文章被收录于专栏:邱邱邱邱yf的读书笔记

最近在研读书籍 深入浅出nodejs , 随手写下的一些笔记, 和大家分享~ 如有错误,欢迎指正~

构建Web应用

Node的出现,打破了前后端的壁垒,借助事件驱动和V8的高性能,成为服务端的佼佼者。接下来会展开描述Web应用在后端实现中的细节和原理

基础功能

  1. 请求方法的判断: 常见的为GET和POST,除此外还有HEAD,DELETE,PUT等(在RESTful类Web服务中,请求方法十分重要,决定了资源的操作行为)
  2. URL路径解析: 最常见的根据路径进行业务处理的应用是静态文件服务器,它会根据路径去查找磁盘中的文件,然后把它响应给客户端。另外还有是根据路径来选择控制器,它预设路径为控制器和行为的组合,无需额外配置路由信息。
  3. 查询字符串的解析 查询字符串位于路径后,这部分经常需要为业务逻辑所用。在业务调用产生之前,我们的中间件或者框架会将查询字符串转换,然后挂载在请求对象上供业务使用。
  4. Cookie的解析 HTTP是一个无状态的协议,现实中的业务是需要一定状态的,否则无法区分用户身份。Cookie就是最早的标识认证一个用户的方式。 Cookie的处理分为三步:
    1. 服务器向客户端发送Cookie
    2. 浏览器将Cookie保存
    3. 之后每次浏览器都会将Cookie发向服务器端

    Cookie的一些设置:

    1. Path: 表示这个Cookie影响到的路径,当访问路径不满足该匹配时,浏览器不会发送这个Cookie
    2. Expires和Max-age是用来告诉浏览器这个Cookie是何时过期的。Expires告知何时过期(时间点),Max-age告知多久后过期(时间段)
    3. HttpOnly: 告知浏览器不允许通过document.Cookie去更改它(其实设置了以后,js中这个Cookie直接不可见)
    4. Secure: true则在HTTP中无效,HTTPS中才有效。

    Cookie的性能影响: 由于Cookie的实现机制,一旦服务器向客户端发送了设置Cookie的意图,除非Cookie过期,否则客户端每次发送请求都会发送这些Cookie到服务器端,一旦设置的Cookie过多,将会导致报文头较大。大多数的Cookie并不需要每次都用上,因为这会造成带宽的部分浪费 最好不要把Cookie设置在根节点下,否则几乎所有子路径下的请求都会带上这个Cookie。 为静态组件使用不同的域名(静态文件几乎不关心状态,Cookie对它而言几乎无用)(但是可能会增加DNS查询)

  5. Session 通过Cookie可以实现状态的记录。但是Cookie并非完美,前文以及提及,体积过大就是一个显著的问题,还有可以被前后端进行修改。Cookie对于敏感数据的保护是无效的。为了解决这个问题,Session应运而生。Session的数据只保留在服务器端,客户端无法修改,这样数据的安全性就得到了一定的保障,数据也无需在协议中每次传递。一般基于Cookie来实现用户和数据的映射
  6. 缓存 传统客户端在安装后的使用过程中仅需要传输数据,Web应用还需要传输构成页面的组件(HTML, CSS, JS文件等)这部分内容在大多数场景下并不经常变更,却需要在每次的应用中传递,如果不进行处理,那么它将会造成不必要的带宽浪费。如果网络速度差,就需要更多的时间来打开页面,对于用户体验也会造成影响。因此节省不必要的传输对于用户和服务者来说都要好处。 如何让浏览器缓存我们的静态资源,这也是iyge需要由服务器与浏览器共同协作完成的事情。通常来说,POST、DELETE、PUT这类带行为性的请求操作都不做任何缓存,大多数缓存只应用在GET请求中。 简单来讲,本地没有文件时,浏览器必定会请求服务器的内容,并将这部分内容放在本地的某个缓存目录中。在第二次请求时,他将对本地文件进行检查,如果不能确定这份本地文件是否可以直接使用,他将会发起一次条件请求。所谓的条件请求,就是在普通的GET请求报文中,附带If-Modified-Since字段。它将询问服务器端是否有需要更新的版本,本地文件的最后修改时间。如果服务器没有新的版本只需要响应一个304的状态码,客户端就使用本地版本,否则就返回新的版本,客户端会放弃本地版本。 条件请求采用的是时间戳的方式,但是有一定的缺陷: 时间,戳改动文件内容不一定改动,且更新频繁的内容不一定生效。为了解决这个问题,引入了ETag(Entity Tag). ETag由服务器生成,服务器端可以决定他的生成规则,一般是根据文件内容生成散列值(If-None-Match/ETag)。 但是以上的两种方法依然会发起一个HTTP请求,使得客户端依然会花一定时间来等待响应。可见最好的方法就是连条件请求都不用发起。可以设置Expires或Cache-Control(能够避免浏览器和服务端时间不一致带来的问题)头。Max-age会覆盖Expires

数据上传

头部报文中的内容已经能够满足大多数的业务逻辑操作了,但是单纯的头部报文无法携带大量的数据,在业务中,我们往往需要接受一些数据,比如表单提交,文件提交,JSON上传,XML上传等。

Node中的http模块只对HTTP报文的头部进行了解析,然后触发request事件。如果请求中还带有部分内容(如POST具有报文和内容),内容部分需要用户自行接收和解析。通过报头的Transfer-Encoding或Content-Length即可判断请求中是否带有内容。在HTTP_Parser解析报文头部接受以后,报文内容会通过data事件触发,我们只需要以流的方式处理即可。(将接收到的buffer列表转化为一个Buffer对象后,再转换为没有乱码的字符串,挂载在req上)

表单数据,Content-type为application/x-www-urlencoded, 报文体内容跟查询字符串相同

JSON文件, application/json(再用JSON.parse解析)

xml文件,applicaton/xml (使用第三方库解析)

附件上传, multipart/form-data, 还有一个boundary分界符。

路径解析

  1. 文件路径型
    1. 静态文件(常见)
    2. 动态文件(比较少见)
  2. MVC 主要思想是将业务逻辑按职责分离,分为Controller(一组行为的集合), Model(数据相关的操作和封装), View(视图的渲染) 工作模式: 路由解析,根据URL寻找对应的控制器和行为。行为调用相关的模型,进行数据操作。数据操作结束后,调用视图和相关数据进行页面渲染,输出到客户端。
  3. RESTful Reprensentational State Transfer 表现层状态转换。主要是将服务器端提供的内容看作一个资源,并体现在HTTP方法上。

中间件

引入中间件来简化和隔离基础设施(例如查询字符串的解析,cookie的解析等)和业务逻辑之间的细节。让开发者关注在业务的开发上,以达到提高开发效率的目的。中间件的行为比较类似过滤器的行为,就是在进入具体业务逻辑之前,先让过滤器处理。每个中间件处理相对简单的逻辑,最终汇成强大的基础框架。

中间件的上下文也是请求对象和响应对象:req和res。由于Node异步的原因,我们需要提供一种机制,在当前中间件处理完成后通知下一个中间件执行。采用尾触发的方式实现。

代码语言:javascript
复制
const middleware = function(req, res, next) {
    //TODO
    next()
}

中间件异常处理:由于异步方法的异常不能直接捕获,中间件异步产生的异常需要自己抛出来( next(err)的方式 )。可以添加异常处理的中间件,多加一个参数(err, req, res, next)

中间件与性能:

  1. 编写高效的中间件:提升单个处理单元的处理速度,尽早调用next。(缓存重复计算结果,避免不必要的计算等)
  2. 合理使用路由(例如: 文件路径型的静态文件查询,加上/public前缀,免得 / 的所有请求都通过该中间件)

页面渲染

  1. 内容响应: 利用Content-*的字段。(Content-Type的MIME在其中十分重要,Content-Disposiition可以用来设定数据是当做即使浏览的内容还是附件下载)
  2. 视图渲染:一般是模板+数据渲染出来的
  3. 模板: 其实就是字符串拼接(语法分解,提出出普通字符串和表达式,处理表达式,生成待执行的语句,与数据一起执行,生成最终的字符串)(一般都会用到new Function, with等语法,不太建议使用)最后为了避免xss漏洞,必须要把能形成HTML标签的字符串转义。 另外,可以简单了解一下Bigpipe

玩转进程

Node选型V8,意味着他的模型和浏览器相似。JS运行在单进程的单线程上,好处是:程序状态单一,在没有线程的情况下没有锁、线程同步问题,操作系统在调度时也因为较少上下文的切换,可以很好的提高CPU的使用率。但是单进程单线程并非完美结构,如今CPU基本都是多核的,一个Node进程只能利用一个核,这将抛出Node实际应用的第一个问题:如何处分利用多核CPU服务器。另外,由于Node执行在单线程长,一旦异常没有被捕获,将会引起整个进程的奔溃。这就带来了第二个问题:如何保证进程的健壮性和稳定性。(严格意义上说,Node不是真正的单线程架构,存在一定的I/O线程存在)

服务模型的变迁

石器时代: 执行模型是同步的,一次只为一个请求服务,所有请求按次序等待服务。假设每次响应服务耗用N秒,QPS(query per second)为1/N.

青铜时代:复制进程。为了解决同步架构的并发问题,简单的改进是通过进程的复制同时服务更多的请求和用户。每个请求都需要一个进程来服务,代价比较昂贵,相同状态会存在很多份,比较浪费。假设进程上限为M,QPS为 M/N

白银时代:多线程。为了解决多进程的浪费问题,多线程被引入,让一个线程服务一个请求。线程相对进程开销要小得多,并且线程之间可以共享数据。但是多线程的每个线程还是需要自己独立的堆栈,所以也只是比多进程略好。另外由于一个CPU核心在一个时刻只能做一件事,操作系统只能将CPU切片,让线程可以较为均匀地使用资源,但是切换线程也需要切换线程上下文。所以在大量并发时,还是无法做到强大的伸缩性。假设线程资源占用为进程的1/L,QPS为 M*L/N

黄金时代: 事件驱动。Apache是采用多线程/多进程模型,当并发数增长到上万时,内存消耗的问题就是暴露出来。为了解决高并发的问题,基于事件驱动的模型出现了,像Node和Nginx均是基于事件驱动的方式实现的,采用单线程避免了不必要的内存开销和上下文切换。问题就是:CPU利用率以及健壮性。

多进程架构

面对单进程单线程对于多核CPU利用不足的问题,就是启动多进程即可。理想状态下每个进程各自利用一个CPU,以此实现多核CPU的利用,Node提供了child_process模块,并且提供了child_process.fork()供我们实现进程的复制。

Master-Worker模式。进程分为两种:主进程和工作进程。主进程不服务具体的业务处理,而是负责调度或者管理工作进程。工作进程负责具体的业务处理。但是fork进程代价还是很昂贵的。我们只是为了充分利用多核CPU,不是为解决并发问题。

进程间通信

在Master-Worker模式中,要实现主进程管理和调度工作进程的功能,需要主进程和工作进程之间的通信。线程之间通过send方法发送数据,message事件实时监听收到的数据。通过fork创建子进程之后,为了实现父子进程之间的通信,父进程与子进程之间将会创建IPC(Inter-Process Communication)通道,父子进程之间才能通过message和send传递消息。

父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正创建子进程,并通过环境变量告诉子进程这个IPC通道的文件描述符。子进程在启动的过程中,根据文件描述符去连接这个已经存在的IPC通道,从而完成父子进程之间的连接。是一个双向通信,在系统内核中就完成了进程间通信,不需要进过实际的网络层。(只有启动的子进程是Node进程,子进程才会去连接IPC通道)

句柄传递: 句柄是一种可以用来表示资源的引用,它的内部包含了指向对象的文件描述符。比如句柄可以用来表示一个服务端socket对象,一个客户端socket对象,一个UDP套接字,一个管道等。发送句柄意味着,在主进程收到socket请求后,将这个socket直接发送给工作进程(或者说直接将一个TCP服务器发送给子进程)(并不意味着可以发送任意对象,只是消息传递,不是真正地传递对象,其中涉及的底层细节不赘述)。

端口共同监听: 依靠句柄传递实现了多个进程可以监听相同的端口而不引起异常。(对于sned发送的句柄还原出来的服务而言,他们的文件描述符是相同的,所以监听相同端口不会报错,但是文件描述符同一时间只能被某个进程使用,这些进程服务是抢占式的)

负载均衡:

在多进程之间监听相同的端口,使得用户请求能够分散到多个进程上进行处理,者带来的好处是可以将CPU资源利用起来。但是不能让一个CPU忙不过来也不能让别的CPU闲着,这种保证多个处理单元工作量公平的策略叫做负载均衡。Node默认提供的机制是采用操作系统抢占式策略。一般而言这种抢占式对大家公平的,各个进程可以根据自己的繁忙度来进行抢占。但是对于Node而言,需要分清的是他的繁忙是由CPU、I/O两部分构成的,影响抢占的是CPU的繁忙度。对于不同业务可能存在I/O繁忙,而CPU空闲的情况,从而形成负载不均衡的情况。为此Node在V0.11中提供了一种新的策略使得负载均衡更加合理,这种新策略叫做Round-Robin,又叫轮叫调度。轮叫调度的工作方式是由主进程接受连接,将其依次分发给工作进程。

Cluster模块

child_process模块比较偏向底层,后来Node提供了cluster模块,用以解决多核CPU的利用率问题,同时也提供了较完善的API,用以处理进程的健壮性问题。

代码语言:javascript
复制
cluster.isMaster
#如果该进程是主进程,则为 true。 这是由 process.env.NODE_UNIQUE_ID 决定的。 如果 process.env.NODE_UNIQUE_ID 未定义,则 isMaster 为 true。
​
cluster.isWorker
#如果该进程不是主进程,则为 true(与 cluster.isMaster 相反)。
​
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/2; i++) {
    cluster.fork();
  }
  cluster.on('online', ()=> {
    console.log('一个子进程出现了');
  })
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是 HTTP 服务器。
  http.createServer((req, res) => {
    console.log(`${process.pid}工作`)
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8888);
​
  console.log(`工作进程 ${process.pid} 已启动`);
}
Cluster的工作原理

事实上cluster就是child_process和net模块的组合应用。cluster启动时,如同我们之前说的,他会在内部启动TCP服务器,在cluster.fork()子进程时,将这个TCP服务器端socket的文件描述符发送给工作进程。如果进程时fork复制出来的,那么它的环境变量里就存在NODE_UNIQUE_ID, 如果工作进程中存在listen()监听端口的调用,它就将拿到该文件描述符,将端口重用,从而实现多个子进程共享端口。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 构建Web应用
    • 基础功能
      • 数据上传
        • 路径解析
          • 中间件
            • 页面渲染
            • 玩转进程
              • 服务模型的变迁
                • 多进程架构
                  • 进程间通信
                • 负载均衡:
                  • Cluster模块
                    • Cluster的工作原理
                相关产品与服务
                消息队列 TDMQ
                消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档