域名解析 —> 发起TCP的3次握手 —> 建立TCP连接后发起http请求 —> 服务器响应http请求,浏览器得到html代码 —> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) —> 浏览器对页面进行渲染呈现给用户

简单来说,共有以下几个过程:

  • DNS解析
  • 发起TCP连接
  • 发送HTTP请求
  • 服务器处理请求并返回HTTP报文
  • 浏览器解析渲染页面
  • 连接结束。

image.png

DNS解析

网址和IP地址的转换,也就是DNS解析。DNS解析其实是一个递归的过程。
image.png
输入www.google.com网址后,首先在本地的域名服务器中查找,没找到去根域名服务器查找,没有再去com顶级域名服务器查找,,如此的类推下去,直到找到IP地址,然后把它记录在本地,供下次使用。
大致过程就是. -> .com -> google.com. -> www.google.com. (你可能觉得我多写 .,并木有,这个.对应的就是根域名服务器,默认情况下所有的网址的最后一位都是.,既然是默认情况下,为了方便用户,通常都会省略,浏览器在请求DNS的时候会自动加上)

DNS优化

DNS缓存

DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。

DNS负载均衡

你访问baidu.com的时候,每次响应的并非是同一个服务器(IP地址不同),一般大公司都有成百上千台服务器来支撑访问。DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡。

发起TCP连接

TCP提供一种可靠的传输,这个过程涉及到三次握手,四次挥手,下面我们详细看看 TCP提供一种面向连接的,可靠的字节流服务。 其首部的数据格式如下
image.png

字段分析

  • 源端口:源端口和IP地址的作用是标识报文的返回地址。
  • 目的端口:端口指明接收方计算机上的应用程序接口。

TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP唯一确定一条TCP连接。

三次握手

第一次握手:
客户端发送syn包(Seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:
服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(Seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:
客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。
image.png

为什么会采用三次握手,若采用二次握手可以吗? 四次呢?

两次握手:建立连接的过程是利用客户服务器模式,假设主机A为客户端,主机B为服务器端。采用三次握手是为了防止失效的连接请求报文段突然又传送到主机B,因而产生错误。失效的连接请求报文段是指:主机A发出的连接请求没有收到主机B的确认,于是经过一段时间后,主机A又重新向主机B发送连接请求,且建立成功,顺序完成数据传输。特殊情况,主机A第一次发送的连接请求并没有丢失,而是因为网络节点导致延迟达到主机B,主机B以为是主机A又发起的新连接,于是主机B同意连接,并向主机A发回确认,但是此时主机A根本不会理会,主机B就一直在等待主机A发送数据,导致主机B的资源浪费。
四次握手:通信中著名的蓝军红军约定, 这个例子说明, 通信不可能100%可靠, 而上面的三次握手已经做好了通信的准备工作, 再增加握手, 并不能显著提高可靠性, 而且也没有必要。

四次挥手

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,假设客户端主动关闭,服务器被动关闭。
image.png
第一次挥手:
客户端发送一个FIN,用来关闭客户端到服务器的数据传送,也就是客户端告诉服务器:我已经不会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,客户端依然会重发这些数据),但是,此时客户端还可以接受数据。 FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
第二次挥手:
服务器收到FIN包后,发送一个ACK给对方并且带上自己的序列号seq,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号)。此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
第三次挥手:
服务器发送一个FIN,用来关闭服务器到客户端的数据传送,也就是告诉客户端,我的数据也发送完了,不会再给你发数据了。由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手:
主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
至此,完成四次挥手。
为什么客户端最后还要等待2MSL?
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。

第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。

第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

为什么建立连接是三次握手,关闭连接确是四次挥手呢?

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。 而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

发送HTTP请求

HTTP的端口为80/8080,而HTTPS的端口为443。
请求报文由请求行,请求抱头,请求正文组成。

请求行

请求行的格式为Method Request-URL HTTP-Version CRLF eg: GET index.html HTTP/1.1 常用的方法有: GET, POST, PUT, DELETE, OPTIONS, HEAD
常见的请求方法区别
展示POST和GET的区别

  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET产生的URL地址可以被Bookmark,而POST不可以。
  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。
  • GET请求只能进行url编码,而POST支持多种编码方式。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST么有。
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  • GET参数通过URL传递,POST放在Request body中。

注意一点你也可以在GET里面藏body,POST里面带参数。
重点区别:GET会产生一个TCP数据包,而POST会产生两个TCP数据包。

  • 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
  • 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

注意一点,并不是所有的浏览器都会发送两次数据包,Firefox就发送一次

请求报头

请求报头允许客户端向服务器传递请求的附加信息和客户端自身的信息。

Connection设置为Keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间。

可以封装Axios然后添加一些特定的请求头,比如Token、Id等。

请求正文

现在的Web应用通常采用Rest架构,请求的数据格式一般为json。这时就需要设置Content-Type: application/json。

缓存的规则

强制缓存的优先级高于协商缓存,若两种缓存皆存在,且强制缓存命中目标,则协商缓存不再验证标识。

强制缓存
image.png
协商缓存
image.png

缓存的方案

强制缓存
对于强制缓存,服务器响应的header中会用两个字段来表明——Expires和Cache-Control。
Expires
Exprires的值为服务端返回的数据到期时间。Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代。
Cache-Control

  • private:客户端可以缓存
  • public:客户端和代理服务器都可以缓存
  • max-age=t:缓存内容将在t秒后失效
  • no-cache:需要使用协商缓存来验证缓存数据
  • no-store:所有内容都不会缓存。

协商缓存
Last-Modified—服务器在响应请求时,会告诉浏览器资源的最后修改时间。

  • if-Modified-Since:浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回304和响应报文头,浏览器只需要从缓存中获取信息即可。
  • 如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK
  • 如果没有被修改:那么只需传输响应header,服务器返回:304 Not Modified
  • if-Unmodified-Since:从字面上看, 就是说: 从某个时间点算起, 是否文件没有被修改
  • 如果没有被修改:则开始`继续’传送文件: 服务器返回: 200 OK
  • 如果文件被修改:则不传输,服务器返回: 412 Precondition failed (预处理错误)

Etag—服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)

  • If-None-Match:再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现If-None-Match则与被请求资源的唯一标识进行对比。
    • 不同,说明资源被改动过,则响应整个资源内容,返回状态码200。
    • 相同,说明资源无心修改,则响应header,浏览器直接从缓存中获取数据信息。返回状态码304.

但是实际应用中由于Etag的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用Etag了。

缓存的优点

  • 减少了冗余的数据传递,节省宽带流量
  • 减少了服务器的负担,大大提高了网站性能
  • 加快了客户端加载网页的速度 这也正是HTTP缓存属于客户端缓存的原因。

不同刷新的请求执行过程

浏览器地址栏中写入URL,回车

  • 浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿。(最快)

F5

  • F5就是告诉浏览器,别偷懒,好歹去服务器看看这个文件是否有过期了。于是浏览器就战战兢兢的发送一个请求带上If-Modify-since。

Ctrl+F5

  • 告诉浏览器,你先把你缓存中的这个文件给我删了,然后再去服务器请求个完整的资源文件下来。于是客户端就完成了强行更新的操作.

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

它会对TCP连接进行处理,对HTTP协议进行解析,并按照报文格式进一步封装成HTTP Request对象,供上层使用。这一部分工作一般是由Web服务器去进行,我使用过的Web服务器有Tomcat, Nginx和Apache等等 HTTP报文也分成三份,状态码 ,响应报头和响应报文。

状态码

状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:

  • 1xx:指示信息–表示请求已接收,继续处理。
  • 2xx:成功–表示请求已被成功接收、理解、接受。
  • 3xx:重定向–要完成请求必须进行更进一步的操作。
  • 4xx:客户端错误–请求有语法错误或请求无法实现。
  • 5xx:服务器端错误–服务器未能实现合法的请求。

响应报头

常见的响应报头字段有: Server, Connection…。

响应报文

你从服务器请求的HTML,CSS,JS文件就放在这里面

浏览器解析渲染页面

image.png

  • 解析HTML形成DOM树
  • 解析CSS形成CSSOM 树
  • 合并DOM树和CSSOM树形成渲染树
  • 浏览器开始渲染并绘制页面

两个比较重要的概念回流和重绘。浏览器刚打开页面一定要经过这两个过程的,但是这个过程非常非常非常消耗性能,所以我们应该尽量减少页面的回流和重绘。
回流:DOM结点都是以盒模型形式存在,需要浏览器去计算位置和宽度等,这个过程就是回流。
重绘:等到页面的宽高,大小,颜色等属性确定下来后,浏览器开始绘制内容,这个过程叫做重绘。

性能优化之回流

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

会导致回流的操作:

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

一些常用且会导致回流的属性和方法:

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

性能优化之重绘

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

优化CSS

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

优化JavaScript

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

JS的解析

JS的解析是由浏览器的JS引擎完成的。由于JavaScript是单线程运行,也就是说一个时间只能干一件事,干这件事情时其他事情都有排队,但是有些人物比较耗时(例如IO操作),所以将任务分为同步任务异步任务,所有的同步任务放在主线程上执行,形成执行栈,而异步任务等待,当执行栈被清空时才去看看异步任务有没有东西要搞,有再提取到主线程执行,这样往复循环,就形成了Event Loop事件循环。

Event Loop

image.png

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

详见宏任务和微任务。

参考

  1. 史上最详细的经典面试题 从输入URL到看到页面发生了什么
  2. 细说浏览器输入URL后发生了什么