从宏观维度

  1. 浏览器进程主要负责用户交互、子进程管理和文件存储等功能。
  2. 网络进程是面向浏览器进程和渲染进程等提供网络下载功能。
  3. 渲染进程的主要职责是把从网络上下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。(运行在渲染进程里的代码是不被信任的=>其内容是通过网络下载,会存在恶意代码利用浏览器漏洞对系统进行攻击)

各进程配合流程图

上图展示了从用户输入 URL 后浏览器各进程之间的协作过程。

浏览器是多进程的

  • 浏览器主进程:负责管理渲染进程、网络进程等子进程
  • 第三方插件进程:
  • GPU 进程
  • 渲染进程

URL 是什么

URL(Uniform Resource Locator)
http://www.hhizz.cn:80/file?name=admin#head1

URL组成:协议(scheme) 主机(host) 域名(domain) 端口(port) 路径(path) 查询参数(query) 锚点(frag)

输入 URL 后,浏览器会解析出协议、主机、端口、路径等信息,并构造一个 HTTP 请求

DNS 解析:将域名解析成 IP 地址

通过域名,最终得到该域名对应的 IP 地址的过程叫做域名解析(或主机名解析)

1
www.hhizz.com (域名)  - DNS解析 -> 11.222.33.444 (IP地址)

在浏览器输入网址之后,首先要经过域名解析,因为浏览器并不能通过域名找到对应的服务器,只能通过 IP 地址。DNS 域名解析分为递归查询和迭代查询两种方式,DNS 客户端和本地域名服务器之间是递归,而本地域名服务器和其他名称服务器之间是迭代。

递归查询

本机向 LDNS 查询一般都是采用递归查询:如果本机所询问的本地域名服务器不知道查询的 IP 地址,那么本地域名服务器就以 DNS 客户的身份,向其他根域名服务器继续发出查询请求报文,而不是让该主机自己进行下一步查询

迭代查询

本地域名服务器向根域名服务器采用迭代查询,根域名服务器告诉本地域名服务器你下一步应当向哪个域名服务器查询。然后本地域名服务器进行后续查询,根域名服务器通常把自己知道的顶级域名服务器告诉本地域名服务器,本地域名服务器再去顶级域名服务器查询。

DNS 缓存

有 dns 的地方,就有缓存。浏览器、操作系统、路由器、ISP、根域名服务器、顶级域名服务器、主域名服务器缓存,它们都会对 DNS 结果做一定程度的缓存。

浏览器缓存

浏览器缓存

浏览器缓存其实就是浏览器保存通过 HTTP 获取的所有资源,是浏览器将网络资源存储在本地

缓存的资源去哪里了?

Memory Cache

Memory Cache 顾名思义,就是将资源缓存到内存中,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。(MemoryCache 在退出进程时数据会被清除,一旦关闭 Tab 页面,内存中的缓存也就被释放了)

Disk Cache

Disk Cache 顾名思义,就是将资源缓存到磁盘中,读取速度稍慢,但是什么都能存储到磁盘中,比 Memory Cache 胜在容量存储时效性上。(DiskCache 在退出进程时数据不会被清除)

Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。要使用 Service Worker传输协议必须为 HTTPS,因为 Service Worker涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全

Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker中获取的内容

Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在 Chrome 浏览器中只有 5 分钟左右,同时它也并非严格执行 HTTP 头中的缓存指令。

  1. 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差
  2. 可以推送 no-cache 和 no-store 的资源
  3. 一旦连接被关闭,Push Cache 就被释放
  4. 多个页面可以使用同一个 HTTP/2 的连接,也就可以使用同一个 Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的 tab 标签使用同一个 HTTP 连接。
  5. Push Cache 中的缓存只能被使用一次
  6. 浏览器可以拒绝接受已经存在的资源推送
  7. 可以给其他域名推送资源
三级缓存原理 (访问缓存优先级)

1 先在内存中查找,如果有,直接加载。
2 如果内存中不存在,则在硬盘中查找,如果有直接加载。
3 如果硬盘中也没有,那么就进行网络请求
4 请求获取的资源缓存到硬盘和内存。

强缓存

浏览器在加载资源时,会先根据本地缓存资源的 header 中的信息判断是否命中强缓存,如果命中则直接使用缓存中的资源不会再向服务器发送请求。(200 from memory/disk cache) (这里的 header 中的信息指的是 Expires 和 Cache-Control)

如果没有命中强缓存规则,则进入下一步关于协商缓存的处理。

HTTP 1.0

服务器使用的响应头字段为 Expires ,值为未来的绝对时间(时间戳),浏览器请求时的当前时间超过了 Expires 设置的时间,代表缓存失效,需要再次向服务器发送请求,否则都会直接从缓存数据库中获取数据。

来源:存在于服务端返回的响应头中
语法:Expires: Wed, 22 Nov 2020 08:41:00 GMT
缺点:服务器的时间和浏览器的时间可能并不一致导致失效

HTTP 1.1

Cache-Control 是 HTTP 1.1 时出现的 header 信息,主要是利用该字段的 max-age 值来进行判断,它是一个相对时间,例如 Cache-Control:max-age=3600,代表着资源的有效期是 3600 秒。Cache-Control 除了该字段外,还有下面几个比较常用的设置值:

请求头:
no-cache:告知(代理)服务器不直接使用缓存,要求向原服务器发起请求

no-store:所有内容都不会被保存到缓存或 Internet 临时文件中

max-age=delta-seconds:告知服务器客户端希望接收一个存在时间不大于 delta-secconds 秒的资源

max-stale[=delta-seconds]:告知(代理)服务器客户端愿意接收一个超过缓存时间的资源,若有定义 delta-seconds 则为 delta-seconds 秒,若没有则为任意超出时间

min-fresh=delta-seconds:告知(代理)服务器客户端希望接收一个在小于 delta-seconds 秒内被更新过的资源

no-transform:告知(代理)服务器客户端希望获取实体数据没有被转换(比如压缩)过的资源

noly-if-cached:告知(代理)服务器客户端希望获取缓存的内容(若有),而不用向原服务器发去请求

cache-extension:自定义扩展值,若服务器不识别该值将被忽略掉

响应头:

public:表明任何情况下都得缓存该资源(即使是需要 HTTP 认证的资源)

Private=[field-name]:表明返回报文中全部或部分(若指定了 field-name 则为 field-name 的字段数据)仅开放给某些用户(服务器指定的 share-user,如代理服务器 CDN 中继缓存服务器)做缓存使用,其他用户则不能缓存这些数据

no-cache:不直接使用缓存,要求向服务器发起(新鲜度校验)请求

no-store:所以内容都不会被保存到缓存或 Internet 临时文件中

no-transform:告知客户端缓存文件时不得对实体数据做任何改变

noly-if-cached:告知(代理)服务器客户端希望获取缓存的内容(若有),而不用向原服务器发去请求

must-revalidate:当前资源一定是向原方法服务器发去验证请求的,如请求是吧会返回 504(而非代理服务器上的缓存)

proxy-revalidate:与must-revalidate类似,但仅能应用于共享缓存(如代理)

max-age=delta-seconds:告知客户端该资源在 delta-seconds 秒内是新鲜的,无需向服务器发请求

s-maxage=delta-seconds:同max-age,但仅能应用于共享缓存(如代理)

cache-extension:自定义扩展值,若服务器不识别该值将被忽略掉

Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高

协商缓存

如果没有命中强缓存规则,浏览器会发送请求,服务器根据请求头的 If-Modified-Since If-None-Match 判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。

浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回 304 状态码,通知客户端比较成功,可以使用缓存数据。

HTTP 1.0

If-Modified-Since(请求头)/Last-Modified(响应头)
在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。
浏览器接收到后,如果再次请求,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间。

服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间Last-Modified对比,确认在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。没更新的话,只返回头部,并不返回资源实体内容,通知浏览器可以使用本地缓存。

但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag。

HTTP 1.1

If-None-Match(请求头)/E-tag (响应头)

ETag是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值给浏览器。浏览器接收到ETag的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。

浏览器缓存流程图

TCP 连接:TCP 三次握手

TCP三次握手

客服端和服务端在进行 http 请求和返回的工程中,需要创建一个 TCP connection(由客户端发起),http 不存在连接这个概念,它只有请求和响应。请求和响应都是数据包,它们之间的传输通道就是 TCP connection。

第一次握手:客户端发送位码为SYN=1,随机产生Seq=X的数据包到服务器,服务器由SYN=1知道,客户端要求建立连接;(第一次握手,由浏览器发起,告诉服务器我要发送请求了)
第二次握手:服务器收到请求后要确认连接信息,向客户端发送SYN=1,ACK=(客户端发送的数据包 Seq + 1),随机产生Seq=Y的包;(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)
第三次握手:客户端收到后检查ACK number是否正确,即第一次发送的Seq number+1,以及位码 SYN 是否为 1,若正确,客户端会再发送ACK number=(服务器发送的数据包Seq + 1)和随机产生Seq=X的包,服务器收到后确认 Seq 值与 ACK=(服务端发送的数据包 Seq + 1)则连接建立成功;(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)

第三次握手的必要性

其实这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客服端的发送能力,第二次握手,服务端 SYN=1,Seq=Y 就确认了发送能力,ACK=X+1 就确认了接收能力,所以第三次握手才可以确认客户端接收能力。不然容易出现丢包的现象。

如果是用两次握手,则会出现下面这种情况:
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。

什么是半连接队列?

服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。

当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

这里在补充一点关于 SYN-ACK 重传次数的问题: 服务器发送完 SYN-ACK 包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。

每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s…

三次握手过程中可以携带数据吗?

其实第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据。
至于为什么这样,
假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。

SYN 攻击

服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到 SYN 洪泛攻击。SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向 Server 不断地发送 SYN 包,Server 则回复确认包,并等待 Client 确认,由于源地址不存在,因此 Server 需要不断重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。
检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。

常见的防御 SYN 攻击的方法有如下几种:

  • 缩短超时(SYN Timeout)时间
  • 增加最大半连接数
  • 过滤网关防护
  • SYN cookies 技术

发送 HTTP 请求

HTTP 版本

HTTP/1.0

  • 定义了三种请求方法:GET POST HEAD
  • 增加 status code 和 header
  • 多字符集支持、多部分发送、权限、缓存等
  • 响应:不再只限于超文本 (Content-Type 头部提供了传输 HTML 之外文件的能力 — 如脚本、样式或媒体文件)

HTTP/1.1

  • 持久链接。TCP 三次握手会在任何连接被建立之前发生一次。最终,当发送了所有数据之后,服务器发送一个消息,表示不会再有更多数据向客户端发送了,则客户端才会关闭连接(断开 TCP)
  • 新增5个方法: PUT DELETE TRACE OPTIONS CONNECT
  • 进行了重大的性能优化和特性增强:分块传输压缩/解压内容缓存磋商虚拟主机(有单个IP地址的主机具有多个域名)更快的响应通过增加缓存节省更多的带宽

HTTP2

  • 所有数据以二进制传输。HTTP1.x 是基于文本的,无法保证健壮性,HTTP2.0 绝对使用新的二进制格式,方便且健壮
  • 同一个连接里面发送多个请求不再需要按照顺序来
  • 头信息压缩以及推送等提高效率的功能

HTTP3
HTTP3 的主要改进在传输层上。传输层不会再有繁重的 TCP 连接了,一切都会走 UDP。

HTTP 特点

  • 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  • 可扩展:在 HTTP/1.0 中出现的 HTTP headers 让协议扩展变得非常容易。只要服务端和客户端就新 headers 达成语义一致,新功能就可以被轻松加入进来。
  • 无状态:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

HTTP 消息结构

请求报文

请求报文

请求行(方法、URI、协议版本)
请求头部
空行
请求数据

响应报文

响应报文

状态行(协议版本 状态码 状态码的原因短语)
响应头部
空行
响应正文

HTTPS

在HTTP的基础上再加一层TLS(传输层安全性协议)或者SSL(安全套接层),就构成了HTTPS协议。
HTTPS 默认工作在 TCP 协议443端口,它的工作流程一般如以下方式:

  • TCP 三次同步握手
  • 客户端验证服务器数字证书
  • DH 算法协商对称加密算法的密钥、hash 算法的密钥
  • SSL 安全加密隧道协商完成
  • 网页以加密的方式传输,用协商的对称加密算法和密钥加密,保证数据机密性;用协商的hash算法进行数据完整性保护,保证数据不被篡改。

    TLS

TLS 握手的关键在于利用通信双方生成的随机字符串和服务端的证书公钥生成一个双方经过协商后的对称密钥,这样通信双方就可以使用这个对称密钥在后续的数据传输中加密消息数据,防止中间人的监听和攻击,保证通讯安全。

HTTPS连接 需要7次握手,3次TCP + 4次TLS。

服务器处理请求并返回 HTTP 报文

每台服务器上都会安装处理请求的应用——Web server。常见的web server产品有Apache、Nginx、IIS、Lighttpd等。

请求的资源分为静态资源动态资源
请求访问静态资源,直接根据url地址去服务器里找就好了。
请求动态资源的话,就需要Web server把不同请求,委托给服务器上处理相应请求的程序进行处理(例如 CGI 脚本,JSP 脚本,servlets,ASP 脚本,服务器端 JavaScript,或者一些其它的服务器端技术等),然后返回后台程序处理产生的结果作为响应,发送到客户端。

浏览器解析渲染页面

渲染页面

浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:

  1. 解析HTML,构建DOM树

  2. 解析CSS,生成CSS规则树

  3. 合并DOM树和CSS规则,生成render树

  4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算

  5. 绘制render树(paint),绘制页面像素信息

解析HTML,构建DOM树

过程:字节 → 字符 → 令牌 → 节点 → 对象模型。

构建DOM树

关键步骤:

  1. Conversion转换:浏览器将获得的HTML内容(Bytes)基于他的编码转换为单个字符

  2. Tokenizing分词:浏览器按照HTML规范标准将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集

  3. Lexing词法分析:分词的结果是得到一堆的token,此时把他们转换为对象,这些对象分别定义他们的属性和规则

  4. DOM构建:因为HTML标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样
    例如:body对象的父节点就是HTML对象,然后段略p对象的父节点就是body对象

解析CSS,生成CSS规则树

过程和构建DOM树类似:字节 → 字符 → 令牌 → 节点 → CSSOM

CSS规则树

合并DOM树和CSS规则树,生成render树

当DOM树和CSSOM都有了后,就要开始构建渲染树了

一般来说,渲染树和DOM树相对应的,但不是严格意义上的一一对应,因为有一些不可见的DOM元素不会插入到渲染树中,如head这种不可见的标签或者display: none等
如下图:
render-tree

布局render树(Layout/Reflow重排)

布局:通过渲染树中渲染对象的信息,计算出每一个节点(元素)的位置和尺寸。

绘制render树(Paint 重绘)

绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。

总结渲染流程

  1. 获取DOM后分割为多个图层
  2. 对每个图层的节点计算样式结果 (Recalculate style–样式重计算)
  3. 为每个节点生成图形和位置 (Layout–重排,回流)
  4. 将每个节点绘制填充到图层位图中 (Paint–重绘)
  5. 图层作为纹理上传至GPU
  6. 组合多个图层到页面上生成最终屏幕图像 (Composite Layers–图层重组)

回流和重绘注意点

回流:当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

一下引起回流:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类(例如::hover)
  • 查询某些属性或调用某些方法

引起回流的属性和方法:

  • clientWidth、clientHeight、clientTop、clientLeft
  • offsetWidth、offsetHeight、offsetTop、offsetLeft
  • scrollWidth、scrollHeight、scrollTop、scrollLeft
  • scrollIntoView()、scrollIntoViewIffNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

如何减少回流

css

  • 避免使用table布局;
  • 尽可能在DOM树的最末端改变class;
  • 避免设置多层内联样式;
  • 将动画效果应用到position属性为absolute或fixed的元素上;
  • 避免使用CSS表达式(例如:calc())。

JS

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

重绘:当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

回流必将引起重绘,而重绘不一定会引起回流

断开连接:TCP 四次挥手

TCP-disconnect
刚开始双方都处于established状态,假如是客户端先发起关闭请求

  1. 第一次挥手:客户端发送一个FIN报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态
  2. 第二次挥手:服务端收到FIN之后,会发送ACK报文,且把客户端的序列号值+1作为ACK报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT状态
  3. 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发送FIN报文,且指定一个序列号。此时服务端处于LAST_ACK的状态
  4. 需要过一阵子以确保服务端收到自己的ACK报文之后才会进入CLOSED状态,服务端收到ACK报文之后,就处于关闭连接了,处于CLOSED状态。

为什么要挥手四次

因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

参考文章:
渲染树构建、布局及绘制
MDN
浏览器渲染详细过程:重绘、重排和 composite 只是冰山一角
从输入URL开始建立前端知识体系
从URL输入到页面展现到底发生什么?