浏览器原理

浏览器事件循环

浏览器的进程模型

何为进程

程序运行需要它自己的专属的内存空间 可以简单的把这块内存空间理解为进程
每个应用至少有一个进程,进程之间相互独立,即使通信 需要同意

图片1

何为线程

有了进程之后,就可以运行程序代码了
运行代码的 容器 称之为 线程
一个进程至少有一个线程 所以在进程开启后会自动创建一个线程来运行代码 该线程称之为主线程
如果程序需要同时执行多块代码,主线程就会开启更多的线程来执行代码,所以一个进程总可以包含多个线程

浏览器有哪些进程和线程

浏览器内部工作复杂,为了避免相互影响,当启动浏览器后会自动开启多个进程(如:浏览器进程、网络进程、渲染进程)

其中主要的线程有:

  • 浏览器进程
    主要负责界面显示、用户交互、子进程管理、浏览器进程内部会启动多个线程处理不同的任务
  • 网络进程
    负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务
  • 渲染进程
    渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、css、JS代码,默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同标签页之间的不相互影响

渲染主线程是如何工作的

渲染主线程是浏览器中最繁忙的线程,需要处理的任务包括但不限于

  • 解析 HTML、CSS
  • 计算样式,布局
  • 处理图层
  • 执行全局 JS 代码
  • 执行事件处理函数和一些回调函数

为什么渲染进程不使用多个线程来处理这些事情?

主线程执行的时候该 如何调度任务?

排队: 即 事件循环(event loop) 使用消息队列来处理(message queue)

  1. 在最开始的时候,渲染主线程会进入一个无限循环
  2. 每一次循环会检查消息队列中是否有任务存在,如果有就取出第一个任务执行,执行完一个后进入下一次循环如果没有则进入休眠状态
  3. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务,新任务会加消息队列的末尾,在添加新任务的时,如果主状态是休眠状态,则会将其唤醒以继续循环拿任务

整个过程被称之为 事件循环 (消息循环)

一些解释

异步

代码执行的过程中会遇到一些无法立即执行的任务 比如:

  • 计时完成后需要执行的任务 – setTimeoutsetInterval
  • 网络通信完成后需要执行的任务 – XHRFETCH
  • 用户操作后执行的任务 – addEventListener

如果让渲染主进程等待这些任务的时机到达,就会导致主线程长期处于阻塞状态从而导致浏览器卡死

渲染主线程承担着极其重要的工作,无论如何都不能阻塞

因此浏览器选择异步来解决这个问题

使用异步的方式,渲染主线程永不会阻塞

如何理解`JS`的异步?

JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个
而渲染主线程承担着很多的工作 比如 渲染html、css 执行js
如果采用同步的方式就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行
这样一来 一方面会导致繁忙的主线程效率低,另一方面导致页面无法更新,用户体验不好
所以浏览器采用异步的方式来避免,具体做法是当某些任务发送时,比如计算器、网络请求、事件监听、主线程将任务交给其他线程去处理。自身立即结束任务的执行,转而执行后面的代码,当其他线程完成时,将事先传递的回调函数包装成任务,加入消息队列的末尾排队,等待主线程调度执行
在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行

JS为何会阻碍渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Megumi</h1>
<button> CLICK ME</button>
<script>
const title = document.querySelector('h1');
const button = document.querySelector('button');
const delay = (duration) => {
const start = Date.now();
while (Date.now() - start < duration) {
// do nothing
}
}
button.addEventListener('click', event => {
title.innerHTML = 'EXPLOSION!';
delay(5000)
});
</script>
</body>
</html>

点击按钮会卡 5s 后才会重新绘制

任务优先级

任务没有优先级 在消息队列中先进先出

但是消息队列是有优先级的
w3c 的最新解释:

  • 每一个任务都有任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行
  • 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行 (microtask queue)

随着浏览器的复杂度提升,W3C不在使用宏队列的说法

在目前 chrom的实现中,至少包含下面的队列:

  • 延时队列:用于存放计时器到达过的回调任务,优先级 中
  • 交互队列:用于存放用户操作后产生的事件处理任务 优先级 高
  • 微队列:用户存放需要最快执行的任务,优先级 最高

添加任务到微队列的主要方式是使用 Promise MutationObserver

例如

1
2
// 立即把一个函数添加到微队列
Promise.resolve().then(函数)
`JAVASCRIPT`的事件循环

事件循环又叫消息循环,是浏览器渲染主线程的工作方式
Chrome的源码中,会开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可
过去简单的把消息队列分为宏任务和微任务 这种说法已经不能满足浏览器的复杂性 取而代之的是一种更加灵活多变的处理方式
根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列,不同任务队列有不同的优先级 再一次事件循环中,由浏览器自行决定那一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级 必须优先调度执行

`JS`中的计时器能做到精确计时吗?为什么?

不能,因为

  • 计算机硬件没有原子钟,无法做到精确计时
  • 操作系统的计时函数本身就会有偏差,JS 是调用的操作系统的函数
  • 按照 W3C 的标准,如果嵌套层级超过 5 层,则会有 4 毫秒的最少事件
  • 受事件循环的影响,计时器的回调函数只能在主线程空闲的时去运行

浏览器渲染原理

渲染时间点

当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列
在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程

整个渲染的流程分为多个阶段,分别是:
HTML 解析 ——> 样式计算 ——> 布局——> 分层——>绘制——>分块——>光栅化——>

每个阶段都有明确的输入和输出,上一个阶段的输出会成为下一个阶段的输入
这样,整个渲染流程就形成了一套组织严密的生产流水线

1. 解析 HTML - Parse HTML

Document Object Modal

CSS Object Modal

渲染的第一步是解析HTML 解析过程中遇到CSS解析遇到JS执行JS 为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和外部的 JS 文件

如果主线程解析到 link 位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML,这是因为下载和解析 CSS 的工作是在预解析线程中进行的,这就是 CSS 不会阻塞 HTML 解析的根本原因

如果主线程遇到 JS 时必须暂停一切的任务,等待下载执行完后才能继续 解析线程可以分担一点下载 JS 的任务

主线程遇到script位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停,这就是 JS 会阻塞 HTML 解析的根本原因
在第一步完成后 会得到 DOM 树和 CSSOM 树 浏览器的默认样式、内部样式、外部样式、行内样式会包含在 CSSOM 树中

2. 计算

主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它的最终样式,称之为 Computed Style

在这一过程中 很多预设值会变成绝对值 比如 RED 会变成 rgb(255,0,0) 相对单位会变成绝对单位 比如 em 会变成 px

这一步完成后会得到一棵带样式的 DOM 树

3. 布局

布局阶段会依次遍历 DOM 树的每一个节点,计算每一个节点的几何信息,例如节点的高度,相对包含块的位置

大部分的时候 DOM 树和布局树并非一一对应

比如display: none的节点没有几何信息 因此不会生成到布局树 又比如使用了伪元素选择器 虽然DOM树中不存在这些伪元素节点 但它们拥有几何信息 所以会生成到布局树中 还有匿名行盒 匿名快盒 等 会导致无法一一对应

4.分层

主线程会使用一套复杂的策略对整个布局树进行分层

分层的好处在于 将来某一个层改变后、仅会对该层进行后续处理 从而提高效率

滚动条 推叠上下文 transfrom opacity等样式都会影响分层的结果 可以通过will-change属性更大程序的影响分层结果

5.绘制

主线程会为每个层单独产生绘制指令集 用于描述这一层的内容该如何画出来
完成绘制之后,主线程将每个图层的绘制信息提交给合成线程,剩余的工作将由合成线程完成

6.分块

将每层分为多个小区域
分块的工作是交给多个线程同时进行的
合成线程首先对每个图层进行分块,将其划分为更多的小区域
会从线程池中拿取多个线程来完成分块工作

7.光栅化

合成线程会将信息交给 GPU 进程 以极高的速度完成光栅化
GPU 进程会开启多个线程来完成光栅化 并且优先处理靠近视口区域的块
光栅化的结果 就是一块一块的位图

8.画

合成线程拿到每个层、每个块的位图后、生成一个个指引(quad)信息
指引会标识出每个位图应该画到屏幕的那个位置以及会考虑到旋转 缩放等变形
变形发生在合成线程 与渲染主线程无关 这就是transform高效率的本质原因
合成线程会把 quad 提交给 GPU 进程 有 GPU 进程产生系统调用 提交给 GPU 硬件 最终完成屏幕的成像

什么是 reflow

reflow 的本质就是重新计算 layout 树
当进行了会影响布局树的操作后,需要重新计算布局树 会引发 layout
为了避免连续的多次操作导致布局树反复计算 浏览器会合并这些操作 当 JS 代码全部完成后在进行统一计算 所以改动属性造成的 reflow 是异步的
也同样因为如此 当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息
决定立即获取属性立即 reflow

什么是 repaint

repaint 的本质就是重新根据分层信息计算了绘制指令
当改动了可见样式后 就需要重新计算 会引发 repaint
由于元素的布局信息也属于可见样式 所以 reflow 一定会引发 repaint

浏览器存储

概念

现代浏览器中提供了多种的存储机制,比如常见的 localStorage、Session Storage、indexedDB、WebSQL、Cookies 等等
数据存储在浏览器中的使用场景有那些呢?比如

  • 浏览器存储中保持应用状态,如用户偏好、设置等的
  • 创建离线工作的任务
  • 缓存静态应用资源,比如 html、css、Js、img 等的
  • 保存上次的会话,比如记录登录状态,购物车内容等的

Web Storage

HTML5 引入了 Web Storage,这让浏览器存储和检索数据变得很容易,WebStorage 提供了两个 API 来获取纯字符串的键值对

  • localStorage: 用于存储持久数据,除非是手动删除或者无痕模式,否则数据会一直保持存在
  • sessionStorage: 用于存储临时会话数据,页面重写加载后仍然存在,选项卡关闭时数据丢失

存储特点

  • 浏览器中有相关的存储 API,同时可以去 listener 去监听 storage 事件
  • 存储限制为 5MB,同时只能存字符串,可以降对象等特殊数据采用 JSON 转换去保持
  • web worker 和 service work 无法访问,容易被 xss 攻击,不能存敏感信息,同步操作会存在阻塞

Cookie 主要用于身份验证和用户数据持久性,Cookie 与请求一起发送到服务器,并在响应时发送给客户端,每次请求都会与服务器交换,服务器可以使用 cookie 向客户端发送个性化的内容,可以在一段时间后自动使数据过期的方式
每个 HTTP 请求和响应都会发送 Cookie 数据,存储过多的 Cookie 数据会使 HTTP 请求变得更加冗长

  • 浏览器限制 cookie 的大小是 4kb,特定域允许的 cookie 的数量是 20 个,并且只能是字符串,操作同步,不能使用 web workers 来访问,通过全局的 window 对象访问
  • 常用于会话管理、个性化、以及跨网站限制用户行为
  • 会话 Cookie: 没有指定 Expires 或 Max-Age 等属性,关闭浏览器会被删除
  • 持久 Cookie: 指定了 Expires 或 Max-Age 属性,在浏览器关闭的时候不会过期

Domain

Domain 属性告诉浏览器允许那些主机访问 Cookie,如果未指定,则默认为设置 cookie 的同一主机,因此,当使用客户端的 js 去访问 cookie 的时,只能访问与 URL 域相同的 cookie,同样与 HTTP 请求的域共享相同域的 cookie 可以与请求头一起发送到服务器,可以设计顶域,方便获取 cookie

Path

属性指定访问 cookie 必须存在的请求 URL 中的路径。除了将 cookie 限制到域之外,还可以通过路径来限制它。 路径属性为 Path=/store 的 cookie 只能在路径 /store 及其子路径 /store/cart、/store/gadgets 等上访问

Expires/Max-size

属性用来设置 cookie 的过期时间。若设置其值为一个时间,那么当到达此时间后,cookie 就会失效。不设置的话默认值是 Session,意思是 cookie 会和 session 一起失效。当浏览器关闭(不是浏览器标签页) 后,cookie 就会失效。

Secure

具有 Secure 属性的 cookie 仅可以通过安全的 HTTPS 协议发送到服务器,而不会通过 HTTP 协议。这有助于通过使 cookie 无法通过不安全的连接访问来防止中间人攻击。

HTTPOnly

属性使 cookie 只能通过服务端访问。 因此,只有服务断可以通过响应头设置它们,然后浏览器会将它们与每个后续请求的头一起发送到服务器,并且它们将无法通过客户端 JavaScript 访问。

IndexedDB

indexedDB 提供了一个类似 NoSQL 的 key/value 数据库,可以存储大量结构化数据,甚至是文件和 blob,每个域至少有 1GB 的可用空间
key/value 数据库意味着存储的所有数据都必须分配给一个 key。它将 key 与 value 相关联,key 用作该值的唯一标识符,这意味着可以使用该 key 跟踪该值。如果应用需要不断获取数据,key/value 数据库使用非常高效且紧凑的索引结构来快速可靠地通过 key 定位值。使用该 key,不仅可以检索存储的值,还可以删除、更新和替换该值

相关术语

  • 数据库:一个域可以创建任意数量的 indexedDB 数据库,只有同一域内的页面才能访问数据库
  • object store:相关数据项的 key/value 存储,类似于数据库中的表
  • key:用于引用 object store 中每条记录(值)的唯一名称。它可以使用自动增量数字生成,也可以设置为记录中的任何唯一值
  • index:在 object store 中组织数据的另一种方式。搜索查询只能检查 key 或 index。
  • schema:object store、key 和 index 的定义。
  • version:分配给 schema 的版本号(整数)。 IndexedDB 提供自动版本控制,因此可以将数据库更新到最新 schema

特点

特点如下:

  • 可以将任何 JavaScript 类型的数据存储为键值对,例如对象(blob、文件)或数组等。
  • IndexedDB API 是异步的,不会在数据加载时停止页面的渲染。
  • 可以存储结构化数据,例如 Date、视频、图像对象等。
  • 支持数据库事务和版本控制。
  • 可以存储大量数据。
  • 可以在大量数据中快速定位/搜索数据。
  • 数据库是域专用的,因此任何其他站点都无法访问其他网站的 IndexedDB 存储,这也称为同源策略。

使用场景:

  • 存储用户生成的内容:例如表单,在填写表单的过程中,用户可以离开并稍后再回来完成表单,存储之后就不会丢失初始输入的数据。
  • 存储应用状态:当用户首次加载网站或应用时,可以使用 IndexedDB 存储这些初始状态。可以是登录身份验证、API 请求或呈现 UI 之前所需的任何其他状态。因此,当用户下次访问该站点时,加载速度会增加,因为应用已经存储了状态,这意味着它可以更快地呈现 UI。
  • 对于离线工作的应用:用户可以在应用离线时编辑和添加数据。当应用程序来连接时,IndexedDB 将处理并清空同步队列中的这些操作。

浏览器中的网络传输

网络

TCP/IP 网络模型 一种通用的网络协议

应用层 –> 传输层 –> 网络层 –> 网络应用层 –> 物理链路层

常见状态码

  • 1xx:属于提示信息,是协议处理中的中间状态(用到的少)
  • 2xx:表示服务器成功的处理了客户端的请求(预期情况)
    • 200ok:最常见的成功状态码,表示一切正常,如果是非HEAD请求,服务器返回的响应头都会有body数据
    • 204 No Content:表示成功状态码,与 200 相同但是没有 body 数据
    • 206 Partial Content:应用于 HTTP 分块下载或者是断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分
  • 3xx:表示客户端的请求资源发生了变动,需要客户端用新的 url 去重新发生请求获取资源,即是重定向
    • 301 Moved Permanently:永久重定向,说明请求的资源已经不存在了,需要用新的 url 去再次访问
    • 302 Found:表示临时重定向,说明请求的资源还在,但暂时需要改用新的 url 去访问
    • 304 Not Modified:不具有跳转的含义,表示资源未修改,去重定向已经缓存的文件,也称缓存重定向,用于缓存控制
  • 4xx:表示客户端发送的报文有误,服务器无法处理
    • 400 Bad Request:表示客户端的请求报文有错误,是一个笼统的错误处理
    • 403 Forbidden:表示服务器禁止访问资源 ,而不是客户端错误
    • 404 Not Found:表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端
  • 5xx:表示客户端请求报文正确,但是服务器处理的时候内部发生了错误,属于服务端的错误码
    • 500 Inter Server Error:与 400 一样是一个笼统的错误码,表示服务器发生了错误
    • 501 Not Implemented:表示客户端的请求还不支付,类似于即将开业的情况
    • 502 Bad Gateway:通常是服务器作为网关或者代理时返回的错误码
    • 503 Service Unavalible:服务器很忙暂时无法响应

HTTP 中常见字段

Host 字段:客户端请求用来指定服务器的域名(Domain + Port)

Content-length 字段:表面本次回应的数据长度

Content-length 可以解决 TCP 中的粘包问题
TCP 中的粘包问题产生:我们不能认为一个用户消息对应一个 TCP 报文,因为 tcp 在发送报文的过程的时候会经过自己内部的发送窗口,拥塞控制和缓冲区大小等等共同解决的,所以 TCP 是面向字节流的协议

解决粘包问题在 HTTP 中是有特殊字符作为边界(空格、换行、回车) 和 content-length 作为数据长度 换行符做 HTTP Header 的边界 && Content-Length 作为 HTTP Body 的边界 从而去解决这个问题

Connection 字段:常用于客户端要求服务器使用 HTTP 长连接机制,以便其他请求去复用 TCP 通道

HTTP 长连接的特点是:只要任意一端没有明确的提出断开连接,特 TCP 会一直保持连接状态

HTTP 中的 Keep-Alive 和 TCP 中的 keepalive

HTTP 中的:HTTP(用户态)中的长链接,避免建立连接和释放的开销,也为 HTTP 流水线技术提供了可实现的基础(客户端可以先一次性发送多个请求,而在发送的过程中不需等待服务器的回应,可以减少整体的响应时间),但是此时服务器的响应还是要按照顺序,这样就可能前一个请求太慢形成了阻塞也就是常说的队头阻塞问题,为了资源浪费也会设置一个 Keep-Alive-time 的字段去定时断开连接

TCP 中的:TCP 的保活机制,如果两端 TCP 一直没有数据交互,则达到触发 TCP 保活机制的条件,在内核中的 TCP 协议栈会发送探测报文去判断报告情况

Content-Type 字段(request && response):用于服务器返回的时候告诉客户端本次的数据格式,客户端请求的时候可以加上Accept表示自己应该接受什么类型

(响应体中可以规定浏览器的默认行为 指向这次的媒体类型(MIME) text/plain && text/html && text/javascript && …

Content-Ending 字段:字段说明数据压缩的方式和请求中的可以接受Accept-Ending对应

GET 和 POST

根据 RFC 规范来说:GET 是指从服务器获取指定资源,GET 的请求参数是写在 url 上的,必须是 ASCII,浏览器会对 URL 的长度有限制。POST 是指根据请求的负荷(body)对指定的资源做出处理,body 可以是任意格式的

幂等:意思是执行多次,结果是一样的

安全:请求方法不会破坏服务器上面的资源

根据 RFC 规范来看:GET 方法就是安全且幂等的,POST 是不安全且不幂等的

但是实际开发的过程中 GET 和可以实现增删改服务器上面的资源,POST 也可以用来查询数据,甚至 GET 可以带上 body,POST 也可以加上 URL 参数

HTTP 缓存技术

对于一些重复性的 HTTP 请求,比如每次请求到的数据是一样的,可以把这样的请求-响应的数据缓存在本地,避免 HTTP 请求的方法就是使用缓存,HTTP 中的缓存分成强制缓存协商缓存

强制缓存(权重高)

只要浏览器的缓存没过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器

强制缓存是利用到下面两个 HTTP 响应头部的字段信息去完成的,都用来表示资源在客户端的缓存有效期

  • Cache-Control:相对时间 && 也可以设置别的参数 如ctx.set('Cache-Control', 'max-age=3600, public') (权重高)
  • Expries: 是一个相对时间

流程:浏览器第一次访问服务器资源的时候,服务器会返回某个资源并且在 response 上面设置对应的响应字段,浏览器保存后,在次请求的时候通过请求资源的时间与 Cache-Control 中设置的过期时间大小,来计算出该资源是否过期,如果没有,则使用该缓存,服务器在次收到请求后会重新更新头部的Cache-Control

协商缓存

请求响应码是 304,服务端告知客户端是否可以使用缓存的方式叫协商缓存,即通过协商缓存结果来判断是否可以使用本地缓存

也可以基于两种形式去完成协商缓存的过程

  • RES: If-Modified-Since && REQ: Last-Modified:资源过期后,发现 reponse 中含有 Last-Modified 字段,则在次发起请求的时候带上 Last-Modified 发送给服务器,服务器去根据这项时间去跟对比返回
  • RES: If-None-Match && REQ: Etag: 资源过期后,同比较Etag 是否 相同, 一般是使用 hash 去给内容做一个唯一的 token 值去比较,权重高,如果不存在则比较时间

使用 Etag 的好处:监控的粒度更加深,可以精确到毫秒内,不会存在手动改写时间的情况

协商缓存字段只有在设置了 Cache-Control 强制缓存字段后,未命中强制缓存的时候,才能发起带有协商缓存的字段请求

HTTP 特性

HTTP

  • 简单:基本报文格式就是 header + body,头部信息也是:key-value 简单文本形式,易于理解
  • 灵活,易于扩展:HTTP 中的各种请求方法,URL,状态码,头字段等都没有被规定死,允许开发人员自定义
  • 应用广泛和跨平台
  • 无状态:服务器不会去记忆 HTTP 状态,不需要额外的资源去使用,减轻服务器的负担,但是没有记忆性,做到一些关联性操作会非常麻烦于是就引出了关于Cookie等技术
  • 明文传输,不安全,通信使用明文,不验证通信的双方,不能验证报文的完整性

HTTP/1.1 性能

基于 TCP/IP 协议,请求-应答的模式,即性能判断就在这两点上面

  • 长连接:早期的 HTTP1.0 是每发起一个请求都会建立 TCP 连接/断开 TCP 连接,消费资源比较大 1.1 提出长连接,没有一方 close 会一直保持
  • 管道网络传输:在同一个 TCP 连接中,客户端可以发起多个请求,可以减少整体的响应时间,但是服务器的响应必须按照请求的顺序,这样就造成了队头阻塞的问题
  • 队头阻塞

HTTPS

HTTP 是明文传输存在,窃听,篡改,冒充等风险

HTTPS 在 HTTP 和 TCP 之间增加了SSL/TLS协议,可以很好的解决上面 HTTP 的缺点

  • 信息加密:混合加密的方式来实现信息的机密性,比如 RSA 算法
  • 校验机制:摘要算法+数字签名 来实现完整性,为数据生成独一无二的指纹
  • 身份证书:将服务器公钥放入数字证书中,解决冒充的风险

HTTP/2

HTTP2 协议是基于 HTTPS 的

相比较 HTTP/1 有如下的更新

  • 头部压缩:HTTP 中会压缩头(Header),如果同时发送的请求头是一样的,协议栈会消除重复的部分,HPACK算法去打表
  • 二进制格式:采用二进制格式去实现,头信息和数据体都是二进制,统称为帧(frame),头信息帧和数据帧
  • 并发传输:引入 Stream 概念,多个 Stream 复用一条 TCP 的连接,针对不同的 HTTP 请求使用独一无二的 Stream 来区分,可以交错的发送和请求报文
  • 服务器主动推送资源:服务端和客户端双方都可以建立 Stream,使用数字的奇偶来区分

缺点:HTTP2 是基于 TCP 协议来传输的数据,TCP 是基于字节流,必须保证接受到的字节流是完整且连续的,这样才会从内核缓冲区中取数据去发送给 HTTP 应用,当前一个字节没有发送的时候,后面的数据只能放在缓冲区中,等待第一个字节数据到达才会发送给 HTTP 应用

HTTP/3

HTTP/1.1 中的管道虽然可以解决请求的问题,但是没有解决响应的队头阻塞问题。HTTP/2 虽然通过 Stream 多路复用解决了 HTTP 队头阻塞的问题,但是一旦发现丢包则会阻塞所有的 HTTP 请求,属于 TCP 层的队头阻塞

HTTP2 中的问题是 TCP 导致的队头阻塞,但是 TCP 的特点就是基于字节流,所以 HTTP3 就是将传输换成了 UDP,实现了基于 UDP 的格式方法机制

UDP 的 QUIC 协议:在 UDP 上面实现的伪 TCP + TLS + HTTP/2 的多路复用协议

  • 无队头阻塞:QUIC 协议也有类似的 Stream 的机制,保证传输的可靠性,但是多个 Stream 是没有依赖的相互独立,当某个流丢包的时候,只会阻塞这个流,并不会影响到其他的流
  • 更快的建立连接:因为 QUIC 内部包含 TLS 1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与 TLS 密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果
  • 连接迁移无感

HTTP 优化

  • 尽量避免发送 HTTP 请求:通过缓存技术(HTTP Cache)去完成缓存功能,包括强制缓存和协商缓存等

  • 在需要发送 HTTP 请求的时候考虑如何减少次数

    • 减少重定向请求次数:通过中间层服务器去完成这个定向请求的功能而不是去跟客户端交互
    • 合并请求:比如通用的配置生成一个 configs 的大请求,小的资源图去形成大的精灵图,减少 HTTP 次数的请求
    • 延迟发送请求:懒加载,按需请求等
  • 减少服务器的 HTTP 响应的数据大小:无损压缩和有损压缩

RPC

本质上就是一种基于传输层的一种通信方式,Remote Procedure Call 远程过程调用,调用方式,有很多封装好的 RPC 协议比如 GRPC 等的是在应用层的一种协议,一般是用来在集群内部的通信,调用远端的方法可以屏蔽一些网络的细节

与 HTTP 的区别:

  • 服务发现:HTTP 中通过 DNS 服务去解析背后的 IP 和端口,RPC 一般会有中间服务去保存服务器名和 IP 信息,Etcd、redis 等
  • 底层连接形式:RPC 会在底层建立一个连接池,在请求量大的时候,建立多条连接放在池内
  • 传输内容:定制程度高,不用考虑浏览的行为,效率性能高

WebSocket

TCP 连接的两端是在同一时间,双方都可以主动的想对方发送数据,即是所谓的全双工,然而 HTTP/1.1 是基于 TCP 的,同一段时间段内客户端和服务器只能有一方主动的发送消息即是半双工

因为最开始的网页设计就是看文本等场景,只需要请求-应答的模式即可以,但是现在场景越多,需求越多,这样基于 TCP 的新应用层协议 Websocket 即出来了

在早期做到服务器推送消息的时候:

HTTP 不断轮询: 在前端代码中不断地定时发 HTTP 请求到服务器,服务器收到请求后给客户端响应信息,伪服务器的推送手段,只是用户无感而已
这种缺点也是非常的明显,消耗带宽,满屏的 http 请求,边缘 case 等

HTTP 长轮询:将 HTTP 请求的超时时间设置的很大,在这段时间内服务器只要收到了请求的信息,就立马返回给客户端,如果超时则立马发起下一个请求

浏览器中怎么建立的 x 连接

为了在浏览器中兼容 HTTP 协议,浏览器在进行 TCP 三次握手后,都统一使用 HTTP 协议先进行一次通信

  • 如果是普通的 HTTP 请求,则继续使用 HTTP 协议进行交互

  • 如果是想建立 WebSocket 过程,则会在 HTTP 中携带一些特殊的 header 头信息,如下所示

    1
    2
    3
    Connection: Upgrade
    Upgrade: Websocket
    Sec-WebSocket-Key: Explosionaabbccdd32423=123\r\n

    header 头的意思是:浏览器想升级协议,并且想升级成 WebSocket,同时随机携带一个随机生成的 base64 码作为 key 发给服务器

    如果服务器支持 WebSocket 连接协议就会去走 WebSocket 的握手流程,同时根据客户端生成的 base64 码,根据公开的算法去生成对应的字符串返回同时变成 101 状态码

    1
    2
    3
    4
    HTTP/1.1 101 Switching Protocols\r\n
    Sec-WebSocket-Accept: acnasunafsvnpsirghpw\r\n
    Upgrade: WebSocket\r\n
    Connection: Upgrade\r\n

WebSocket 使用场景

完成的实现了 TCP 的全双工的能力,并且也解决了粘包问题,适用于服务端和客户端需要大量交互的场景

查看默认打开的折叠框
  • TCP 协议本身是全双工的,但是 HTTP 协议设计的是半双工,对大部分服务器要推送的场景使用 WebSocket 更合适
  • 基于简单的场景的比如登录等场景,可以使用长轮询或者定时轮询去实习服务器推送功能
  • 正因为各个浏览器都支持 HTTP 协 议,所以 WebSocket 会先利用 HTTP 协议加上一些特殊的 header 头进行握手升级操作,升级成功后就跟 HTTP 没有任何关系了

TCP

TCP 的基本格式

序列号:在建立连接计算机随机生成的随机数去做为初始值,通过 SYN 包发送给接收端的主机,每次增加数据字节数去累加判断,用来解决网络包乱序的问题

确认应答号:指下一次期望接受收到的数据序列号,用来解决丢包的问题

TCP 是一个工作在传输层的可靠数据传输的服务,能确保接受端接受到的网络包是无损坏、无间隔、非冗余、按序的

TCP 特点

TCP 是 面向连接可靠的基于字节流的传输层通信协议

  • 面向连接:一对一才能连接
  • 可靠的:无论网络链路出现了什么样的变化,TCP 都可以保证一个报文一定能到达接收端
  • 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个 TCP 报文,通过消息边界来确定,没接受到的等待,重复的丢弃

TCP 连接

用于保证可靠状态和流量传输控制维护的某些状态信息这些信息包括:Sokcet(IP + port)、序列号(解决乱序问题)、窗口大小(流量控制)

TCP 建立连接过程

TCP 是面相连接的协议,所以必须先建立连接,连接是通过三次握手来进行的,过程如下

  • 刚开始:client 和 server 端都处理 CLOSE 状态,server 主动监听端口,变成 LISTEN 状态
  • 客户端会初始化序列号(client_isn),将此序列号置于 TCP 首部的序列号字段中,同时把 SYN 的字段设置成 1,表示 SYN 报文,之后把这个 SYN 报文发送给服务端,表示发起连接请求,该报文并不包含应用层数据,发送后的客户端处于 SYN-SEND 状态
  • 服务端收到客户端的 SYN 后,首先也直接初始化一个随机数字作为 server_isn,放入序列号字段中,同时将确认答应号填成 client_isn + 1 把 SYN 和 ACK 的值都变成 1,发送回去,同时也不包含任何的应用层数据
  • 客户端接受到了服务端数据后,还要向服务端去发送一个确认消息,确认应答号是 srever_isn + 1,将 ACK 变成 1,之后服务器处于 ESTABLISHED 状态,这个时候可以携带应用层数据
  • 客户端处于 ESTABLISHED 状态

为什么需要三次握手

  • 阻止重复历史连接的初始化:在网络拥堵的情况下,客户端可以确认自己希望收到的应答号去进行对比确认
  • 同步双方的初始序列号:去除从重复数据,根据数据包的序列号按需接受,可以识别发送的数据包是否被接受
  • 避免浪费资源

TCP 断开连接的过程

TCP 的连接是通过四次挥手来完成

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置位 1 的报文,即是 FIN 报文,之后客户端进入 FIN_WAIT_1 的状态
  • 服务端接受到报文信息后就向客户端发送 ACK 应答报文,同时自己的状态进入 CLOSE_WAIT
  • 客户端接受到 ACK 报文后,进入 FIN_WAIT_2 的状态
  • 等地服务器处理完数据后,向客户端发送一个 FIN 报文,之后进入 LAST_ACK 状态
  • 客户端接受到 FIN 报文,回答一个 ACK 报文,同时自己的状态进入 TIME_WAIT
  • 服务器收到 ACK 报文后,进入 CLOSE 状态
  • 客户端经过 2MSL 后一段时间后,自动进入 CLOSE 状态(丢包重传的情况)

为什么需要四次挥手

  • 关闭连接时,客户端向服务端发送 FIN,只是代表了客户端不发送数据了,但是还是可以去接受数据
  • 服务端收到客户端的 FIN 时,回首先回复一个 ACK 的报文,而服务端可能还有数据需要处理和转发,等服务器不在发送数据后才会回复一个 FIN 报文向客户端表示同意关闭连接了

故服务端通常是要等待数据发送和处理完,所以服务端的 ACK 和 FIN 会分开发送即是四次挥手

服务器出现大量的 TIME_WAIT 状态有那些原因?

  • HTTP 没有使用长连接
  • HTTP 长连接超时
  • HTTP 长连接请求达到数量

TCP 功能

  • 重传机制
  • 滑动窗口
  • 流量控制
  • 拥塞控制

IP 层

IP 层是在网络层,主要作用是:实现主机与主机之间的通信,即是点对点(end to end)通信

网络层和数据链路层的区别和关系

MAC 的作用是实现直连的两个设备之间的通信,而 IP 层则负责在没有直连的两个网络之间进行通信传输

计算机中是依靠数据链路层和网络层去实现目标 IP 的最终通信,在网络传输的过程中,源 IP 和目标 IP 地址是不会变化的(除了 NAT 技术),只有源 MAC 和目标的 MAC 在不断地发生变化,不停的在网络中去寻找下一跳要发送的地方

在 TCP/IP 网络通信中,为了保证能正常的通信,每个设备都要配置正确的 IP 地址,以便寻址

IP 地址分类

IP 地址最初被划分成 ABCDE 五种类型,通过前几位来判断是处于的地方

在主机号中有两个 IP 是特殊的

  • 主机号全为 1:指定某个网络下的所有主机,用于广播,分本地广播和直接广播
  • 主机号为 0:指定某个网络

无分类地址 CIDR

由于 IP 分类存在一些缺点,比如同一个网络下没有地址层次,缺少地址的灵活性,大类中的数量不能很好的与现实网络匹配等

提出了无分类地址CIDR的概念 32 位的地址划分成了前面网络号,后面是主机号

怎么划分网络号和主机号呢?

变现形式有:a.b.c.d/x x 表示网络号 && 给 IP 地址和子网掩码去进行与运算得到网络号

为什么存在网络号?

方便路由寻址工作,在路由寻址的时候,先进行广播找到网络地址是否相同,如果相同则在同一个网段内,直接发送数据包到目的主机即可,用于路由控制

子网掩码的另一个作用:可以划分子网,将主机地址划分成:子网网络地址 & 子网主机地址

IP 地址和路由控制

IP 地址中的网络地址这一部分是用于进行路由控制

路由控制表中记录着网络地址和下一步应该发送至路由器的地址,在主机和路由器上都会有各自的路由器控制表

在发送 IP 包时,首先要确定 IP 包首部中的目标地址,再从路由控制表中找到与该地址具有相同网络地址的记录,根据该记录将 IP 包转发给相应的下一个路由器。如果路由控制表中存在多条相同网络地址的记录,就选择相同位数最多的网络地址,也就是最长匹配。

IP 协议相关技术

  • DNS: DNS 域名解析,递归或者循环去取数据,去完成查询到最终域名的 IP 地址,(从本地域名开始访问,根域不同的向下查询)
  • ARP: 通过 ARP 协议,可以根据 IP 地址去查询到对应的 MAC 地址,ARP 是借助 ARP 请求和 ARP 响应来完成功能的,通过广播发送 ARP 请求,设备去查看,在形成 ARP 响应,返回得到信息,操作系统中存在 arp 的 cache,反之有RARP 协议
  • DHCP: DHCP 去获取动态的 IP,全程通信使用 UDP
  • NAT: 生成私有 IP,用于学校,家庭等设备很多的地方,可以架设一个 NAT 池
  • ICMP: 互联网控制报文协议,确认 IP 包是否成功的送达目标地址,报告发送过程中 IP 包被废弃的原因和改善网络设施等

同源策略和跨域

同源策略

跨域问题其实就是浏览器的同源策略造成的

同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互,这是浏览器的一个用于隔离潜在恶意文件的重要安全机制

image-20230112102913056

同源策略: protocol(协议)、domain(域名)、port(端口) 三者必须一致

同源策略主要限制了下面方面

  • 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage、indexDB
  • 当前域下的 js 脚本不能操作其他域下的 DOM
  • 当前域下 ajax 无法发送跨域请求

同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者 script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。

如何解决跨域问题

CORS

CORS:跨域资源共享,使用额外的 HTTP 头来告诉浏览器,让运行在一个 origin(domain)上的 Web 应用被准许访问来自不同源服务器上的指定的资源,当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求

CORS 需要浏览器和服务器同时支持,整个 CORS 过程都是浏览器完成的,无需用户参与。因此实现 CORS 的关键就是服务器,只要服务器实现了 CORS 请求,就可以跨源通信了

浏览器将 CORS 分成简单请求复杂请求
简单请求不会触发 CORS 预检请求,若该请求满足一下两个条件

  • 请求方法是 HEAD、GET、POST 中的一种
  • 自定义的 HTTP 的头部信息不超过:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type: 只限于 application/x-www-form-urlencoded、multipart/form-data、text/plain

简单请求过程:
对于简单请求,浏览器会直接发出 CORS 请求,会在请求的头信息中增加一个 Origin 字段,字段用来说明是在哪个源,服务器会根据这个值来决定是否同意请求
在简单请求中,在服务器内,至少需要设置字段:Access-Control-Allow-Origin

复杂请求
复杂请求是对服务器有特殊要求,比如请求方法为 DELETE 或 PUT 等,复杂请求会在正式通信之前进行一次 HTTP 查询请求,称为预检请求
浏览器会询问服务器,当前所在的网页是否在服务器允许访问的范围内,以及可以使用哪些 HTTP 请求方式和头信息字段,只有得到肯定的回复,才会进行正式的 HTTP 请求,否则就会报错。
预检请求使用的请求方法是 OPTIONS,表示这个请求是来询问的。他的头信息中的关键字段是 origin,表示请求来自哪个源。除此之外,头信息中还包括两个字段:

  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法。
  • Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。
    服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有 Access-Control-Allow-Origin 这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。
1
2
3
4
5
Access-Control-Allow-Origin: https://blog.yueyun.com  // 允许跨域的源地址
Access-Control-Allow-Methods: GET, POST, PUT // 服务器支持的所有跨域请求的方法
Access-Control-Allow-Headers: X-Custom-Header // 服务器支持的所有头信息字段
Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
Access-Control-Max-Age: 1728000 // 用来指定本次预检请求的有效期,单位为秒

JSONP

jsonp 的原理就是利用<script>标签没有跨域限制,通过<script>标签 src 属性,发送带有 callback 参数的 GET 请求,服务端将接口返回数据拼凑到 callback 函数中,返回给浏览器,浏览器解析执行,从而前端拿到 callback 函数返回的数据。
原生 JS 代码:

1
2
3
4
5
6
7
8
9
10
11
<script>
const script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://blog.yueyun.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>

后端 nodejs 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
const queryString = require('queryString')
const http = require('http')
const server = http.createServer()
server.on('request', function (req, res) {
const params = queryString.parse(req.url.split('?')[1])
const fn = params.callback
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' })
res.write(fn + '(' + JSON.stringify(params) + ')')
res.end()
})
server.listen('8080')
console.log('Server is running at port 8080...')

JSONP 的缺点:

  • 具有局限性, 仅支持 get 方法
  • 不安全,可能会遭受 XSS 攻击

nginx 代理跨域

  1. 代理静态资源
  2. 服务端反向代理

nodejs 中间件代理跨域