整个流程
DNS解析
TCP连接
发送HTTP请求
服务器处理请求并返回HTTP报文
浏览器解析渲染页面
流程详细解析
1.DNS解析
DNS解析即资源寻找过程。例如输入的URL为 www.baidu.com ,其实这个网址并不是百度的实际地址。在互联网中,每一台机器都有其唯一标识的IP地址,但是这个IP地址是一串数字,并且这串数字很乱,不好记,这时就采用网址和IP地址的转换来处理,即DNS解析。
DNS的解析过程
DNS解析实际上是一个递归的过程: 输入 www.baidu.com 网址后,首先在本地的域名服务器中进行查找,如果没有找到,再去根域名服务器查找,如果还没有,再去com顶级域名服务器查找,直到找到IP地址,然后将其记录在本地,以供下次使用。
DNS的优化
由DNS的解析过程,我们可以知道解析中经历了很多过程,每个过程都有一定时间消耗,因此需要进行优化。
DNS的缓存
DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
- 在你的chrome浏览器中输入: chrome://dns/ ,你可以看到chrome浏览器的DNS缓存。
- 系统缓存主要存在/etc/hosts(Linux系统)中。
DNS负载均衡
实际上,在访问baidu.com的时候,每次响应的并非是同一个服务器(因为IP地址不同),一般大公司都有成百上千台服务器来支撑访问,假设只有一个服务器,那它的性能和存储量要多大才能支撑这样大量的访问呢?DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡。
2.TCP连接
TCP连接这个过程其实就是我们老生常谈的两个过程:三次握手、四次挥手。
三次握手
第一次
客户端向服务端发送SYN包,其中SYN=1,Seq=X,然后进入SYN_SEND状态,等待服务器响应
第二次
服务端收到SYN包,确认客户端的SYN,同时向客户端发送一个SYN包,其中SYN=1,ACK=X+1,Seq=Y,即SYN+ACK,然后服务端进入SYN_RECV状态
第三次
客户端收到服务端发送的SYN+ACK,然后向服务端发送确认包ACK,其中ACK=Y+1,此包发送完后客户端和服务端都会进入ESTABLISHED状态,这是三次握手完成
注意点:
- 在三次握手的过程中,客户端与服务端发送的包中不包含数据,在三次五首完成后,客户端与服务端之间才会正式开始传输数据。
- TCP连接一旦建立好,在客户端与服务端中任意一端主动关闭连接之前,TCP连接都会一直保持。
- 两次握手能实现吗? 答案是不可以。 假设客户端为A,服务端为B。在三次握手中, A和B都有一个发syn和收ack的过程, 双方都是发后能收, 表明通信则准备工作完成。
四次挥手
TCP连接建立完成后,双方进行数据传输,在数据传输完成后,双方均可断开连接。假设客户端主动断开连接,则连接断开过程如下:
第一次挥手
客户端发送一个FIN给服务端,即客户端告诉服务端:我已经不会再给你发数据了(在FIN包之前发送出去的数据,如果没有收到对应的ACK确认报文,客户端依然会重发这些数据),但是,此时客户端还可以接受数据。 FIN=1,Seq=X,ACK=Z(等于前面已经传送过来的数据的最后一个字节的序号加1),然后客户端进入FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
第二次挥手
服务器收到客户端发来的FIN包,并发送一个ACK+Seq给客户端,其中ACK=X+1,Seq=Z。然后服务端进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
第三次挥手
服务器向客户端发送一个FIN包,即服务端告诉客户端:我已经不会再给你发数据了。其中FIN=1,ACK=X,Seq=Y。然后服务器进入LAST-ACK状态,等待客户端确认。
第四次挥手
客户端收到服务端发来的FIN包后,然后发送一个ACK给服务端,其中ACK=Y,Seq=X。然后客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有断开,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
注意点:
- 为什么客户端最后需要等待2MSL? (1)保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。 (2)防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
- 三次挥手能实现吗? 答案是不可以。 在断开连接时,服务端收到客户端发送的FIN报文时,仅仅表示客户端不再发送数据了但是还能接收数据,而服务端本身也未必已将全部数据都发送给对方了,所以服务端可以立即关闭,也可以再次发送一些数据给客户端后,然后再发送FIN报文给客户端来表示同意现在断开连接,因此,服务端ACK和FIN一般都会分开发送,从而导致多了一次。
发送HTTP请求
发送HTTP请求的过程其实就是构建HTTP请求报文并通过TCP协议发送到服务器指定的端口。其中,请求报文由请求行、请求头和请求正文三部分组成。
请求行
请求行的格式为Method Request-URL HTTP-Version CRLF eg: GET index.html HTTP/1.1
常用的方法有GET, POST, PUT, DELETE, OPTIONS, HEAD
请求方法的区别(POST、GET)
- GET参数通过url传递,POST放在request body中
- GET请求在url中传递的参数是有长度限制的,而POST没有
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
- GET请求参数会给完整保留在浏览器历史记录里,而POST的参数不会被保留
- GET请求只能进行url编码,而POST支持多种编码方式
- GET请求被浏览器主动cache,而POST不会,除非手动设置
- GET产生的url地址可以被bookmark,而POST不可以
- GET在浏览器回退时是无害的,而POST会再次提交请求 最大的区别: GET会产生一个TCP数据包,POST会产生两个数据包。
- 对于GET请求,浏览器会将http header和data一起发送,然后服务器响应200,返回数据
- 对于POST请求,浏览器会先发送header,服务器响应100 continue,然后浏览器再将data发送,服务器响应200,返回数据(火狐浏览器只会发送一次)
请求头(Request Headers)
- 请求头中包含有accept、accept-encoding、accept-language、access-control-request-headers等字段(具体可以自行打开浏览器Network进行查看)。
- 其中,accept用于指定客户端用于接受哪些类型的信息,accept-Encoding与accept类似,它用于指定接受的编码方式。connection设置为keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间。
请求正文
对于POST, PUT等请求,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中。在请求头中有一些与请求正文相关的信息,例如: 现在的Web应用通常采用Rest架构,请求的数据格式一般为json。这时就需要设置Content-Type: application/json。
HTTP缓存
HTTP缓存属于客户端缓存,我们常认为浏览器有一个缓存数据库,用来保存一些静态文件。
HTTP缓存规则
强制缓存
- 当缓存数据库中有客户端需要的数据,客户端直接将数据从其中拿出来使用(如果数据未失效),当缓存服务器没有需要的数据时,客户端才会向服务端请求。
协商缓存
- 客户端会先从缓存数据库拿到一个缓存的标识,然后向服务端验证标识是否失效,如果没有失效服务端会返回304,这样客户端可以直接去缓存数据库拿出数据,如果失效,服务端会返回新的数据
- 强制缓存的优先级高于协商缓存,若两种缓存皆存在,且强制缓存命中目标,则协商缓存不再验证标识。
HTTP缓存的方案
HTTP缓存相关的规则信息包含在header中。
强制缓存的方案
对于强制缓存,服务器响应的header中会用两个字段来表明——Expires和Cache-Control。
- Expires Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差,另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代。
- Cache-Control Cache-Control有很多属性,不同的属性代表的意义也不同。
- private:客户端可以缓存
- public:客户端和代理服务器都可以缓存
- max-age=t:缓存内容将在t秒后失效
- no-cache:需要使用协商缓存来验证缓存数据
- no-store:所有内容都不会缓存。
协商缓存的方案
协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就可以直接使用缓存数据了。
HTTP缓存的优点
- 减少了冗余的数据传递,节省宽带流量
- 减少了服务器的负担,大大提高了网站性能
- 加快了客户端加载网页的速度 这也正是HTTP缓存属于客户端缓存的原因。
不同刷新对应的请求执行过程
浏览器地址栏中写入URL,回车
- 浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿。(最快)
F5
- F5就是告诉浏览器,别偷懒,好歹去服务器看看这个文件是否有过期了。于是浏览器就战战兢兢的发送一个请求带上If-Modify-since。
Ctrl+F5
- 告诉浏览器,你先把你缓存中的这个文件给我删了,然后再去服务器请求个完整的资源文件下来。于是客户端就完成了强行更新的操作.
服务器处理请求并返回HTTP报文
它会对TCP连接进行处理,对HTTP协议进行解析,并按照报文格式进一步封装成HTTP Request对象,供上层使用。HTTP报文也分成三份,状态码 ,响应报头和响应报文
状态码
状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:
- 1xx:指示信息–表示请求已接收,继续处理。
- 2xx:成功–表示请求已被成功接收、理解、接受。
- 3xx:重定向–要完成请求必须进行更进一步的操作。
- 4xx:客户端错误–请求有语法错误或请求无法实现。
- 5xx:服务器端错误–服务器未能实现合法的请求。
平时遇到比较常见的状态码有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500
- 200 请求成功,通常服务器提供了需要的资源
- 204 服务器成功处理了请求,但没有返回任何内容
- 301 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置
- 302 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
- 304 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容
- 400 服务器不理解请求的语法
- 401 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应
- 403 服务器拒绝请求
- 404 服务器找不到请求的网页
- 422 请求格式正确,但是由于含有语义错误,无法响应
- 500 服务器遇到错误,无法完成请求
响应头
常见的响应头字段有: Server, Connection...
响应报文
从服务器请求的HTML,CSS,JS文件就放在这里面
浏览器解析渲染页面
- 浏览器解析HTML形成DOM树
- 浏览器解析CSS形成CSSOM 树
- 浏览器合并DOM树和CSSOM树形成渲染树(render tree)
- 浏览器开始渲染并绘制页面
回流与重绘
回流
当渲染树(render tree)中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的DOM元素
- 激活CSS伪类(例如::hover)
- 查询某些属性或调用某些方法
一些常用且会导致回流的属性和方法:
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、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(事件循环机制)
js的事件循环机制主要由调用栈、消息队列和微任务、宏任务组成。一段js执行时,首先自上而下会压入调用栈中,若为宏任务则进入消息队列,若为微任务进入微任务中。微任务会在调用栈清空时立即执行,宏任务会在调用栈清空后执行。关于宏任务、微任务等概念在此不细说。
setTimeout(function () {
console.log('定时器开始啦')
});
new Promise(function (resolve) {
console.log('马上执行for循环啦');
for (var i = 0; i < 10000; i++) {
i == 99 && resolve();
}
}).then(function () {
console.log('执行then函数啦')
});
console.log('代码执行结束');
输出结果为:
- 马上执行for循环啦
- 代码执行结束
- 执行then函数啦
- 定时器开始啦
setTimeout(function () {
console.log('setTimeout');
})
new Promise(function (resolve) {
console.log('promise');
}).then(function () {
console.log('then');
})
console.log('console');
输出结果为:
- promise
- console
- setTimeout
console.log('1');
setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})
process.nextTick(function () {
console.log('6');
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8')
})
setTimeout(function () {
console.log('9');
process.nextTick(function () {
console.log('10');
})
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {
console.log('12')
})
})
输出结果为:
- 1
- 7
- 6
- 8
- 2
- 4
- 3
- 5
- 9
- 11
- 10
- 12
总结
本文为本人根据看过的文章、自身的实践和理解之后,整理的文章,若有错误,欢迎指正,谢谢。