主体流程
主体流程:
1.解析url
2.DNS协议解析域名
3.ARP协议将ip地址转换为MAC地址
4.TCP协议的三次握手
5.发起请求时,触发浏览器缓存
6.从后台/浏览器返回html
7.若向后台发起请求,则进行TCP协议的四次挥手
8.浏览器的渲染进程对html进行解析
构建DOM树(DOM tree):
1.从上到下解析HTML文档生成DOM节点树(DOM tree)
2.构建CSSOM(CSS Object Model)树:加载解析样式生成CSSOM树
3.执行JavaScript:加载并执行JavaScript代码(包括内联代码或外联JavaScript文件)
4.构建渲染树:根据DOM树和CSSOM树,生成渲染树(render tree)
5.渲染树:按顺序展示在屏幕上的一系列矩形,这些矩形带有字体,颜色和尺寸等视觉属性
6.布局(layout):根据渲染树将节点树的每一个节点布局在屏幕上的正确位置
7.回执(painting):遍历渲染树绘制所有节点,为每一个节点适用对应的样式
url方面
1.解析url:
url为什么要解析?
因为网络标准规定了URL只能是英文字母和阿拉伯数字,还有一些其它特殊符号(-_.~ ! * ' ( ) ; : @ & = + $ , / ? # [ ])
2.url编码问题:
RFC1738规定:
url只有字母和数字[0-9a-zA-Z]、一些特殊符号"$-_.+!*'(),"[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL
这就意味着,如果url中有汉字,就必须编码后使用(导致各个浏览器自己决定)
情况1:网址路径中包含汉字:
http://zh.wikipedia.org/wiki/春节
"春节"这两个字此时是网址路径的一部分
IE(Firefox)将"春节"编码成"%E6%98%A5%E8%8A%82"(使用utf-8)
情况2:查询字符串包含汉字
http://www.baidu.com/s?wd=春节
"春节"这两个字属于查询字符串,不属于网络路径
IE将"春节"编码成"B4 BA BD DA"(使用操作系统的默认编码,此时是GB2312编码)
情况3:Get方法生产的URL包含汉字(用网页设定的编码)
前面说的是输入网址的情况,另一种是在已经打开的网页,直接用Get或Post方法发出HTTP请求
这时的编码方法由网页的编码决定,也就是由HTML源码中字符集的设定决定:
<meta http-equiv="Content-Type" content="text/html;charset=xxxx">
这时charset的值为utf-8时,则url就以utf-8编码.如果是GB2313,URL就以GB2312编码
情况4:Ajax调用的URL包含汉字
使用Javascript生成HTTP请求,也是Ajax调用
当网页使用什么字符集,IE传送给服务器的总是"q=%B4%BA%BD%DA",而Firefox传送给服务器的是"q=%E6%98%A5%E8%8A%82"
得出的结论:
Ajax调用中,IE总是GB2312编码(操作系统的默认编码),而Firefox总是采用utf-8编码
3.有没有办法,能够保证客户的只用一种编码方法向服务器发出请求?
有的,就是使用Javascript先对URL编码,然后再向服务器提交,不要给浏览器插手的机会(因为Javascirpt的输出总是一致的,所以就保证了服务器得到的数据是格式统一的)
1.escape()(已经逐渐被淘汰)
escape("春节")
"%u6625%u8282"
//无论网页的原始编码是什么,一旦被Javascript编码,就都变成unicode字符
2.encodeURI()
用于对整个URL进行编码,
不进行编码:
1.保留字符:'; , / ? : @ & = + $'
2.非转义的字符:'字母 数字 '- _ . ! ~ * ' ( )'
3.数字符号:'#'
encodeURI 自身无法产生能适用于 HTTP GET 或 POST 请求的 URI,例如对于 XMLHTTPRequests,因为 "&", "+", 和 "=" 不会被编码,然而在 GET 和 POST 请求中它们是特殊字符
encodeURI("春节")
"%E6%98%A5%E8%8A%82"
3.encodeURIComponent()
用于对URL的组成部分进行个别编码,而不是对整个URL进行编码,而不用于对整个URL进行编码
不转译字符:
'A-Z a-z 0-9 - _ . ! ~ * ' ( )'
自动编码为utf-8
";/?:@&=+$,#",这些在encodeURI()中不被编码的符号,在encodeURIComponent()中统统会被编码
encodeURIComponent("mail@example.com")
"mail%40example.com"
总结:
它们都是编码URL,唯一区别就是编码的字符范围,其中
encodeURI方法不会对下列字符编码 ASCII字母、数字、~!@#$&*()=:/,;?+'
encodeURIComponent方法不会对下列字符编码 ASCII字母、数字、~!*()'
所以encodeURIComponent比encodeURI编码的范围更大.
encodeURI适合用于URL本身编码
encodeURIComponnent编码范围更广,适合给参数编码
项目里一般都用qs库去处理
2.DNS协议:
DNS解析过程:
1.当用户在浏览器中输入URL时,
操作系统会首先查找自己本地的hosts文件,
如果没有,hosts文件再去查DNS缓存.
如果DNS缓存中未找到,则会向本地DNS服务器发出请求
非转发模式:
2.如果本地DNS服务器没有缓存或映射,它会向根DNS服务器(.)发送请求
3.根DNS服务器返回顶级域名服务器的地址,然后本地DNS服务器再去请求这个顶级域名服务器,顶级域名服务器返回google.com权威DNS服务器的地址,再通过权威DNS服务器查到google.com的A记录
4.最后,通过权威DNS服务器的地址,就会找到magedu.com域服务器,本地DNS服务器将www.google.com的IP地址返回浏览器,浏览器可以使用它来连接到所请求的网站
转发模式:
2.此DNS服务器就会把请求转发至上一级ISP DNS服务器,由上一级服务器进行解析,上一服务器如果不能解析,或找根DNS或把请求转至上上级,以此循环.不管本地DNS服务器用的是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机
DNS域名称空间(整个DNS域名空间呈倒立的树状结构分布):
1.根域:.
2.顶级域:net、com、org(由两三个字母组成的名称,用于指示国家(地区)或使用名称的单位的类型)
3.二级域(权限域名):nwtraders
4.子域:west、south、east
DNS组成:
是由多个层次的域名系统组成,其中最顶层的域名是根域名(.),例如baidu.com完整域名是baidu.com.,其中com是顶级域名,baidu是com的子域名
二级域名及更多级别的域名都由权威域名服务器进行解析
每个域名可以有多个子域名,其中子域名是父域名的一个分支
DNS通过使用层级结构来管理域名和IP地址之间的映射关系
两种查询方法详解:
1.递归查询:
客户端得到结果要么成功,要么失败(本地客户端和DNS服务器直接交互,被请求的DNS服务器必须给出最终答案)
2.迭代查询:
服务器以相关参考性应答返回本地DNS(DNS服务和DNS服务交互,得到的是参考答案)
本地客户端向本地域名服务器查询请求时,查询类型为:
一次递归,多次迭代
3.ARP协议
ARP协议就是将IP地址解析成对应的MAC地址
4.TCP协议
TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议
互联网和单个网络有很大的不同,因为互联网络的不同部分可能有截然不同的拓扑结构、带宽、延迟、数据包大小和其他参数
TCP的设计目标是能够动态地适应互联网络的这些特性,而且具备面对各种故障时的健壮性
1.TCP三次握手:
流程:
建立TCP连接时,需要客户端和服务器端发送三个包以确定连接的建立
一旦获取到服务器IP地址,浏览器就会通过TCP握手,和服务器建立连接
TCP的"三次握手",被称为SYN(synchronizing同步)、SYN-ACK、ACK(Acknowledge承认)
TCP是一种可靠的数据传输协议:
1.不丢包(发送)
2.保证数据的有序性(接收)
可靠的传输质量:
1.从发包角度,所有的数据都被对方收到了
2.从收包角度,所有对端发出的数据自己都按序收到了
准备工作:
为了保证不丢包并保证包到达的顺序,TCP设计了一系列的工作机制,这些工作机制需要建立在双方互相了解的基础上
假设A和B之间要建立连接,则A要告诉B自己的初始序列号Seq和接收窗口大小,B要用Ack表明自己能接收到A的消息
同理,B也要告诉A自己的初始序列号和接收窗口大小,A最后也要用Ack表明自己能接收到B的消息
通过这样的几次交互,各自的Seq、Ack和接收窗口的元信息就都确认了,这就为后续的网络数据传输做好了准备
设计机制:
1.序号Seq
2.回执Ack
3.重传机制
4.接收窗口累积确认
基本流程:
为了保证不丢包(ack回执或重传机制):
刚开始发送方给每个发出的包都编排一个序号seq=X,若是对端收到序号为X的包,则要告知发送方seq为X的包都收到了,TCP协议使用ACK机制来确认消息,使用ack=X+1的回执来确认seq=X的包已经收到
如果没有收到seq=X的数据包,TCP协议使用重传机制来处理异常,发送方在等待一段时间之后,如果一直没有ack=X+1的回执,就会重传seq=X的数据包
保证包到达的顺序:
就是保证应用程序按照发送方发出的顺序,接收到所有的数据包,TCP协议使用了接收窗口累积确认机制
例如:
数据包通过网络到达接收窗口缓冲区的顺序不一定是有序的,比如seq=1、2、3、6的包依次到达接收窗口,但只有seq<=3的数据包可以交付给上层的TCP应用程序,应用程序会按序取走接收窗口里可交付的数据包
但seq=6的包不能紧挨着放在seq为3的包后面,而只能放到接收窗口里索引为6的位置上,中间留出两个位置,需要等待seq=4和seq=5的包到来之后,才会讲seq<=6的包一起交付给上层的应用程序.收到seq=6的包之后,内核回复ack=3+1,如果接下来先收到seq=5的包,此时内核还是回复ack=3+1,最后收到seq=4的包,此时内核才回复ack=6+1
TCP报文格式:
1.源端口
2.目的端口
3.序号Seq:
每次交互都使用序列号Seq
Seq字段使用32位整数
4.确认号Ack:
堆上次接收到的信息的确认号都放在Ack里
Ack字段使用32位整数
5.接收窗口大小:
代表自己的可用窗口大小,发送方发出的包的序列号不难超出这个窗口
6.交验和
7.其他
8.选项
9.填充
10.数据
建立了socket连接的应用程序,都被分配了一个固定大小的接收缓冲区.接收窗口是缓冲区的一部分.
如果上层应用程序从接收缓冲区取走数据的速度慢,那么缓冲区堆积,留给接收窗口的可用区域会变得越来越小.如果缓冲区堆积,留给接收窗口的可用区域会变得越来越小,
如果上层应用程序从接收缓冲区取走数据的速度快,那么接收窗口就会留出更多的可用空间
2.TCP状态变迁
1.客户端发起连接,对应着网络底层发出SYN包:
初始状态为:init
SYN标记置为1,报文字段的seq=x,这里x是系统随机生成的一个数,然后客户端连接被设置为Sync-Sent状态.
状态命名为Sync-Sent形象地解释了客户端刚刚的动作
2.服务端收到SYN包后,发送SYN+ACK包:
初始状态为:init
SYN+ACK标记都置为1,报文字段ack=x+1,seq=y,
这里y是系统随机生成的一个数,然后服务端连接被设置为Sync-Rcvd状态
状态命名为Sync-Rcvd(收到Syn包后)形象地说明是TCP连接建立的中间状态
3.客户端收到SYN+ACK包,先处理包中携带的ACK部分,确认自己最初的SYN包是否已经被收到的
并且,客户端发送ACK包:
ACK标记置为1,报文字段的ack=y+1,客户端连接被设置为EStablished状态
状态命名为Established状态形象说明客户端认为连接已经建立成功,可以开始传输数据
4.服务端收到ACK包,服务端连接被设置为Established状态
状态命名为Established状态形象说明服务端认为连接已经成功,可以开始传输数据
5.最终建立了一条TCP连接
3.TCP报文里定义了几个特殊的比特标记位:
1.如果SYN标记位为1,则说明这个报文是一个SYN类型的包,SYN包的数据部分是空的,只用于连接的建立
连接建立过程:
客户端发送SYN包:syn标记置为1,初始序列号seq=x
服务端收到SYN包后,发送ACK和自己的SYN包:
SYN被置为1,初始序列号seq=y;通过syn报文交换了初始序列号之后,客户端和服务端在传输数据的过程中会使用单调递增的序列号
如果ACK标记位为1,则说明这个报文是一个ACK类型的包,ACK用于确定某个数据包收到.
在上面的例子里,服务端收到SYN包,首先回复ACK,确认号ack=x+1,表示seq=x端包已经收到,期待下次收到seq=x+1的包.
ACK信息(ACK比特标记位和ACK确认号)一般可携带在其他类型的数据包里一起发送
2.其他3个比特标记位依次是:
1.RST标记位:
可用于强制断开链接
2.PSH标记位:
告知对方这些数据包收到后应马上交给上层的应用
3.FIN标记位:
用于关闭连接
4.TCP数据包重传机制:
超时重传机制:
问题:
场景1(ACK超时):
在数据传输过程中如果发生了ACK超时,客户端会将这个包重传
TCP报文重传采取指数加倍策略,而且因为TCP数据传输要保证不丢包,所以一般来说,重传次数没有设置上限,会不断重传被丢弃的包,直到正常交付(但有些系统的实现可能不一样,采用容忍度略低的处理方式,多次重传未果后可能会RESET重置该TCP连接)
场景2(丢包):
如果网络很顺畅,但是有一个包在传输过程中意外丢失了,而重传的超时时间相对来说又比较长,则这个丢失的包可能会阻塞之后的一批包的交付
客户端可能会收到好多次这个丢失包对应的ACK,比如如果收到10次相同的ACK(中间有一个包丢失,后续的包返回的ACK都是丢失包的ACK),则意味着这个包后续的10个包都阻塞在对方的接收窗口(因为要等那一次的ACK发生超时)
TCP超时重传机制:
如果请求发出去,在一段时间内没有收到响应就会重传消息
问题:
有一个同步数据的应用程序,需要连接很多HTTP服务,去同步一些数据到本地系统.但有时由于网络问题,连接到这些HTTP服务会出现连接超时的告警,当同步数据的客户端创建连接超时了,我发现常常是在127s左右后发出警告
问题:
那么多久时间后重传呢?
TCP将从发出一个包开始到接收到对应的ACK回执所需要的时间为RTT
网络的情况总是在变化的,所以RTT也是在动态变化的,内核会采样RTT的数据,用采样的RTT计算出一个重传的标准时间:RTO(retransmission timeout)
1.如果当前网络状态良好,RTT不会超过几毫秒,基本为1s
2.如果当前网络状态不好,则进行重试机制:
第一种:
固定时间间隔尝试策略.比如每隔1s重试一次.
第二种:
指数时间间隔尝试策略.比如1s、2s、4s...,一共重试6次
解决方式:
超时无应答后,因为系统设置的重传次数是6,RTO为1s,使用指数时间间隔尝试策略
所以依次重传等待的秒数为1、2、4、8、16、32、64.这些等待时间加起来大约是127s左右
快速重传机制(数据驱动):
收到3个重复的ACK,发生重传
例子:
如果当收到1、2后,回复ack3,随后收到4、5,但是还没收到3,这样4、5的ack也回复3,当3次发送一样的ack,会知道传输出了问题
缺点:
回传个数问题,如果一次发了20条,就不知道是哪3个发的ack了,需要回传这20条
sack重传:
选择性重传,tcp的头会多一个sack,快速重传的ack还在
sack只回复已经到达的碎片,这样发送端就能准确知道是重传哪部分字节流
5.发送窗口swnd和接收窗口rwnd:
有大批货物需要运送,如果只有一个卡车传送,那么效率很低
等收到前面包的 ACK 之后再发出下一个包,会使得吞吐率非常低
如果有10个卡车并发,就会快的多
当发送端同时发送很多包到网络上,这些还没有收到ACK的包需要缓存在发送方内核,另外还没交付给对端应用程序的包也需要先缓存起来
针对两种缓存的场景,内核分布引入了发送窗口swnd和接收窗口rwnd的概念,来记录在途数据的情况
发送方滑动窗口:在途发送数据
因为系统资源有限,发送窗口不能无限大.另外在传输过程中,发送窗口也会考虑接收方的接收能力,如果接收方处理不过来更大的吞吐量,发送方就应该减少发送窗口(发送窗口的大小不会超过接收窗口)
发送缓冲区根据数据的发送情况被划分为几个区域,发送端端socket缓冲区示意图:
发送缓冲区有两个指针将这块区域从右到左分为3个区域:
1.绿色区域位于ACK指针右边,是已发送并收到ACK确认的包,这里的包(序号1、2、3)将被内核清除掉
2.红色区域位于ACK指针左边,且在发送指针(发送窗口的起点)右边,这是已发送但未收到ACK确认的包(序号4、5、6)
3.蓝色区域位于发送指针左边,且是发送窗口内还没发送的包(序号7、8、9).蓝色区域是发送方的可用窗口(可用窗口越大,基本就意味着当前吞吐量越大)
发送窗口:是绿色区域和红色区域之和(发送窗口大小恒定)
ACK指针示意当前收到ACK的包,是发送窗口的起点.
发送指针示意当前可用发送的包,但不能超出发送窗口.
随着ACK指针移动,后续超出接收方处理范围的发送缓冲区的包便纳入发送窗口中
接收方接收窗口:在途顺序交付
ACK指针将接收缓冲区从右到左分为两个区域:
1.绿色区域位于ACK指针右边,是收到并发送来ACK确认的包,这个区域的包(序号1、2、3)在等待上层的应用程序来取
2.红色区域位于ACK指针左边,是当前的接收窗口.在接收窗口内的序号是当前可以接收的包(序号4、5、6、7、8、9),比如序号5到了,就放到5的位置,序号9到了,就放到9的位置
接收窗口:红色区域
接收窗口和应用程序获取数据的能力有关:
1.如果应用程序卡顿导致一直不从socket缓冲区获取数据,绿色区域会变大,则红色区域变得越来越小
2.如果相反应用程序取包很快,绿色区域会变小,红色区域变大
为了保证包到达的顺序,发送方会使用递增的序列号
假设接收端未收到编号4和5的包,而后面6-8编号的包都收到了,那ACK指针还是停留在4(下一个希望收到编号4的包),并不会到9.
当发送端超时重传(或者快速重传)4和5的包之后,接收端才会将ACK指针指向9.
6.流量控制机制:
TCP包头的16bit的窗口大小字段是接收方反馈给发送方的接收窗口大小,发送方根据收到的窗口大小调整自己的发送窗口大小,使得不会超过接收窗口的大小
于是,发送方滑动窗口和接收方窗口一起配合使得发送方的发送速度和接收方的处理速度相匹配,这就是TCP协议为应用程序提供的流量控制机制
好处:
1.一方面尽可能使得发送方能达到更大的吞吐量
2.一方面也消除接收方缓存溢出的可能性
7.TCP拥塞:
复杂的网络道路:易拥塞
在上线到线网环境后,传输的速度起伏比较大,没有测试环境那么稳定
计算机网络就像承载运输车辆的运输网络(包括高速公路、岔路口、国道和一般的公路)一样,包括各种终端、网线、路由器和交换机等。数据包从一个终端到大洋彼岸的另一个终端,可能会通过各种介质,比如无线 Wi-Fi、双绞线、海底光纤等.
不同的通道,比如双绞线和光纤的传输速率会不一致,对于一个数据包,它前面的比特到达一个路由器之后,会等待这个包后面的比特的到达,整个包都到达这个路由器之后再通过出链路传递到下一站.
如果到达路由器的数据包很多,那么它们在路由器上面还需要排队转发.
当队列时间长的时候,可能会出现ACK超时情况.
甚至如果一个路由器上面的包排队数量超过了路由器的队列大小,后来的包则会被丢弃
因为资源有限,大量的负载很容易造成排队超时,溢出丢弃
因为有丢包重传,计算机网络和普通公路比起来拥堵更甚,当拥塞和丢包发生的时候,整个网络会出现很多重传的包的流量
若出现拥塞而没有得到有效控制,拥塞情况就会变更严重,整个网络吞吐量不增反减
拥塞控制算法:
发送方使用发送窗口来发送数据
发送窗口的大小是一个变量swnd,它是另外另外两个变量函数:min(cwnd,rwnd)的结果
拥塞窗口大小cwnd:是发送方维护的一个状态变量,它根据网络的拥塞程度动态变化
接收窗口大小rwnd
拥塞窗口发展的变化:
1.慢开始-从小窗口加速到门限值
cwnd的值从一个很小的值开始,假设cwnd的初始窗口大小是1(一个TCP数据包),等收到这个包的ACK的时候,cwnd变为2,再然后收到这2个ACK的时候,cwnd变为4...
按照轮次来看,发送窗口大小的变化呈现指数级增长,虽然初始值比较小,但cwnd增长的速度比较快
直到增长到阈值ssthresh
每个连接到ssthresh初始默认是个无穷大的值,但是内核会cache对端ip上次sthresh(系统认为,上一次的历史带宽,是这次的期望带宽)
2.拥塞避免
如果还能没有感知到超时和重传,那么将会进入另外一个慢增长的阶段,称之为拥塞避免
当到达ssthresh的时候,cwnd的大小开始变为**线性加速**,当cwnd窗口内发出的包都收到ACK之后,cwnd窗口的值才加1
3.拥塞发生时
在拥塞避免阶段,窗口还是在慢慢增大,如果不丢包重现,网络吞吐量就还在增长
但是网络的容量毕竟是有限的,如果只增不减,网络一定会出现拥塞排队或者丢包
1.超时拥塞-急刹
从发送方来看,当出现ACK超时时:
在Reno版本的算法里,将ssthreash设置为当前拥塞窗口值cwnd的一半,并且将cwnd设置为1,重新进入慢启动阶段
2.快速重传拥塞-点刹
当ACK被快速重传的时候,也意味着有丢包事件的发生,但不一定是非常拥塞的网络导致的
将cwnd和ssthresh都设置为当前cwnd的一半,然后进入拥塞避免阶段
问题:
如果在慢开始阶段出现ACK超时事件,状态会如何改变呢?
应该是cwnd=1,然后重新开启慢启动过程
8.TCP四次挥手:
完善的关闭连接:
在高并发场景下,关闭连接就是要达到:一点多余的资源都不要占用
连接建立后,任意方都可以关闭连接:
1.A主动关闭连接.A发出第5个包Fin,状态从Established变为Fin-Wait1
2.B收到第5个包,发出第6个包Ack回执,状态从Established变为Close-Wait(服务端)
3.A收到第6个包,状态从Fin-Wait1变为Fin-Wait2(客户端)
4.直到处于Close-Wait状态的B发出第7个包Fin,B状态从Close-Wait变为Last-Ack
5.A收到第7个包,回复第8个包Ack回执,状态从Fin-Wait2变为Time-Wait
6.这以后B可能收到第8个包Ack回执,也可能超时没收第8个包Ack回执
1.如果B收到第8个包Ack回执,则连接变为Closed状态,A等待一定时间后关闭连接
2.如果超时没收到第8个包Ack回执(等待时间为Time-Wait),则重传第7个包Fin(也就是第9个包Fin)
3.A收到第9个包Fin后,重置Time-Wait的计时器
终止TCP连接时,需要客户端和服务端总共发送4个包以确定连接的断开
客户端终止TCP
客户端发送FIN,请求断开连接
服务器端针对客户端TCP断开的回应,并重新发送ack给客户端.客户端收到后,将不会再发送数据
服务端终止TCP
服务端发送FIN,请求断开连接
客户端针对服务器TCP断开的回应,并重新发送ack给服务器.服务器收到后,将不会再发送数据
5.浏览器缓存
1.强缓存:
当浏览器发起HTTP请求时,会向浏览器缓存进行一次询问,若浏览器缓存没有该资源的缓存数据,那么浏览器便会向服务器发起请求,服务器接收请求后将资源返回给浏览器,浏览器会将资源的响应数据存储到浏览器缓存中,这便是强缓存的生成过程
缓存失效策略划分为:
1.强缓存
2.协商缓存
缓存位置划分为:
1.Service Worker Cache
2.Memory Cache(内存)
3.Disk Cache(磁盘)
4.Push Cache
强缓存情况:
第一次打开首页时(浏览器未有缓存资源),打开开发者工具时,因为几乎每一个资源都需要从服务器获取并加载,所以网页打开速度会受影响,这里浏览器用了1.76s加载完了页面的所有资源(图片、脚本、样式等),1.1MB的数据被传输到本地
对应流程:
浏览器 浏览器缓存 服务器
浏览器向浏览器缓存发起HTTP请求,先向浏览器缓存询问
浏览器缓存告诉浏览器没有该资源的缓存数据
浏览器向服务器发起HTTP请求
服务器向浏览器返回资源响应数据和缓存标识
浏览器将该资源的缓存数据存入浏览器缓存
关闭Tab后,第二次访问首页时,原先的资源加载大小变成了disk cache(磁盘缓存),而变成这一数据对应的Time列资源加载速度异常之快,加载总耗时由原来的1.76s变成了1.1s,而传输到本地的数据降到了44.3KB,加载速度提升了37.5%
对应流程:
浏览器 浏览器缓存 服务器
浏览器发起HTTP请求,去浏览器缓存中寻找
浏览器缓存中存在请求的缓存数据,且未失效
不关闭Tab,重新刷新下某宝页面,Size中变为memory cache(内存缓存),其对应的Time列变成了0ms
查找顺序:
1、先查找内存,如果内存中存在,从内存中加载
2、如果内存中未查找到,选择硬盘获取,如果硬盘中有,从硬盘中加载
3、如果硬盘中未查找到,那就进行网络请求
4、加载到的资源缓存到硬盘和内存
强缓存是否新鲜 = 缓存新鲜度 > 缓存使用期
缓存新鲜度 = max-age || (expires - date)
date 表示创建报文的日期时间,可以理解为服务器(包含源服务器和代理服务器)返回新资源的时间,和 expires 一样是一个绝对时间,比如
max-age出现在响应中时,max-age是给出缓存过期的相对时间,单位为秒数(max-age和Expires同时出现时,max-age的优先级更高,但往往为了做向下兼容,两者都会经常出现在响应首部中)
缓存使用期可以理解为浏览器已经使用该资源的时间
缓存使用期主要与响应使用期、传输延迟时间和停留缓存时间有关:
缓存使用期 = 响应使用期 + 传输延迟时间 + 停留缓存时间
2.协商缓存
协商缓存:
浏览器发起HTTP请求后浏览器缓存发现该请求的资源失效,便将其缓存标识返回给浏览器,于是浏览器携带该缓存标识向服务器发起HTTP请求,之后服务器根据该标识判断这个资源其实没有更新过,最终返回304给浏览器,浏览器收到无更新的响应后便转向浏览器缓存获取数据
对应流程:
浏览器 浏览器缓存 服务器
浏览器发起http请求,向浏览器缓存寻找缓存资源
浏览器缓存表示该资源的缓存失效,只返回缓存标识给浏览器
浏览器携带该资源的缓存标识,向服务器发起HTTP请求
服务器标识304,该资源无更新,告知浏览器
浏览器向浏览器缓存获取该资源的缓存数据
浏览器缓存返回该资源的缓存数据给浏览器
缓存标识:
last-modified、eTag
eTag的优先级高于last-modified
只要有这两个缓存标识之一,在强缓存失效后,浏览器便会携带它们向服务器发起请求,
if-modified-since对应last-modified的值
if-none-match对应eTag的值
服务器根据优先级高的缓存标识的值进行判断
last-modified的弊端:
1.last-modified是一个时间,最小单位为秒,如果资源的修改时间非常快,快到毫秒级别,那么服务器会误认为该资源仍然是没有修改的,这便导致了资源无法在浏览器及时更新的现象
2.另一种情况,服务器资源确实被编辑了,但是其实资源的实质内容并没有被修改,那么服务器还是会返回最新的last-modified时间值,但是我们并不希望浏览器认为这个资源被修改而重新加载
eTag原理:
生存eTag值的两种方式(生成强eTag值):
1.使用文件大小和修改时间(当处理的内容是文件stats对象时,就会用这个方法生成eTag值,最后返回的值是由文件大小和文件最后一次修改时间组成的字符串)
2.使用文件内容的hash值和内容长度(当处理的内容是非stats对象时,通过对内容的hash转化和截取,最终返回内容长度和其hash值组成的字符串)
3.启发式缓存:
强缓存新鲜度的公式:
缓存新鲜度 = max-age || (expires - date)
当没有max-age(s-maxage)和expires这两个关键字时,强缓存的新鲜度如何计算?
例如:
下发的响应报头:
date: Thu, 02 Sep 2021 13:28:56 GMT
age: 10467792
cache-control: public
last-modified: Mon, 26 Apr 2021 09:56:06 GMT
以上报头中没有用来确定强缓存过期时间的字段,这便无法使用上面提到的缓存新鲜度公式,虽然有和协商缓存相关的last-modified首部,但不会走协商缓存,反而浏览器会触发启发式缓存
启发式缓存:
缓存新鲜度 = max(0,(date - last-modified)) * 10%
通常会根据响应头中的两个时间字段Date减去Last-Modified值的10%作为缓存时间
6.浏览器的执行机制
1.js是单线程:
js的用途:
主要用途是和用户互动,以及操作DOM
如果是多线程:
一个线程在某个dom节点上添加内容,另一个线程删除这个节点,这时浏览器应该以哪个线程为准?
2.
macro-task:宏任务
micro-task:微任务
3.
宏任务队列:
当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候,都会执行队列中的每个队列
在每次迭代开始之后,加入到队列中的任务需要在下一次迭代开始之后才会被执行
微任务队列:
每次当一个宏任务退出且执行上下文为空的时候,微任务队列中的每个微任务会依次被执行
不同之处:
即使中途有微任务加入,也要在下一个宏任务开始之前执行完所有的微任务
4.进程:
进程是对正在运行中的程序的一个抽象
进程是某一类特定活动的综合,它有程序、输入输出以及状态
单个处理器(CPU)可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务
5.线程:
进程是CPU资源分配的最小单位
一进程由一个或多个线程组成,线程可以理解为是一个进程中代码的不同执行路线
6.浏览器是多进程的:
拿chrome来说,每打开一个tab页就会产生一个进程,我们使用chrome打开很多标签页不关,电脑会越来越卡,很耗CPU
浏览器包含哪些进程:
1.Browser进程:
1.1浏览器的主进程(负责协调、主控),该进程只有一个
1.2负责浏览器界面显示,和用户交互.如前进、后退等
1.3负责各个页面的管理,创建和销毁其他进程
1.4将渲染进程得到的内存中的Bitmap(位图),绘制到用户界面上
1.5网络资源的管理,下载等
2.第三方插件进程:
2.1每种类型的插件对于一个进程,当使用该插件时才创建
3.GPU进程:
3.1该进程只有一个,用于3D绘制等等
4.渲染进程(重):
4.1即通常所说的浏览器内核(Renderer进程,内部是多线程)
4.2每个Tab页面都有一个渲染进程,互不影响
4.3主要作用为页面渲染,脚本执行,事件处理等
浏览器多进程的原因:
假设浏览器是单进程,那么tab页崩溃,就影响整个浏览器
同理如果插件崩溃,也会影响整个浏览器
渲染进程(包含的线程)
1.GUI渲染线程:
1.1负责渲染浏览器界面,解析HTMl、CSS,构建DOM树和RenderObject树,布局和绘制等
1.1.1解析html代码(HTML代码本质是字符串)转化为浏览器认识的节点,生成DOM树,也就是DOM Tree.
1.1.2解析css,生成CSSDOM(CSS规则树)
1.1.3把DOM Tree和CSSOM结合,生成Rendering Tree(渲染树)
1.2当我们修改一些元素的颜色或者背景色时,页面就会重绘
1.3当我们修改元素的尺寸,页面就会重排
1.4当页面需要重排和重绘时,GUI线程就会执行,绘制页面
1.5重排比重绘的成本要高,重排一定导致重绘
1.6GUI渲染线程和JS引擎线程是互斥的,一个被使用时,另一个被挂起
每次任务队列中,JS引擎空闲时,会执行GUI渲染线程
2.JS引擎线程(js线程):
2.1Js引擎线程就是JS内核,负责处理javascript脚本程序
2.2Js引擎一直等待着任务队列中任务的到来,然后加以处理(浏览器中只有一个JS引擎线程)
2.3在浏览器渲染的时候,遇到<script>标签,就会停止GUI的渲染,然后js引擎线程开始工作,执行里面的js代码,等js执行完毕,js引擎线程停止工作,GUI继续渲染下面的内容.
所以如果js执行时间过长就会页面卡顿
3.事件触发线程(协助js线程,处理一些等待任务)
3.1用来控制事件循环,并且管理着一个事件队列
3.2当js遇到事件和一些异步操作(如setTimeout,点击事件添加到定时器线程),等异步事件有了结果,便把他们的的回调操作添加到事件队列,等待js引擎线程空闲时来处理
3.3当对应的回调事件符合触发条件时,该线程会把事件添加到待处理队列的队尾,等待js引擎线程空闲时来处理
4.定时触发器线程
4.1setInterval和setTimeout所在线程
4.2浏览器定时计数器并不是由js引擎计数的(因为js引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确)
4.3通过单独线程来计时并触发定时(计时完毕后,添加到事件触发线程的事件队列中,等待js引擎空闲后执行),这个线程就是定时触发器线程,也叫定时器线程.
5.异步http请求线程
5.1在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
5.2将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中由js引擎执行
6.总结:
简单来说,当执行到一个http异步请求后,就把异步请求事件添加到异步请求线程,
等到http状态变化,再把回调函数添加到事件队列,等待js引擎线程来执行
事件循环(Event Loop):
js分为同步任务和异步任务
同步任务都在主线程(js引擎线程)上执行,会形成一个执行栈
事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列中放一个事件回调
一旦执行栈中的所有同步任务执行完毕(也就是js引擎线程空闲),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行.
流程:
1.执行栈开始顺序执行
2.判断是否为同步,异步则进入异步线程,最终事件回调给事件触发线程的任务队列等待执行,同步继续执行
3.执行栈空,询问任务队列中是否有事件回调
4.任务队列中有事件回调则回调加入执行栈末尾继续从第一步开始执行
5.任务队列中没有事件回调则不停发起询问
宏任务和微任务
宏任务:
我们可以把每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行),每一宏任务会从头到尾执行完毕,不会执行其他.
微任务:
当前宏任务执行后立即执行的任务
由于js引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染.
宏任务 -> 微任务 -> GUI渲染(页面渲染) -> 宏任务 -> 微任务
完整的Event Loop:
首先,整体的script(作为第一个宏任务)开始执行,会把所有代码分为同步代码、异步代码两部分
同步任务会直接进入主线程依次执行
异步任务会分为宏任务和微任务
宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务
layout(构建布局模型)
paint(绘制图层样式)
composite(组合计算渲染呈现结果)
上述过程会不断重复,这就是Event Loop.比较完整的事件循环
只有异步任务分宏任务和微任务
参考资源:
1.掘金小册-图解网络协议
2.掘金小测-前端缓存技术与方案