作为一道面试经常会问到的题目,看过很多写的很好的博文,整理成自己的笔记
大概来说分为以下几个过程:
- 输入url地址
- 应用层进行DNS解析
- 应用层生成HTTP请求报文
- 传输层建立TCP连接
- 网络层使用IP协议来选择路线
- 数据链路层实现网络相邻节点间可靠的数据通信
- 物理层传输数据
- 服务器处理反向传输
- 服务器返回一个 HTTP 响应
- 浏览器渲染
而主干的流程又分为:
- 浏览器构建HTTP Request请求
- 网络传输
- 服务器构建HTTP Response 响应
- 网络传输
- 浏览器渲染页面
输入地址
- 当我们开始在浏览器中输入网址的时候,浏览器其实就已经在智能的匹配可能得 url 了,他会从历史记录,书签等地方,找到已经输入的字符串可能对应的 url,然后给出智能提示,让你可以补全url地址。
- 对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
(一)DNS域名解析
我们先来了解一下url的组成部分
- scheme:代表的是访问的协议,一般为http或者https以及ftp等。 host:主机名,域名,比如www.baidu.com。
- path:查找路径。比如:www.jianshu.com/trending/now,后面的trending/now就是path。
- query-string:查询字符串,比如:www.baidu.com/s?wd=python,后面的wd=python就是查询字符串。
- anchor:锚点,后台一般不用管,前端用来做页面定位的。
为什么要进行DNS解析?
互联网中每一台机器都有唯一标识的IP地址,但是它不好记,所以我们通常通过访问域名来访问目标网站,例如 baidu.com、google.com 等。我们只需记住这些网站的名称,而非它们的 IP 地址。因此,在访问服务器到时候,需要 DNS 解析实现网址和IP地址的转换,从而访问到真正对应IP的服务器。
具体解析过程
DNS解析其实是一个递归的过程
输入www.google.com网址后,首先在本地的域名服务器中查找,没找到去根域名服务器查找,没有再去com顶级域名服务器查找,,如此的类推下去,直到找到IP地址,然后把它记录在本地,供下次使用。大致过程就是. -> .com -> google.com. -> www.google.com.。 (你可能觉得我多写 .,并木有,这个.对应的就是根域名服务器,默认情况下所有的网址的最后一位都是.,既然是默认情况下,为了方便用户,通常都会省略,浏览器在请求DNS的时候会自动加上)
DNS缓存
DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
DNS负载均衡
例如访问baidu.com的时候,每次响应的并非是同一个服务器(IP地址不同),一般大公司都有成百上千台服务器来支撑访问,假设只有一个服务器,那它的性能和存储量要多大才能支撑这样大量的访问呢?DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡。
(二)TCP 连接:TCP 三次握手
- 第一次握手:客户端给服务端发一个SYN报文,并指明客户端的初始化序列号ISN。此时客户端处于SYN_SENT的状态。
- 第二次握手:服务端收到客户端的SYN报文之后,会以自己的SYN报文作为应答,并且也是指定了自己的初始化序列号ISN(S)。同时会把客户端的ISN+1作为ACK的值,表示自己已经收到了客户端的SYN,此时服务器处于SYN_RCVD的状态。
- 第三次握手:客户端收到SYN报文之后,会发送一个ACK报文,当然,也是一样把服务器的ISN+1作为ACK的值,表示已经接收到了服务端的SYN报文,此时客户端于ESTABLISHED状态。
服务器收到ACK报文之后,也处于ESTABLISHED状态,此时,双方建立起了连接。
为什么是三次而不是两次?
假定出现一种异常情况,即A发出的第一个连接请求报文段并没有丢失,而是在某些网络节点长时间滞留了,以致延误到连接释放以后的某个时间才到B。本来这是一个已失效的报文段。但是B收到此失效的连接请求报文段后,就误认为是A有发出一次新的连接请求。于是就向A发出确认报文段,同意建立连接。假定不采用三次握手,那么只要B发出确认,新的连接就建立了。
由于现在A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据。但B却以为新的运输连接已经建立了,并一直等待A发来数据。B的许多资源就这样拜拜浪费了。
采用三次握手的办法可以防止上述现象的发生。例如在刚才的情况下,A不会向B的确认发出确认。B由于收不到确认,就知道A并没有要求建立连接。
(三)发送HTTP请求
发送http请求的过程就是构建HTTP请求报文并通过TCP协议发送到服务器指定端口。请求报文由请求行,请求报头,请求正文组成。
请求行
请求行的格式为 Method, Request-URL, HTTP-Version, CRLF
常用的方法有:GRT,POST,PUT,DELETE,OPTIONS,HEAD
常见的请求方法区别
- GET在浏览器回退时是无害的,而POST会再次请求提交
- GET产生的URL地址可以被Bookmark,而POST不可以
- GET请求会被浏览器主动cache,而POST不会,除非手动设置
- GET请求参数会被完成保留在浏览器历史记录里,而POST中的参数不会被保留
- GET请求在URL中传送的参数是有长度限制的,而POST没有
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
- GET参数通过URL传递,POST放在Request body中
请求正文
当使用POST,PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中。在请求报头中有一些与请求正文相关的信息,例如:现在的web应用通常采用Rest架构,请求的数据格式一般为接送,这时就需要设置Content-Type: application/json。
HTTP缓存
HTTP属于客户端缓存,我们常认为浏览器有一个缓存数据库,用来保存一些静态文件,下面我们分为以下几方面来简单介绍HTTP缓存
- 缓存的规则
- 缓存的方案
- 缓存的优点
- 不同刷新的请求执行过程
缓存的规则
分为强制缓存和协商缓存
强制缓存
当缓存数据库中有客户端需要的数据,客户端直接将数据从其中拿出来使用(如果数据未失效),当缓存服务器没有需要的数据时,客户端才会向服务端发请求
协商缓存
又称对比缓存,客户端会优先从缓存数据库拿到一个缓存的标识,然后向服务端验证标识是否失效,如果没有失效服务端会返回304,这样客户端可以直接去缓存数据库拿出数据,如果失效,服务端会返回新的数据
强制缓存的优先级高于协商缓存,若两种缓存皆存在,且强制缓存命中目标,则协商缓存不再验证标识。
缓存的方案
上面的内容让我们大概了解了缓存机制是怎么运行的,但是,服务器是如何判断缓存是否失效呢?我们知道浏览器和服务器进行交互的时候会发送一些请求数据和响应数据,我们称之为HTTP报文。报文中包含首部的header和主体部分body。与缓存相关的规则信息就包含在header中。body中的内容是HTTP请求真正要传输的部分。
强制缓存方案
对于强制缓存,服务器响应的header中会用两个字段来说明——Expires和Cache-Control。
Expires
Expires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差,另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代。
Cache-Control
Cache-Control有很多属性,不同的属性代表的意义也不同。
- private:客户端可以缓存
- public:客户端和代理服务器都可以缓存
- max-age=t:缓存内容将在t秒后失效
- no-cache:需要使用协商缓存来验证缓存数据
- no-store:所有内容都不会缓存
协商缓存
协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就可以直接使用缓存数据了。
对于协商缓存来说,缓存标识我们需要着重理解一下,下面我们将着重介绍它的两种缓存方案。
Last-Modified
Last-Modified:服务器在响应请求时,会告诉浏览器资源的最后修改时间。
if-Modified-Since:浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务器收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致就返回304和响应报文头,浏览器只需要从缓存中获取信息即可。从字面上看,就是说:从某个时间节点算起,是否文件被修改了
- 如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK
- 如果没有被修改:那么只需传输响应header,服务器返回:304 Not Modified
if-Unmodified-Since:从字面上看, 就是说: 从某个时间点算起, 是否文件没有被修改
- 如果没有被修改:则开始`继续’传送文件: 服务器返回: 200 OK
- 如果文件被修改:则不传输,服务器返回: 412 Precondition failed (预处理错误)
这两个的区别是一个是修改了才下载一个是没修改才下载。
Last-Modified 说好却也不是特别好,因为如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。为了解决这个问题,HTTP1.1推出了Etag。
Etag
Etag:服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)
If-None-Match:再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现If-None-Match则与被请求资源的唯一标识进行对比。
- 不同,说明资源被改动过,则响应整个资源内容,返回状态码200。
- 相同,说明资源无心修改,则响应header,浏览器直接从缓存中获取数据信息。返回状态码304。
但是实际应用中由于Etag的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用Etag了。
不同刷新的请求执行过程
输入地址栏中输入URL,回车
浏览器发现缓存中有这个文件栏,不用继续请求栏,直接去缓存拿(最快)
F5
F5就是告诉浏览器被偷懒,好歹去服务器看看这个文件是否过期了。于是浏览器战战兢兢的发送一个请求带上If-Modify-since
Ctrl+F5
告诉浏览器,你先把你缓存中的这个文件给我删了,然后再去服务器请求一个完整的资源文件下来。于是客户端就完成了强行更新的操作
(四)浏览器解析渲染页面
浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:
- 解析HTML,构建DOM树
- 解析CSS,生产CSS规则树
- 合并DOM树和CSS规则,生成render树
- 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
- 绘制render树(paint),绘制页面像素信息
以下就是webkit内核的渲染步骤:
1.HTML解析,构建DOM
其中比较关键的几个步骤: - Conversion转换:浏览器将获得的HTML内容(Bytes)基于他的编码转换为单个字符
- Tokenizing分词:浏览器按照HTML规范标准将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集
- Lexing词法分析:分词的结果是得到一堆的token,此时把他们转换为对象,这些对象分别定义他们的属性和规则
- DOM构建:因为HTML标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样
例如:body对象的父节点就是HTML对象,然后段落p对象的父节点就是body对象
2.解析CSS,生成CSS规则树
同理,CSS规则数的生成也是类似。
Bytes → characters → tokens → nodes → CSSOM
3.合并DOM树和CSS规则,生成render树
当DOM数和CSSDOM都有了后,就要开始构建渲染树了
一般来说,渲染树和DOM树相对应的,但不是严格意义上的一一对应,因为有一些不可见的DOM元素不会插入到渲染树中,如head这种不可见的标签或者display: none等
4.布局render树(Layout/Reflow),负责各元素尺寸、位置的计算
布局:通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸。
5.绘制render树(paint),绘制页面像素信息
绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。
这张图片中重要的四个步骤
- 计算CSS样式
- 构建渲染树
- 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性
- 绘制,将图像绘制出来
回流
Layout,也称为Reflow,当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的DOM元素
- 激活CSS伪类(例如::hover)
- 查询某些属性或调用某些方法
一些常用且会导致回流的属性和方法:
- clientWidth、clientHeight、clientTop、clientLeft
- offsetWidth、offsetHeight、offsetTop、offsetLeft
- scrollWidth、scrollHeight、scrollTop、scrollLeft
- scrollIntoView()、scrollIntoViewIfNeeded() getComputedStyle()
- getBoundingClientRect() scrollTo()
重绘
Repaint,当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
优化css:
- 避免使用table布局。
- 尽可能在DOM树的最末端改变class。
- 避免设置多层内联样式。
- 将动画效果应用到position属性为absolute或fixed的元素上。
- 避免使用CSS表达式(例如:calc())。
优化js:
- 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
- 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
- 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
其他修改和补充,持续更新
文章参考:
https://juejin.cn/post/6844903784229896199
https://juejin.cn/post/6844903832435032072#heading-59
https://blog.csdn.net/dayangjie/article/details/121459428